Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add localized labels to remaining table sub-elements #1519

Merged
merged 18 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Add additional localized labels to sub-elements in the table to improve accessibility",
"packageName": "@ni/nimble-components",
"email": "20542556+mollykreis@users.noreply.github.com",
"dependentChangeType": "patch"
}
36 changes: 34 additions & 2 deletions packages/nimble-components/src/label-provider/table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import { DesignTokensFor, LabelProviderBase } from '../base';
import {
tableCellActionMenuLabel,
tableColumnHeaderGroupedIndicatorLabel,
tableColumnHeaderSortedAscendingIndicatorLabel,
tableColumnHeaderSortedDescendingIndicatorLabel,
tableGroupCollapseLabel,
tableGroupExpandLabel,
tableGroupsCollapseAllLabel
tableGroupSelectAllLabel,
tableGroupsCollapseAllLabel,
tableRowOperationColumnLabel,
tableRowSelectLabel,
tableSelectAllLabel
} from './label-tokens';

declare global {
Expand All @@ -20,7 +26,15 @@ const supportedLabels = {
groupExpand: tableGroupExpandLabel,
groupsCollapseAll: tableGroupsCollapseAllLabel,
cellActionMenu: tableCellActionMenuLabel,
columnHeaderGroupedIndicator: tableColumnHeaderGroupedIndicatorLabel
columnHeaderGroupedIndicator: tableColumnHeaderGroupedIndicatorLabel,
columnHeaderSortedAscendingIndicator:
tableColumnHeaderSortedAscendingIndicatorLabel,
columnHeaderSortedDescendingIndicator:
tableColumnHeaderSortedDescendingIndicatorLabel,
selectAll: tableSelectAllLabel,
groupSelectAll: tableGroupSelectAllLabel,
rowSelect: tableRowSelectLabel,
rowOperationColumn: tableRowOperationColumnLabel
} as const;

/**
Expand All @@ -44,6 +58,24 @@ export class LabelProviderTable
@attr({ attribute: 'column-header-grouped-indicator' })
public columnHeaderGroupedIndicator: string | undefined;

@attr({ attribute: 'column-header-sorted-ascending-indicator' })
public columnHeaderSortedAscendingIndicator: string | undefined;

@attr({ attribute: 'column-header-sorted-descending-indicator' })
public columnHeaderSortedDescendingIndicator: string | undefined;

@attr({ attribute: 'select-all' })
public selectAll: string | undefined;

@attr({ attribute: 'group-select-all' })
public groupSelectAll: string | undefined;

@attr({ attribute: 'row-select' })
public rowSelect: string | undefined;

@attr({ attribute: 'row-operation-column' })
public rowOperationColumn: string | undefined;

protected override readonly supportedLabels = supportedLabels;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,11 @@ export const tableLabelDefaults: { readonly [key in TokenName]: string } = {
tableGroupExpandLabel: 'Expand group',
tableGroupsCollapseAllLabel: 'Collapse all groups',
tableCellActionMenuLabel: 'Options',
tableColumnHeaderGroupedIndicatorLabel: 'Grouped'
tableColumnHeaderGroupedIndicatorLabel: 'Grouped',
tableColumnHeaderSortedAscendingIndicatorLabel: 'Sorted ascending',
tableColumnHeaderSortedDescendingIndicatorLabel: 'Sorted descending',
tableSelectAllLabel: 'Select all rows',
tableGroupSelectAllLabel: 'Select all rows in group',
tableRowSelectLabel: 'Select row',
tableRowOperationColumnLabel: 'Row operations'
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,37 @@ export const tableColumnHeaderGroupedIndicatorLabel = DesignToken.create<string>
name: 'table-column-header-grouped-indicator-label',
cssCustomPropertyName: null
}).withDefault(tableLabelDefaults.tableColumnHeaderGroupedIndicatorLabel);

export const tableColumnHeaderSortedAscendingIndicatorLabel = DesignToken.create<string>({
name: 'table-column-header-sorted-ascending-indicator-label',
cssCustomPropertyName: null
}).withDefault(
tableLabelDefaults.tableColumnHeaderSortedAscendingIndicatorLabel
);

export const tableColumnHeaderSortedDescendingIndicatorLabel = DesignToken.create<string>({
name: 'table-column-header-sorted-descending-indicator-label',
cssCustomPropertyName: null
}).withDefault(
tableLabelDefaults.tableColumnHeaderSortedDescendingIndicatorLabel
);

export const tableSelectAllLabel = DesignToken.create<string>({
name: 'table-select-all-label',
cssCustomPropertyName: null
}).withDefault(tableLabelDefaults.tableSelectAllLabel);

export const tableGroupSelectAllLabel = DesignToken.create<string>({
name: 'table-group-select-all-label',
cssCustomPropertyName: null
}).withDefault(tableLabelDefaults.tableGroupSelectAllLabel);

export const tableRowSelectLabel = DesignToken.create<string>({
name: 'table-row-select-label',
cssCustomPropertyName: null
}).withDefault(tableLabelDefaults.tableRowSelectLabel);

export const tableRowOperationColumnLabel = DesignToken.create<string>({
name: 'table-row-operation-column-label',
cssCustomPropertyName: null
}).withDefault(tableLabelDefaults.tableRowOperationColumnLabel);
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const template = html<TableCell>`
@toggle="${(x, c) => x.onActionMenuToggle(c.event as CustomEvent<MenuButtonToggleEventDetail>)}"
@click="${(_, c) => c.event.stopPropagation()}"
class="action-menu"
title="${x => x.actionMenuLabel ?? tableCellActionMenuLabel.getValueFor(x)}"
>
<${iconThreeDotsLineTag} slot="start"></${iconThreeDotsLineTag}>
${x => x.actionMenuLabel ?? tableCellActionMenuLabel.getValueFor(x)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { iconArrowExpanderRightTag } from '../../../icons/arrow-expander-right';
import { checkboxTag } from '../../../checkbox';
import {
tableGroupCollapseLabel,
tableGroupExpandLabel
tableGroupExpandLabel,
tableGroupSelectAllLabel
} from '../../../label-provider/table/label-tokens';

// prettier-ignore
Expand All @@ -24,6 +25,8 @@ export const template = html<TableGroupRow>`
class="selection-checkbox"
@change="${(x, c) => x.onSelectionChange(c.event as CustomEvent)}"
@click="${(_, c) => c.event.stopPropagation()}"
title="${x => tableGroupSelectAllLabel.getValueFor(x)}"
aria-label="${x => tableGroupSelectAllLabel.getValueFor(x)}"
>
</${checkboxTag}>
</span>
Expand All @@ -35,6 +38,7 @@ export const template = html<TableGroupRow>`
content-hidden
class="expand-collapse-button"
tabindex="-1"
title="${x => (x.expanded ? tableGroupCollapseLabel.getValueFor(x) : tableGroupExpandLabel.getValueFor(x))}"
>
<${iconArrowExpanderRightTag} ${ref('expandIcon')} slot="start" class="expander-icon ${x => x.animationClass}"></${iconArrowExpanderRightTag}>
${x => (x.expanded ? tableGroupCollapseLabel.getValueFor(x) : tableGroupExpandLabel.getValueFor(x))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { iconArrowDownTag } from '../../../icons/arrow-down';
import { iconArrowUpTag } from '../../../icons/arrow-up';
import { iconTwoSquaresInBracketsTag } from '../../../icons/two-squares-in-brackets';
import { TableColumnSortDirection } from '../../types';
import { tableColumnHeaderGroupedIndicatorLabel } from '../../../label-provider/table/label-tokens';
import {
tableColumnHeaderGroupedIndicatorLabel,
tableColumnHeaderSortedAscendingIndicatorLabel,
tableColumnHeaderSortedDescendingIndicatorLabel
} from '../../../label-provider/table/label-tokens';

// prettier-ignore
export const template = html<TableHeader>`
Expand All @@ -14,15 +18,27 @@ export const template = html<TableHeader>`
@mousedown="${(_x, c) => !((c.event as MouseEvent).detail > 1)}"
>
<slot></slot>
${'' /* Omit title attribute for sort indicators because aria-sort is set on the 1st sorted column */}
${when(x => x.sortDirection === TableColumnSortDirection.ascending, html`
<${iconArrowUpTag} class="sort-indicator" aria-hidden="true"></${iconArrowUpTag}>
${'' /* Set aria-hidden="true" on sort indicators because aria-sort is set on the 1st sorted column */}
${when(x => x.sortDirection === TableColumnSortDirection.ascending, html<TableHeader>`
<${iconArrowUpTag}
class="sort-indicator"
title="${x => tableColumnHeaderSortedAscendingIndicatorLabel.getValueFor(x)}"
aria-hidden="true"
></${iconArrowUpTag}>
`)}
${when(x => x.sortDirection === TableColumnSortDirection.descending, html`
<${iconArrowDownTag} class="sort-indicator" aria-hidden="true"></${iconArrowDownTag}>
${when(x => x.sortDirection === TableColumnSortDirection.descending, html<TableHeader>`
<${iconArrowDownTag}
class="sort-indicator"
title="${x => tableColumnHeaderSortedDescendingIndicatorLabel.getValueFor(x)}"
aria-hidden="true"
></${iconArrowDownTag}>
`)}
${when(x => x.isGrouped, html<TableHeader>`
<${iconTwoSquaresInBracketsTag} class="grouped-indicator" title="${x => tableColumnHeaderGroupedIndicatorLabel.getValueFor(x)}"></${iconTwoSquaresInBracketsTag}>
<${iconTwoSquaresInBracketsTag}
class="grouped-indicator"
title="${x => tableColumnHeaderGroupedIndicatorLabel.getValueFor(x)}"
aria-label="${x => tableColumnHeaderGroupedIndicatorLabel.getValueFor(x)}"
></${iconTwoSquaresInBracketsTag}>
`)}
</template>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export class TableRow<
@attr({ attribute: 'menu-open', mode: 'boolean' })
public menuOpen = false;

@attr({ attribute: 'row-operation-grid-cell-hidden', mode: 'boolean' })
public rowOperationGridCellHidden = false;

/** @internal */
@observable
public readonly selectionCheckbox?: Checkbox;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const styles = css`
background-color: ${fillHoverSelectedColor};
}

.checkbox-container {
.row-operations-container {
display: flex;
}

Expand Down
23 changes: 14 additions & 9 deletions packages/nimble-components/src/table/components/row/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@ import type { TableRow, ColumnState } from '.';
import type { MenuButtonToggleEventDetail } from '../../../menu-button/types';
import { tableCellTag } from '../cell';
import { checkboxTag } from '../../../checkbox';
import { tableRowSelectLabel } from '../../../label-provider/table/label-tokens';

// prettier-ignore
export const template = html<TableRow>`
<template role="row" aria-selected=${x => x.ariaSelected}>
${when(x => x.selectable && !x.hideSelection, html<TableRow>`
<span role="gridcell" class="checkbox-container">
<${checkboxTag}
${ref('selectionCheckbox')}
class="selection-checkbox"
@change="${(x, c) => x.onSelectionChange(c.event as CustomEvent)}"
@click="${(_, c) => c.event.stopPropagation()}"
>
</${checkboxTag}>
${when(x => !x.rowOperationGridCellHidden, html<TableRow>`
<span role="gridcell" class="row-operations-container">
${when(x => x.selectable && !x.hideSelection, html<TableRow>`
<${checkboxTag}
${ref('selectionCheckbox')}
class="selection-checkbox"
@change="${(x, c) => x.onSelectionChange(c.event as CustomEvent)}"
@click="${(_, c) => c.event.stopPropagation()}"
title="${x => tableRowSelectLabel.getValueFor(x)}"
aria-label="${x => tableRowSelectLabel.getValueFor(x)}"
>
</${checkboxTag}>
`)}
</span>
`)}
${'' /* This is needed to help align the cell widths exactly with the column headers, due to the space reserved for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ describe('TableRow', () => {
expect(document.createElement('nimble-table-row')).toBeInstanceOf(TableRow);
});

it('includes row operations gridcell when rowOperationGridCellHidden is false', async () => {
element.rowOperationGridCellHidden = false;
await connect();

expect(
element.shadowRoot!.querySelectorAll('[role="gridcell"]').length
).toBe(1);
});

it('does not include row operations gridcell when rowOperationGridCellHidden is true', async () => {
element.rowOperationGridCellHidden = true;
await connect();

expect(
element.shadowRoot!.querySelectorAll('[role="gridcell"]').length
).toBe(0);
});

it('does not have aria-selected attribute when it is not selectable', async () => {
element.selectable = false;
element.selected = false;
Expand Down
7 changes: 7 additions & 0 deletions packages/nimble-components/src/table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ export class Table<
return this.tableValidator.getValidity();
}

public get showRowOperationColumn(): boolean {
return (
this.selectionMode === TableRowSelectionMode.multiple
|| this.showCollapseAll
);
}

/**
* @internal
*/
Expand Down
20 changes: 20 additions & 0 deletions packages/nimble-components/src/table/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,26 @@ export const styles = css`
.row {
position: relative;
}

.accessibly-hidden {
${
/**
* Hide content visually while keeping it screen reader-accessible.
* Source: https://webaim.org/techniques/css/invisiblecontent/#techniques
* See discussion here: https://github.com/microsoft/fast/issues/5740#issuecomment-1068195035
*/
''
}
display: inline-block;
height: 1px;
width: 1px;
position: absolute;
margin: -1px;
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
overflow: hidden;
padding: 0;
}
`.withBehaviors(
themeBehavior(
Theme.color,
Expand Down
40 changes: 25 additions & 15 deletions packages/nimble-components/src/table/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import { buttonTag } from '../button';
import { ButtonAppearance } from '../button/types';
import { iconTriangleTwoLinesHorizontalTag } from '../icons/triangle-two-lines-horizontal';
import { checkboxTag } from '../checkbox';
import { tableGroupsCollapseAllLabel } from '../label-provider/table/label-tokens';
import {
tableGroupsCollapseAllLabel,
tableRowOperationColumnLabel,
tableSelectAllLabel
} from '../label-provider/table/label-tokens';

// prettier-ignore
export const template = html<Table>`
Expand All @@ -46,29 +50,34 @@ export const template = html<Table>`
<div class="glass-overlay">
<div role="rowgroup" class="header-row-container">
<div class="header-row" role="row">
<span class="header-row-action-container" ${ref('headerRowActionContainer')}>
<span role="${x => (x.showRowOperationColumn ? 'columnheader' : '')}" class="header-row-action-container" ${ref('headerRowActionContainer')}>
${when(x => x.showRowOperationColumn, html<Table>`
<span class="accessibly-hidden">
${x => tableRowOperationColumnLabel.getValueFor(x)}
</span>
`)}
${when(x => x.selectionMode === TableRowSelectionMode.multiple, html<Table>`
<span role="columnheader" class="checkbox-container">
<span class="checkbox-container">
<${checkboxTag}
${ref('selectionCheckbox')}
class="${x => `selection-checkbox ${x.selectionMode ?? ''}`}"
@change="${(x, c) => x.onAllRowsSelectionChange(c.event as CustomEvent)}"
title="${x => tableSelectAllLabel.getValueFor(x)}"
aria-label="${x => tableSelectAllLabel.getValueFor(x)}"
>
</${checkboxTag}>
</span>
`)}
<span role="gridcell">
<${buttonTag}
class="collapse-all-button ${x => `${x.showCollapseAll ? 'visible' : ''}`}"
content-hidden
appearance="${ButtonAppearance.ghost}"
title="${x => tableGroupsCollapseAllLabel.getValueFor(x)}"
@click="${x => x.handleCollapseAllGroupRows()}"
>
<${iconTriangleTwoLinesHorizontalTag} slot="start"></${iconTriangleTwoLinesHorizontalTag}>
${x => tableGroupsCollapseAllLabel.getValueFor(x)}
</${buttonTag}>
</span>
<${buttonTag}
class="collapse-all-button ${x => `${x.showCollapseAll ? 'visible' : ''}`}"
content-hidden
appearance="${ButtonAppearance.ghost}"
title="${x => tableGroupsCollapseAllLabel.getValueFor(x)}"
@click="${x => x.handleCollapseAllGroupRows()}"
>
<${iconTriangleTwoLinesHorizontalTag} slot="start"></${iconTriangleTwoLinesHorizontalTag}>
${x => tableGroupsCollapseAllLabel.getValueFor(x)}
</${buttonTag}>
</span>
<span class="column-headers-container" ${ref('columnHeadersContainer')}>
${repeat(x => x.visibleColumns, html<TableColumn, Table>`
Expand Down Expand Up @@ -129,6 +138,7 @@ export const template = html<Table>`
:dataRecord="${(x, c) => c.parent.tableData[x.index]?.record}"
:columns="${(_, c) => c.parent.columns}"
:nestingLevel="${(x, c) => c.parent.tableData[x.index]?.nestingLevel}"
?row-operation-grid-cell-hidden="${(_, c) => !c.parent.showRowOperationColumn}"
@click="${(x, c) => c.parent.onRowClick(x.index, c.event as MouseEvent)}"
@row-selection-toggle="${(x, c) => c.parent.onRowSelectionToggle(x.index, c.event as CustomEvent<TableRowSelectionToggleEventDetail>)}"
@row-action-menu-beforetoggle="${(x, c) => c.parent.onRowActionMenuBeforeToggle(x.index, c.event as CustomEvent<TableActionMenuToggleEventDetail>)}"
Expand Down