ui: PagedCollection component (#12404)

* ui: PagedCollection component

* ui: Use PagedCollection (#12436)

* ui: Integrate PagedCollection into DisclosureMenu

* Integrate PageCollection into DC, Nspace and Partition menus
pull/12453/head
John Cowen 2022-02-25 10:01:08 +00:00 committed by GitHub
parent 79a07c7a3d
commit 121bd2e0ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 424 additions and 62 deletions

View File

@ -2,18 +2,26 @@
{{#if (can "choose nspaces")}}
{{#let
(or @nspace 'default')
as |nspace|}}
(is-href 'dc.nspaces' @dc.Name)
as |nspace isManaging|}}
<li
class="nspaces"
data-test-nspace-menu
>
<DisclosureMenu
aria-label="Namespace"
@items={{append
(hash
Name="Manage Namespaces"
href=(href-to 'dc.nspaces' @dc.Name)
)
(reject-by 'DeletedAt' @nspaces)
}}
as |disclosure|>
<disclosure.Action
{{on 'click' disclosure.toggle}}
>
{{nspace}}
{{if isManaging 'Manage Namespaces' nspace}}
</disclosure.Action>
<disclosure.Menu as |panel|>
{{#if (gt @nspaces.length 0)}}
@ -40,34 +48,39 @@
/>
{{/if}}
<panel.Menu as |menu|>
{{#each (reject-by 'DeletedAt' @nspaces) as |item|}}
{{#each menu.items as |item|}}
<menu.Item
aria-current={{if (eq nspace item.Name) 'true'}}
data-test-main-nav-nspaces={{not-eq item.href undefined}}
aria-current={{if
(or
(and isManaging item.href)
(and (not isManaging) (eq nspace item.Name))
)
'true'
}}
>
<menu.Action
{{on 'click' disclosure.close}}
@href={{href-to '.' params=(hash
partition=(if (gt @partition.length 0) @partition undefined)
nspace=item.Name
)}}
@href={{if item.href
item.href
(if isManaging
(href-to 'dc.services.index' params=(hash
partition=(if (gt @partition.length 0) @partition undefined)
nspace=item.Name
dc=@dc.Name
))
(href-to '.' params=(hash
partition=(if (gt @partition.length 0) @partition undefined)
nspace=item.Name
))
)
}}
>
{{item.Name}}
</menu.Action>
</menu.Item>
{{/each}}
{{#if (can 'manage nspaces')}}
<menu.Separator />
<menu.Item
data-test-main-nav-nspaces
>
<menu.Action
{{on 'click' disclosure.close}}
@href={{href-to 'dc.nspaces' @dc.Name}}
>
Manage Namespaces
</menu.Action>
</menu.Item>
{{/if}}
</panel.Menu>
</disclosure.Menu>
</DisclosureMenu>

View File

@ -1,6 +1,7 @@
{{#let
(or @partition 'default')
as |partition|}}
(is-href 'dc.partitions' @dc.Name)
as |partition isManaging|}}
{{#if (can "choose partitions" dc=@dc)}}
<li
class="partitions"
@ -8,11 +9,18 @@ as |partition|}}
>
<DisclosureMenu
aria-label="Admin Partition"
@items={{append
(hash
Name="Manage Partitions"
href=(href-to 'dc.partitions' @dc.Name)
)
(reject-by 'DeletedAt' @partitions)
}}
as |disclosure|>
<disclosure.Action
{{on 'click' disclosure.toggle}}
>
{{partition}}
{{if isManaging 'Manage Partition' partition}}
</disclosure.Action>
<disclosure.Menu as |panel|>
<DataSource
@ -25,34 +33,37 @@ as |partition|}}
@onchange={{fn (optional @onchange)}}
/>
<panel.Menu as |menu|>
{{#each (reject-by 'DeletedAt' @partitions) as |item|}}
{{#each menu.items as |item|}}
<menu.Item
class={{if (eq partition item.Name) 'is-active'}}
aria-current={{if
(or
(and isManaging item.href)
(and (not isManaging) (eq partition item.Name))
)
'true'
}}
>
<menu.Action
{{on 'click' disclosure.close}}
@href={{href-to '.' params=(hash
partition=item.Name
nspace=undefined
)}}
@href={{if item.href
item.href
(if isManaging
(href-to 'dc.services.index' params=(hash
partition=item.Name
nspace=undefined
dc=@dc.Name
))
(href-to '.' params=(hash
partition=item.Name
nspace=undefined
))
)
}}
>
{{item.Name}}
</menu.Action>
</menu.Item>
{{/each}}
{{#if (can 'manage partitions')}}
<menu.Separator />
<menu.Item
data-test-main-nav-partitions
>
<menu.Action
{{on 'click' disclosure.close}}
@href={{href-to 'dc.partitions.index' @dc.Name}}
>
Manage Partitions
</menu.Action>
</menu.Item>
{{/if}}
</panel.Menu>
</disclosure.Menu>
</DisclosureMenu>

View File

@ -4,6 +4,7 @@
>
<DisclosureMenu
aria-label="Datacenter"
@items={{sort-by 'Name' @dcs}}
as |disclosure|>
<disclosure.Action
{{on 'click' disclosure.toggle}}
@ -16,7 +17,7 @@
@onchange={{action (mut @dcs) value="data"}}
/>
<panel.Menu as |menu|>
{{#each (sort-by 'Name' @dcs) as |item|}}
{{#each menu.items as |item|}}
<menu.Item
aria-current={{if (eq @dc.Name item.Name) 'true'}}
class={{class-map

View File

@ -38,14 +38,12 @@ common usecase of having a floating menu.
<DisclosureMenu as |disclosure|>
<disclosure.Action
{{on 'click' disclosure.toggle}}
{{css-prop 'height' returns=(set this 'height')}}
>
{{if disclosure.expanded 'Close' 'Open'}}
</disclosure.Action>
<disclosure.Menu
style={{style-map
(array 'position' 'absolute')
(array 'top' this.height)
(array 'background-color' 'rgb(var(--tone-gray-000))')
}}
as |panel|>
@ -62,11 +60,53 @@ common usecase of having a floating menu.
</figure>
```
`DisclosureMenu` also supports virtually scrolling its menu items for when you have 1000s of items to display in the menu whilst avoiding DOM stuttering. The set up is a tinsy bit more involved. but eesnetially you provide the data items for the menu using the `@items` argument, and then you can loop through these in the menu to make/use your menu items. The `menu.items` property is automatically paged for you depending on the scroll position of the menu panel. Importantly, right now, you should provide a height value for each menu.item using the `--paged-row-height` CSS property, you can do this inline or within your CSS (preferred). If you don't do this the component is unable to calculate the size of the scroll track/thumb. In the future (when needed) we will provide a callback for each item so you can specify a function to calculate the size of each individual item to give us a little more flexibility on what we can do with this component.
```hbs preview-template
<DataSource
@src={{uri
'/${partition}/${nspace}/${dc}/nodes'
(hash
nspace=''
partition=''
dc='dc-1'
)
}}
as |source|>
<DisclosureMenu
@items={{source.data}}
as |disclosure|>
<disclosure.Action
{{on 'click' disclosure.toggle}}
>
{{if disclosure.expanded 'Close' 'Open'}}
</disclosure.Action>
<disclosure.Menu
style={{style-map
(array 'position' 'absolute')
(array 'max-height' '360' 'px')
(array 'width' '560' 'px')
(array '--paged-row-height' '42px')
}}
as |panel|>
<panel.Menu as |menu|>
{{#each menu.items as |item|}}
<menu.Item>
<menu.Action>{{item.Node}}</menu.Action>
</menu.Item>
{{/each}}
</panel.Menu>
</disclosure.Menu>
</DisclosureMenu>
</DataSource>
```
## Arguments
| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `expanded` | `Boolean` | false | The _initial_ state of the disclosure. Please note: this is the _initial_ state only, please use the `disclosure.open` and `disclosure.close` for controling the state. |
| `items` | `object[]` | | When using a paginated menu you should add all possible items to this arguments and then uses the disclosure.Panel.Menu.items property for `each`ing through, see above example |
## Exported API

View File

@ -9,7 +9,11 @@
as |disclosure|>
{{yield (hash
Action=(component 'disclosure-menu/action' disclosure=disclosure)
Menu=(component 'disclosure-menu/menu' disclosure=disclosure)
Menu=(component 'disclosure-menu/menu'
disclosure=disclosure
items=@items
rowHeight=@rowHeight
)
disclosure=disclosure
toggle=disclosure.toggle
close=disclosure.close

View File

@ -3,4 +3,6 @@
}
.disclosure-menu [aria-expanded] ~ * {
@extend %menu-panel;
overflow-y: auto !important;
will-change: scrollPosition;
}

View File

@ -1,11 +1,25 @@
<@disclosure.Details as |details|>
<div
{{on-outside 'click' @disclosure.close}}
...attributes
>
{{yield (hash
Menu=(component 'menu' disclosure=@disclosure)
)}}
</div>
<PagedCollection
@items={{or @items (array)}}
as |pager|>
<div
{{on-outside 'click' @disclosure.close}}
{{did-insert pager.viewport}}
{{on-resize pager.resize}}
{{css-prop '--paged-row-height' returns=pager.rowHeight}}
{{css-prop 'max-height' returns=pager.maxHeight}}
class={{class-map
(array 'paged-collection-scroll' (contains pager.type (array 'virtual-scroll' 'native-scroll')))
}}
...attributes
>
{{yield (hash
Menu=(component 'menu'
disclosure=@disclosure
pager=pager
)
)}}
</div>
</PagedCollection>
</@disclosure.Details>

View File

@ -3,14 +3,21 @@
@extend %main-nav-vertical-hoisted;
left: 100px;
}
nav .dcs .menu-panel {
min-width: 250px;
}
nav li.partitions,
nav li.nspaces {
@extend %main-nav-vertical-popover-menu;
/* --panel-height: 300px;
--row-height: 43px; */
}
nav li.dcs [aria-expanded] ~ * {
min-width: 250px;
}
nav li.dcs [aria-expanded] ~ * {
max-height: 560px;
--paged-row-height: 43px;
}
nav li.partitions [aria-expanded] ~ *,
nav li.nspaces [aria-expanded] ~ * {
max-height: 360px;
--paged-row-height: 43px;
}
[role='banner'] a svg {

View File

@ -1,8 +1,10 @@
<ul
role="menu"
aria-labelledby={{@disclosure.button}}
id={{@disclosure.panel}}
...attributes
style={{{style-map
(array 'height' (if (and @pager (not-eq @pager.type 'native-scroll')) @pager.totalHeight) 'px')
(array '--paged-start' (if (and @pager (not-eq @pager.type 'native-scroll')) @pager.startHeight) 'px')
}}}
{{did-insert (optional @pager.pane)}}
{{aria-menu
onclose=(or @onclose @disclosure.close)
openEvent=(or @event @disclosure.event)
@ -12,5 +14,6 @@
Action=(component 'menu/action' disclosure=@disclosure)
Item=(component 'menu/item')
Separator=(component 'menu/separator')
items=@pager.items
)}}
</ul>

View File

@ -0,0 +1,109 @@
# PagedCollection
A renderless component to act as a helper for different types of pagination.
```hbs preview-template
<figure>
<figcaption>
Provide a widget so we can try switching between two pagination methods
</figcaption>
<select
onchange={{action (mut this.type) value="target.value"}}
>
<option>virtual-scroll</option>
<option>index</option>
</select>
</figure>
<figure>
<figcaption>Get some data and page it</figcaption>
<DataSource
@src={{uri "/partition/default/dc-1/nodes"}}
as |source|>
<PagedCollection
@type={{or this.type "virtual-scroll"}}
@items={{or source.data (array)}}
@perPage={{8}}
@page={{or this.page 1}}
@rowHeight="43"
as |pager|>
<div
style={{{style-map
(array 'outline' '1px solid rgb(var(--tone-gray-300))')
(array 'max-height' '360' 'px')
(array '--paged-row-height' (if (not-eq this.type 'index') '43px'))
}}}
{{did-insert pager.viewport}}
{{on-resize pager.resize}}
>
<ul
style={{{style-map
(array 'height' pager.totalHeight 'px')
(array '--paged-start' pager.startHeight 'px')
}}}
>
{{#each pager.items as |item|}}
<li
style={{{style-map
(array 'height' '43' 'px')
(array 'outline' '1px solid rgb(var(--tone-gray-100))')
}}}
>
{{item.Node}}
</li>
{{/each}}
</ul>
</div>
<pager.Pager>
<div
style={{{style-map
(array 'display' 'flex')
(array 'flex-direction' (if (not-eq pager.page pager.totalPages) 'row-reverse'))
(array 'justify-content' 'space-between')
}}}
>
{{#if (not-eq pager.page pager.totalPages)}}
<Action
{{on 'click' (set this 'page' (add pager.page 1))}}
>
Next
</Action>
{{/if}}
{{#if (not-eq pager.page 1)}}
<Action
{{on 'click' (set this 'page' (sub pager.page 1))}}
>
Prev
</Action>
{{/if}}
</div>
</pager.Pager>
</PagedCollection>
</DataSource>
</figure>
```
## Arguments
| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | `(native-scroll \| virtual-scroll \| index)` | `native-scroll` | The type of pagination |
| `items` | `array` | `undefined` | An array or items to be paginated |
| `rowHeight` | `(string \| number)` | `undefined` | When `@type=virtual-scroll` this informs the scroller of the size of each row in the scroll pane. For the moment this _must_ be the same for every row. |
| `page` | `number` | `undefined` | When `@type=index` this is the current page number to show |
| `perPage` | `number` | `undefined` | When `@type=index` this is the amount of rows to show per page |
## Exported API
| Name | Type | Description |
| --- | --- | --- |
| `items` | `array` | An array of the items to be shown on the current page |
| `page` | `number` | The current page number (a mirror of @page) |
| `resize` | `Function` | Function to be called on the resize of the viewport |
| `viewport` | `Function` | Function to be called on the `did-insert` of the viewport to be used for scrolling |
| `rowHeight` | `Function` | Function to be called to set the rowHeight of the virtual-scroller |
| `startHeight` | `number` | Size of the area before the panel to be virtually-scroller, usually you should use this to set `--paged-start` wrapping element of the scrollable items |
| `totalHeight` | `number` | Size of the of the entire panel in order to show the correctly sized scroll thumb |
| `totalPages` | `number` | Totol number of pages |

View File

@ -0,0 +1,15 @@
{{yield (hash
items=this.items
page=@page
pane=(fn this.setPane)
resize=(fn this.resize)
viewport=(fn this.setViewport)
rowHeight=(fn this.setRowHeight)
maxHeight=(fn this.setMaxHeight)
startHeight=this.startHeight
totalHeight=this.totalHeight
totalPages=this.totalPages
Pager=(if (eq type "index") (component 'yield') '')
)}}
{{will-destroy this.disconnect}}

View File

@ -0,0 +1,125 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class PagedCollectionComponent extends Component {
@tracked $pane;
@tracked $viewport;
@tracked top = 0;
@tracked visibleItems = 0;
@tracked overflow = 10;
@tracked _rowHeight = 0;
@tracked _type = 'native-scroll';
get type() {
return this.args.type || this._type;
}
get items() {
return this.args.items.slice(this.cursor, this.cursor + this.perPage);
}
get perPage() {
switch (this.type) {
case 'virtual-scroll':
return this.visibleItems + (this.overflow * 2);
case 'index':
return parseInt(this.args.perPage);
}
// 'native-scroll':
return this.total;
}
get cursor() {
switch (this.type) {
case 'virtual-scroll':
return this.itemsBefore;
case 'index':
return (parseInt(this.args.page) - 1) * this.perPage;
}
// 'native-scroll':
return 0;
}
get itemsBefore() {
if(typeof this.$viewport === 'undefined') {
return 0;
}
return Math.max(0, Math.round(this.top / this.rowHeight) - this.overflow);
}
get rowHeight() {
return parseFloat(this.args.rowHeight || this._rowHeight);
}
get startHeight() {
switch (this.type) {
case 'virtual-scroll':
return Math.min(this.totalHeight, this.itemsBefore * this.rowHeight);
case 'index':
return 0;
}
// 'native-scroll':
return 0;
}
get totalHeight() {
return this.total * this.rowHeight;
}
get totalPages() {
return Math.ceil(this.total / this.perPage);
}
get total() {
return this.args.items.length;
}
@action
scroll(e) {
this.top = this.$viewport.scrollTop;
}
@action
resize() {
if(this.$viewport.clientHeight > 0 && this.rowHeight > 0) {
this.visibleItems = Math.ceil(this.$viewport.clientHeight / this.rowHeight);
} else {
this.visibleItems = 0;
}
}
@action
setViewport($viewport) {
this.$viewport = $viewport === 'html' ? [...document.getElementsByTagName('html')][0] : $viewport;
this.$viewport.addEventListener('scroll', this.scroll);
if($viewport === 'html') {
this.$viewport.addEventListener('resize', this.resize);
}
this.scroll();
this.resize();
}
@action setPane($pane) {
this.$pane = $pane;
}
@action setRowHeight(str) {
this._rowHeight = parseFloat(str);
}
@action setMaxHeight(str) {
const maxHeight = parseFloat(str);
if(!isNaN(maxHeight)) {
this._type = 'virtual-scroll';
}
}
@action
disconnect() {
this.$viewport.removeEventListener('scroll', this.scroll);
this.$viewport.removeEventListener('resize', this.resize);
}
}

View File

@ -0,0 +1,16 @@
%paged-collection-scroll {
overflow-y: auto !important;
will-change: scrollPosition;
}
.paged-collection-scroll {
@extend %paged-collection-scroll;
}
[style*="--paged-row-height"] {
@extend %paged-collection-scroll;
}
[style*="--paged-start"]::before {
content: '';
display: block;
height: var(--paged-start);
}

View File

@ -0,0 +1 @@
{{yield}}

View File

@ -27,6 +27,7 @@
@import 'consul-ui/components/oidc-select';
@import 'consul-ui/components/radio-card';
@import 'consul-ui/components/panel';
@import 'consul-ui/components/paged-collection';
@import 'consul-ui/components/pill';
@import 'consul-ui/components/popover-menu';
@import 'consul-ui/components/popover-select';