Skip to content

Commit

Permalink
Feature/1526 grid keyboard navigation (#1616)
Browse files Browse the repository at this point in the history
* get it working with visualizer

* delegate focus

* delegate focus

* screenreader improvements

* updated tokens

* remove roving tab index

* add skip links

* fix too tight clipping in table cells that would cause outline not to be visible

* creative workaround for skip links

* style skip links

* cleanup + added rowcount

* more cleanup

* skip to links, not empty div

* localisation

* fix tests

* border fix

* fix tests

* apply tokens

* fixed commit in wrong branch

* fixed incorrect commit

* trigger full snapshots

* fix build

* review comments

* added changeset

* review comment

* fix build?

* update @custom-elements-manifest/analyzer dependency to version 0.10.3
  • Loading branch information
Diaan authored Dec 20, 2024
1 parent 24ca981 commit b1e3b74
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 27 deletions.
9 changes: 9 additions & 0 deletions .changeset/quick-pots-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@sl-design-system/shared': patch
'@sl-design-system/grid': patch
---

Improved accessibilty of the table;
- Added aria-rowindex and aria-rowcount;
- Improved keyboardnavigation, including skip table links
- Changed the way selecting works; active row by clicking on the entire row and selecting a row by checking the checkbox
1 change: 1 addition & 0 deletions grid-poc
Submodule grid-poc added at a55a9c
58 changes: 55 additions & 3 deletions packages/components/grid/src/grid.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
--_dragging-opacity: 0.8;
--_drop-target-outline: 2px solid #056dc2;
--_drop-target-outline-offset: -2px;
--_focus-outline: var(--sl-color-border-focused) solid var(--sl-border-width-focusring-default);
--_focus-outline-offset: var(--sl-border-width-focusring-offset);
--_focus-radius: var(--sl-border-radius-focusring-default);
--_font: var(--sl-text-body-md-normal);
--_group-background: var(--sl-elevation-surface-raised-sunken-idle);
--_header-background: var(--sl-elevation-surface-raised-alternative-idle);
Expand All @@ -22,7 +25,7 @@
--_vertical-border: linear-gradient(var(--_border-color), var(--_border-color)) 0
calc(100% - var(--_vertical-border-offset)) / var(--_border-width) calc(100% - var(--_vertical-border-offset) * 2)
no-repeat;
--_vertical-border-offset: var(--sl-space-new-md);
--_vertical-border-offset: var(--sl-space-100);
--_selected-background: var(--sl-color-background-selected-subtle-idle);
--_striped-background: var(--sl-elevation-surface-raised-alternative-idle);

Expand Down Expand Up @@ -154,6 +157,12 @@ table {
inline-size: fit-content;
min-inline-size: 100%;
position: relative;
z-index: 1;
}

thead,
tbody {
box-sizing: border-box;
}

thead {
Expand All @@ -168,6 +177,7 @@ thead {
}

tbody {
background-color: var(--sl-elevation-surface-raised-default-idle);
max-inline-size: calc(var(--sl-grid-width) - var(--sl-size-borderWidth-subtle) * 2);
min-block-size: calc(var(--sl-grid-tbody-min-height) + var(--_border-width));
overflow: auto clip;
Expand Down Expand Up @@ -234,7 +244,8 @@ td {
display: inline-flex;
flex-shrink: 0;
overflow: clip;
overflow-clip-margin: content-box 0;
overflow-clip-margin: content-box
calc(var(--sl-border-width-focusring-offset) + var(--sl-border-width-focusring-default));
padding: var(--_cell-padding-block) var(--_cell-padding-inline);

&:first-of-type:not([part~='drag-handle']) {
Expand Down Expand Up @@ -323,7 +334,7 @@ td[part~='text-field'] {
cursor: pointer;

&:hover {
--_cell-background: var(--sl-color-action-background-accent-subtle-hover);
--_cell-background: var(--sl-elevation-surface-raised-default-hover);
}
}

Expand All @@ -350,6 +361,47 @@ td[part~='text-field'] {
z-index: 1;
}

/* Set some base styles, so it is easy to see */
a[class^='skip-link'] {
background-color: var(--sl-color-background-focused);
color: var(--sl-link-focused-idle);
display: inline-block;
inset-inline-start: var(--sl-space-100);
padding: var(--sl-space-100) var(--sl-space-200);
position: absolute;
transition: transform 250ms ease-in;
z-index: 0;

&:focus {
outline: var(--_focus-outline);
outline-offset: var(--sl-border-width-focusring-offset);
}

&:hover {
color: var(--sl-link-focused-hover);
}
}

.skip-link-start {
border-radius: var(--sl-size-borderRadius-default) var(--sl-size-borderRadius-default) 0 0;
inset-block-start: 0;
translate: 0;
}

.skip-link-start:focus {
translate: 0 calc(-1 * (1lh + 2 * var(--sl-space-100)));
}

.skip-link-end {
border-radius: 0 0 var(--sl-size-borderRadius-default) var(--sl-size-borderRadius-default);
inset-block-end: 0;
translate: 0;
}

.skip-link-end:focus {
translate: 0 calc(1lh + 2 * var(--sl-space-100));
}

tfoot {
inset-block-end: 0;
max-inline-size: calc(var(--sl-grid-width) - var(--sl-size-borderWidth-subtle) * 2);
Expand Down
28 changes: 26 additions & 2 deletions packages/components/grid/src/grid.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable lit/prefer-static-styles */
import { localized } from '@lit/localize';
import { localized, msg } from '@lit/localize';
import { type VirtualizerHostElement, virtualize, virtualizerRef } from '@lit-labs/virtualizer/virtualize.js';
import { type ScopedElementsMap, ScopedElementsMixin } from '@open-wc/scoped-elements/lit-element.js';
import { ArrayDataSource, type DataSource } from '@sl-design-system/data-source';
Expand Down Expand Up @@ -307,7 +307,15 @@ export class Grid<T = any> extends ScopedElementsMixin(LitElement) {
<style>
${this.renderStyles()}
</style>
<table part="table">
<a
id="table-start"
href="#table-end"
class="skip-link-start"
@click=${(e: Event & { target: HTMLSlotElement }) => this.#onSkipTo(e, 'end')}
>
${msg('Skip to end of table')}</a
>
<table part="table" aria-rowcount=${this.dataSource?.items.length || 0}>
<thead
@sl-filter-change=${this.#onFilterChange}
@sl-filter-value-change=${this.#onFilterValueChange}
Expand Down Expand Up @@ -335,6 +343,14 @@ export class Grid<T = any> extends ScopedElementsMixin(LitElement) {
`
: nothing}
</table>
<a
id="table-end"
href="#table-start"
class="skip-link-end"
@click=${(e: Event & { target: HTMLSlotElement }) => this.#onSkipTo(e, 'start')}
>${msg('Skip to start of table')}</a
>
`;
}

Expand Down Expand Up @@ -440,6 +456,7 @@ export class Grid<T = any> extends ScopedElementsMixin(LitElement) {
class=${classMap({ active })}
part=${parts.join(' ')}
index=${index}
aria-rowindex=${index}
>
${rows[rows.length - 1].map(col => col.renderData(item))}
</tr>
Expand Down Expand Up @@ -732,6 +749,12 @@ export class Grid<T = any> extends ScopedElementsMixin(LitElement) {
this.#virtualizer?._layout?._metricsCache?.clear();
}

#onSkipTo(event: Event & { target: HTMLSlotElement }, destination: string): void {
// Not all frameworks work well with hash links, so we need to prevent the default behavior and focus the target manually
event.preventDefault();
(this.renderRoot.querySelector(`#table-${destination}`) as HTMLLinkElement).focus();
}

#onScroll(): void {
const { offsetWidth, scrollLeft, scrollWidth } = this.tbody;

Expand Down Expand Up @@ -870,6 +893,7 @@ export class Grid<T = any> extends ScopedElementsMixin(LitElement) {
this.#applySorters();

dataSource?.update();

this.stateChangeEvent.emit({ grid: this });
}
}
2 changes: 1 addition & 1 deletion packages/components/grid/src/select-column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class GridSelectColumn<T = any> extends GridColumn<T> {

override renderData(item: T): TemplateResult {
return html`
<td part="data select">
<td part="data select delegate-focus">
<sl-select
@sl-change=${(event: SlChangeEvent) => this.#onChange(event, item)}
.value=${getValueByPath(item, this.path!)}
Expand Down
2 changes: 1 addition & 1 deletion packages/components/grid/src/sort-column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class GridSortColumn<T = any> extends GridColumn<T> {
const parts = ['header', 'sort', ...this.getParts()];

return html`
<th part=${parts.join(' ')} aria-sort=${ifDefined(this.ariaSorting)}>
<th part=${parts.join(' ')} aria-sort=${ifDefined(this.ariaSorting)} role="columnheader" scope="col">
<sl-grid-sorter .column=${this} .direction=${this.direction} .path=${this.path} .sorter=${this.sorter}>
${this.header ?? getNameByPath(this.path)}
</sl-grid-sorter>
Expand Down
51 changes: 49 additions & 2 deletions packages/components/grid/src/stories/basics.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const ColumnGroups: Story = {

export const EllipsizeTextAllColumns: Story = {
render: (_, { loaded: { people } }) => html`
<sl-grid .items=${people} style="max-inline-size: 500px" ellipsize-text>
<sl-grid .items=${people} style="max-inline-size: 500px" ellipsize-text column-divider>
<sl-grid-column path="firstName"></sl-grid-column>
<sl-grid-column path="lastName"></sl-grid-column>
<sl-grid-column path="address.street"></sl-grid-column>
Expand All @@ -89,7 +89,7 @@ export const EllipsizeTextAllColumns: Story = {

export const EllipsizeTextSingleColumn: Story = {
render: (_, { loaded: { people } }) => html`
<sl-grid .items=${people} style="max-inline-size: 800px">
<sl-grid .items=${people} style="max-inline-size: 800px" column-divider>
<sl-grid-column path="firstName"></sl-grid-column>
<sl-grid-column path="lastName"></sl-grid-column>
<sl-grid-column path="address.street" ellipsize-text></sl-grid-column>
Expand All @@ -99,6 +99,53 @@ export const EllipsizeTextSingleColumn: Story = {
`
};

export const SkipLinks: Story = {
render: (_, { loaded: { people } }) => {
const linkRenderer: GridColumnDataRenderer<Person> = ({ email }) => {
return html`<a href="mailto:${email}">${email}</a>`;
};

const menuButtonRenderer: GridColumnDataRenderer<Person> = person => {
const onClick = () => {
console.log('Menu item for person clicked', person);
};

return html`
<sl-menu-button fill="ghost">
<sl-icon slot="button" name="ellipsis"></sl-icon>
<sl-menu-item @click=${onClick}>Do something with this person</sl-menu-item>
<sl-menu-item @click=${onClick}>Something else</sl-menu-item>
<hr />
<sl-menu-item @click=${onClick}>Delete person</sl-menu-item>
</sl-menu-button>
`;
};
return html`
<h1>Some data for your information:</h1>
<sl-grid .items=${people} column-divider>
<sl-grid-column path="firstName"></sl-grid-column>
<sl-grid-column path="lastName"></sl-grid-column>
<sl-grid-column path="email" .renderer=${linkRenderer}></sl-grid-column>
<sl-grid-column path="address.city"></sl-grid-column>
<sl-grid-column path="address.phone"></sl-grid-column>
<sl-grid-column path="profession"></sl-grid-column>
<sl-grid-column
header=""
.renderer=${menuButtonRenderer}
grow="0"
width="64"
.scopedElements=${{
'sl-icon': Icon,
'sl-menu-button': MenuButton,
'sl-menu-item': MenuItem
}}
></sl-grid-column>
</sl-grid>
<p>A paragraph that follows the table, with a <a href="#">link</a> in it.</p>
`;
}
};

export const CustomRenderers: Story = {
render: (_, { loaded: { people } }) => {
const avatarRenderer: GridColumnDataRenderer<Person> = ({ firstName, lastName }) => {
Expand Down
1 change: 1 addition & 0 deletions packages/components/grid/src/stories/grouping.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const Basic: Story = {

return html`
<sl-grid .dataSource=${dataSource}>
<sl-grid-selection-column></sl-grid-selection-column>
<sl-grid-column path="firstName"></sl-grid-column>
<sl-grid-column path="lastName"></sl-grid-column>
<sl-grid-column path="email"></sl-grid-column>
Expand Down
6 changes: 5 additions & 1 deletion packages/components/grid/src/stories/selection.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,12 @@ export const Grouped: Story = {
const dataSource = new ArrayDataSource(people as Person[]);
dataSource.setGroupBy('membership');

const onActiveItemChange = ({ detail: { item } }: SlActiveItemChangeEvent<Person>): void => {
console.log('current active item:', item);
};

return html`
<sl-grid .dataSource=${dataSource}>
<sl-grid .dataSource=${dataSource} clickable-row @sl-active-item-change=${onActiveItemChange}>
<sl-grid-selection-column></sl-grid-selection-column>
<sl-grid-column path="firstName"></sl-grid-column>
<sl-grid-column path="lastName"></sl-grid-column>
Expand Down
12 changes: 12 additions & 0 deletions packages/components/menu/src/menu-button.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,18 @@ export const All: Story = {
Delete...
</sl-menu-item>
</sl-menu-button>
<span>Ghost</span>
<sl-menu-button aria-label="Label" fill="ghost">
<sl-icon name="far-gear" slot="button"></sl-icon>
<sl-menu-item>
<sl-icon name="far-pen"></sl-icon>
Rename...
</sl-menu-item>
<sl-menu-item>
<sl-icon name="far-trash"></sl-icon>
Delete...
</sl-menu-item>
</sl-menu-button>
</div>
`
};
Loading

0 comments on commit b1e3b74

Please sign in to comment.