Skip to content

Commit

Permalink
feat: new Data Table Column Sort feature
Browse files Browse the repository at this point in the history
  • Loading branch information
hperrin committed Apr 17, 2021
1 parent 236f6d2 commit c831855
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 16 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -281,7 +281,7 @@ New components from the upstream library to build for SMUI v3:
- [x] Touch Target Wrappers
- [x] Data Table Pagination
- [x] Data Table Progress Indicator
- [ ] Data Table Column Sort Buttons
- [x] Data Table Column Sort Buttons

<sub>† This is Sass based, and therefore doesn't require Svelte components. I've included a demo showing how you can use it.</sub>

Expand Down
2 changes: 2 additions & 0 deletions packages/common/CommonLabel.svelte
Expand Up @@ -13,6 +13,8 @@
'mdc-segmented-button__label': context === 'segmented-button',
'mdc-data-table__pagination-rows-per-page-label':
context === 'data-table:pagination',
'mdc-data-table__header-cell-label':
context === 'data-table:sortable-header-cell',
})}
{...context === 'snackbar' ? { 'aria-atomic': 'false' } : {}}
{tabindex}
Expand Down
39 changes: 37 additions & 2 deletions packages/data-table/Cell.svelte
Expand Up @@ -8,13 +8,33 @@
'mdc-data-table__header-cell': true,
'mdc-data-table__header-cell--numeric': numeric,
'mdc-data-table__header-cell--checkbox': checkbox,
'mdc-data-table__header-cell--with-sort': sortable,
'mdc-data-table__header-cell--sorted': sortable && $sort === columnId,
...internalClasses,
})}
on:change={(event) => checkbox && notifyHeaderChange(event)}
role="columnheader"
scope="col"
data-column-id={columnId}
aria-sort={sortable ? ($sort === columnId ? $sortDirection : 'none') : null}
{...internalAttrs}
{...$$restProps}><slot /></th
{...$$restProps}
>{#if sortable}
<div class="mdc-data-table__header-cell-wrapper">
<slot />
<div
class="mdc-data-table__sort-status-label"
aria-hidden="true"
id="{columnId}-status-label"
>
{$sort === columnId
? $sortDirection === 'ascending'
? sortAscendingAriaLabel
: sortDescendingAriaLabel
: ''}
</div>
</div>
{:else}<slot />{/if}</th
>
{:else}
<td
Expand All @@ -39,7 +59,7 @@
</script>

<script>
import { onMount, getContext } from 'svelte';
import { onMount, getContext, setContext } from 'svelte';
import { get_current_component } from 'svelte/internal';
import {
forwardEventsBuilder,
Expand All @@ -58,10 +78,25 @@
export let numeric = false;
export let checkbox = false;
export let columnId = header ? 'SMUI-data-table-column-' + counter++ : null;
export let sortable = getContext('SMUI:data-table:sortable');
let element;
let internalClasses = {};
let internalAttrs = {};
let sort = getContext('SMUI:data-table:sort');
let sortDirection = getContext('SMUI:data-table:sortDirection');
let sortAscendingAriaLabel = getContext(
'SMUI:data-table:sortAscendingAriaLabel'
);
let sortDescendingAriaLabel = getContext(
'SMUI:data-table:sortDescendingAriaLabel'
);
if (sortable) {
setContext('SMUI:label:context', 'data-table:sortable-header-cell');
setContext('SMUI:icon-button:context', 'data-table:sortable-header-cell');
setContext('SMUI:icon-button:aria-describedby', columnId + '-status-label');
}
onMount(() => {
const accessor = {
Expand Down
44 changes: 32 additions & 12 deletions packages/data-table/DataTable.svelte
Expand Up @@ -42,16 +42,15 @@
</div>

{#if $$slots.progress}
<div class="mdc-data-table__progress-indicator">
<div
class="mdc-data-table__progress-indicator"
style={Object.entries(progressIndicatorStyles)
.map(([name, value]) => `${name}: ${value};`)
.join(' ')}
>
<div class="mdc-data-table__scrim" />
<slot name="progress" />
</div>
{#if !$progressClosed}
<!--
MDC docs put this under mdc-data-table__progress-indicator,
but then it doesn't cover the table, so I think it goes here.
-->
<div class="mdc-data-table__scrim" />
{/if}
{/if}

<slot name="paginate" />
Expand All @@ -78,6 +77,11 @@
let className = '';
export { className as class };
export let stickyHeader = false;
export let sortable = false;
export let sort = null;
export let sortDirection = 'ascending';
export let sortAscendingAriaLabel = 'sorted, ascending';
export let sortDescendingAriaLabel = 'sorted, descending';
export let container$use = [];
export let container$class = '';
export let table$use = [];
Expand All @@ -89,14 +93,28 @@
let header;
let body;
let internalClasses = {};
let progressIndicatorStyles = {};
let addLayoutListener = getContext('SMUI:addLayoutListener');
let removeLayoutListener;
let postMount = false;
let progressClosed = writable(false);
let sortStore = writable(sort);
let sortDirectionStore = writable(sortDirection);
setContext('SMUI:checkbox:context', 'data-table');
setContext('SMUI:linear-progress:context', 'data-table');
setContext('SMUI:linear-progress:closed', progressClosed);
setContext('SMUI:data-table:sortable', sortable);
setContext('SMUI:data-table:sort', sortStore);
setContext('SMUI:data-table:sortDirection', sortDirectionStore);
setContext('SMUI:data-table:sortAscendingAriaLabel', sortAscendingAriaLabel);
setContext(
'SMUI:data-table:sortDescendingAriaLabel',
sortDescendingAriaLabel
);
$: $sortStore = sort;
$: $sortDirectionStore = sortDirection;
if (addLayoutListener) {
removeLayoutListener = addLayoutListener(layout);
Expand Down Expand Up @@ -136,6 +154,8 @@
header.orderedCells[index].removeClass(className);
},
notifySortAction: (data) => {
sort = data.columnId;
sortDirection = data.sortValue;
dispatch(getElement(), 'MDCDataTable:sorted', data);
},
getTableContainerHeight: () => container.getBoundingClientRect().height,
Expand All @@ -148,8 +168,8 @@
}
return tableHeader.getBoundingClientRect().height;
},
setProgressIndicatorStyles: (_styles) => {
/* Not Implemented. */
setProgressIndicatorStyles: (styles) => {
progressIndicatorStyles = styles;
},
addClassAtRowIndex: (rowIndex, className) => {
body.orderedRows[rowIndex].addClass(className);
Expand Down Expand Up @@ -224,7 +244,7 @@
}
},
setSortStatusLabelByHeaderCellIndex: (_columnIndex, _sortValue) => {
/* Not Implemented. */
// Handled automatically.
},
});
Expand Down Expand Up @@ -270,7 +290,7 @@
}
const headerCell = closest(
event.target,
event.detail.target,
'.mdc-data-table__header-cell--with-sort'
);
Expand Down
4 changes: 4 additions & 0 deletions packages/icon-button/IconButton.svelte
Expand Up @@ -27,6 +27,8 @@
'mdc-top-app-bar__action-item': context === 'top-app-bar:action',
'mdc-snackbar__dismiss': context === 'snackbar:actions',
'mdc-data-table__pagination-button': context === 'data-table:pagination',
'mdc-data-table__sort-icon-button':
context === 'data-table:sortable-header-cell',
...internalClasses,
})}
style={Object.entries(internalStyles)
Expand All @@ -37,6 +39,7 @@
aria-label={pressed ? ariaLabelOn : ariaLabelOff}
data-aria-label-on={ariaLabelOn}
data-aria-label-off={ariaLabelOff}
aria-describedby={ariaDescribedby}
on:click={() => instance && instance.handleClick()}
on:click={() =>
context === 'top-app-bar:navigation' &&
Expand Down Expand Up @@ -81,6 +84,7 @@
let internalStyles = {};
let internalAttrs = {};
let context = getContext('SMUI:icon-button:context');
let ariaDescribedby = getContext('SMUI:icon-button:aria-describedby');
export let component = href == null ? Button : A;
Expand Down
82 changes: 82 additions & 0 deletions site/src/routes/demo/data-table/_Sortable.svelte
@@ -0,0 +1,82 @@
<DataTable
sortable
bind:sort
bind:sortDirection
on:MDCDataTable:sorted={handleSort}
table$aria-label="User list"
style="width: 100%;"
>
<Head>
<Row>
<!--
Note that whatever you supply to "columnId" is
appended with "-status-label" and used as an ID
for the hidden label that describes the sort
status to screen readers.
You can localize those labels with the
"sortAscendingAriaLabel" and
"sortDescendingAriaLabel" props on the DataTable.
-->
<Cell numeric columnId="id">
<!-- For numeric columns, icon comes first. -->
<IconButton class="material-icons">arrow_upward</IconButton>
<Label>ID</Label>
</Cell>
<Cell columnId="name" style="width: 100%;">
<Label>Name</Label>
<!-- For non-numeric columns, icon comes second. -->
<IconButton class="material-icons">arrow_upward</IconButton>
</Cell>
<Cell columnId="username">
<Label>Username</Label>
<IconButton class="material-icons">arrow_upward</IconButton>
</Cell>
<Cell columnId="email" l>
<Label>Email</Label>
<IconButton class="material-icons">arrow_upward</IconButton>
</Cell>
<!-- You can turn off sorting for a column. -->
<Cell sortable={false}>Website</Cell>
</Row>
</Head>
<Body>
{#each items as item (item.id)}
<Row>
<Cell numeric>{item.id}</Cell>
<Cell>{item.name}</Cell>
<Cell>{item.username}</Cell>
<Cell>{item.email}</Cell>
<Cell>{item.website}</Cell>
</Row>
{/each}
</Body>
</DataTable>

<script>
import DataTable, { Head, Body, Row, Cell, Label } from '@smui/data-table';
import IconButton from '@smui/icon-button';
let items = [];
let sort = 'id';
let sortDirection = 'ascending';
if (typeof fetch !== 'undefined') {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((json) => (items = json));
}
function handleSort() {
items.sort((a, b) => {
const [aVal, bVal] = [a[sort], b[sort]][
sortDirection === 'ascending' ? 'slice' : 'reverse'
]();
if (typeof aVal === 'string') {
return aVal.localeCompare(bVal);
}
return aVal - bVal;
});
items = items;
}
</script>
2 changes: 1 addition & 1 deletion site/src/routes/demo/data-table/iframe.svelte
@@ -1,4 +1,4 @@
<DataTable table$aria-label="User list" stickyHeader style="width: 100%;">
<DataTable stickyHeader table$aria-label="User list" style="width: 100%;">
<Head>
<Row>
<Cell numeric>ID</Cell>
Expand Down
3 changes: 3 additions & 0 deletions site/src/routes/demo/data-table/index.svelte
Expand Up @@ -32,6 +32,8 @@
<Demo component={Pagination} file="data-table/_Pagination.svelte">
Pagination
</Demo>

<Demo component={Sortable} file="data-table/_Sortable.svelte">Sortable</Demo>
</section>

<script>
Expand All @@ -41,4 +43,5 @@
import StickyHeader from './_StickyHeader.svelte';
import RowSelection from './_RowSelection.svelte';
import Pagination from './_Pagination.svelte';
import Sortable from './_Sortable.svelte';
</script>

0 comments on commit c831855

Please sign in to comment.