Skip to content

Commit

Permalink
feat(table): Add interactionMode property to control focus behavior (
Browse files Browse the repository at this point in the history
…#8686)

**Related Issue:** #8659 

## Summary
- Adds an `interactionMode` property with `static` and `interactive`
(default) values to Table to allow the table to be used without cell +
header focus.
- When set, prevents keyboard navigation with arrow / home / page keys.
- Still allows focus and tab / shift tab for `interactionMode` selection
affordances in cell + header.
- Still allows tab to / shift tab to reach focusable content
- Prevent focus of "unused" `interactionMode` footer cell in `static`
mode.
- Adds test to check that only interactionMode cells + header are
focused in `static` mode.
- Does not change the default behavior.
  • Loading branch information
macandcheese authored Feb 12, 2024
1 parent 80f6dad commit 0cb78c0
Show file tree
Hide file tree
Showing 13 changed files with 4,643 additions and 4,371 deletions.
18 changes: 16 additions & 2 deletions packages/calcite-components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ import { StepperItemMessages } from "./components/stepper-item/assets/stepper-it
import { TabID, TabLayout, TabPosition } from "./components/tabs/interfaces";
import { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces";
import { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n";
import { RowType, TableLayout, TableRowFocusEvent } from "./components/table/interfaces";
import { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent } from "./components/table/interfaces";
import { TableMessages } from "./components/table/assets/table/t9n";
import { TableCellMessages } from "./components/table-cell/assets/table-cell/t9n";
import { TableHeaderMessages } from "./components/table-header/assets/table-header/t9n";
Expand Down Expand Up @@ -165,7 +165,7 @@ export { StepperItemMessages } from "./components/stepper-item/assets/stepper-it
export { TabID, TabLayout, TabPosition } from "./components/tabs/interfaces";
export { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces";
export { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n";
export { RowType, TableLayout, TableRowFocusEvent } from "./components/table/interfaces";
export { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent } from "./components/table/interfaces";
export { TableMessages } from "./components/table/assets/table/t9n";
export { TableCellMessages } from "./components/table-cell/assets/table-cell/t9n";
export { TableHeaderMessages } from "./components/table-header/assets/table-header/t9n";
Expand Down Expand Up @@ -4648,6 +4648,10 @@ export namespace Components {
* When `true`, number values are displayed with a group separator corresponding to the language and country format.
*/
"groupSeparator": boolean;
/**
* When `"interactive"`, allows focus and keyboard navigation of `table-header`s and `table-cell`s. When `"static"`, prevents focus and keyboard navigation of `table-header`s and `table-cell`s when assistive technologies are not active. Selection affordances and slotted content within `table-cell`s remain focusable.
*/
"interactionMode": TableInteractionMode;
/**
* Specifies the layout of the component.
*/
Expand Down Expand Up @@ -4708,6 +4712,7 @@ export namespace Components {
* When true, prevents user interaction. Notes: This prop should use the
*/
"disabled": boolean;
"interactionMode": TableInteractionMode;
"lastCell": boolean;
/**
* Use this property to override individual strings used by the component.
Expand Down Expand Up @@ -4752,6 +4757,7 @@ export namespace Components {
* A heading to display above description content.
*/
"heading": string;
"interactionMode": TableInteractionMode;
"lastCell": boolean;
/**
* Use this property to override individual strings used by the component.
Expand Down Expand Up @@ -4787,6 +4793,7 @@ export namespace Components {
* When `true`, interaction is prevented and the component is displayed with lower opacity.
*/
"disabled": boolean;
"interactionMode": TableInteractionMode;
"lastVisibleRow": boolean;
"numbered": boolean;
"positionAll": number;
Expand Down Expand Up @@ -12143,6 +12150,10 @@ declare namespace LocalJSX {
* When `true`, number values are displayed with a group separator corresponding to the language and country format.
*/
"groupSeparator"?: boolean;
/**
* When `"interactive"`, allows focus and keyboard navigation of `table-header`s and `table-cell`s. When `"static"`, prevents focus and keyboard navigation of `table-header`s and `table-cell`s when assistive technologies are not active. Selection affordances and slotted content within `table-cell`s remain focusable.
*/
"interactionMode"?: TableInteractionMode;
/**
* Specifies the layout of the component.
*/
Expand Down Expand Up @@ -12212,6 +12223,7 @@ declare namespace LocalJSX {
* When true, prevents user interaction. Notes: This prop should use the
*/
"disabled"?: boolean;
"interactionMode"?: TableInteractionMode;
"lastCell"?: boolean;
/**
* Use this property to override individual strings used by the component.
Expand Down Expand Up @@ -12252,6 +12264,7 @@ declare namespace LocalJSX {
* A heading to display above description content.
*/
"heading"?: string;
"interactionMode"?: TableInteractionMode;
"lastCell"?: boolean;
/**
* Use this property to override individual strings used by the component.
Expand Down Expand Up @@ -12283,6 +12296,7 @@ declare namespace LocalJSX {
* When `true`, interaction is prevented and the component is displayed with lower opacity.
*/
"disabled"?: boolean;
"interactionMode"?: TableInteractionMode;
"lastVisibleRow"?: boolean;
"numbered"?: boolean;
"onCalciteInternalTableRowFocusRequest"?: (event: CalciteTableRowCustomEvent<TableRowFocusEvent>) => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export const CSS = {
selectedCell: "selected-cell",
assistiveText: "assistive-text",
lastCell: "last-cell",
staticCell: "static-cell",
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@
}

td {
@apply text-start focus-base align-middle text-color-1 whitespace-normal;
@apply text-start align-middle text-color-1 whitespace-normal;
background: var(--calcite-internal-table-cell-background);
font-size: var(--calcite-internal-table-cell-font-size);
border-inline-end: 1px solid var(--calcite-color-border-3);
padding: var(--calcite-internal-table-cell-padding);

&:focus {
@apply focus-inset;
&:not(.static-cell) {
@apply focus-base;
&:focus {
@apply focus-inset;
}
}
padding: var(--calcite-internal-table-cell-padding);
}

td.last-cell {
Expand All @@ -56,8 +59,11 @@ td.last-cell {
}

.selection-cell {
@apply cursor-pointer text-color-3;
@apply text-color-3;
inset-inline-start: 2rem;
&:not(.footer-cell) {
@apply cursor-pointer;
}
}

.selected-cell:not(.number-cell):not(.footer-cell) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import { connectLocalized, disconnectLocalized, LocalizedComponent } from "../../utils/locale";
import { TableCellMessages } from "./assets/table-cell/t9n";
import { CSS } from "./resources";
import { RowType } from "../table/interfaces";
import { RowType, TableInteractionMode } from "../table/interfaces";
import { getElementDir } from "../../utils/dom";
import { CSS_UTILITY } from "../../utils/resources";

Expand Down Expand Up @@ -58,6 +58,9 @@ export class TableCell
/** @internal */
@Prop() disabled: boolean;

/** @internal */
@Prop() interactionMode: TableInteractionMode = "interactive";

/** @internal */
@Prop() lastCell: boolean;

Expand Down Expand Up @@ -212,6 +215,10 @@ export class TableCell

render(): VNode {
const dir = getElementDir(this.el);
const staticCell =
this.disabled ||
(this.interactionMode === "static" &&
(!this.selectionCell || (this.selectionCell && this.parentRowType === "foot")));

return (
<Host>
Expand All @@ -225,13 +232,14 @@ export class TableCell
[CSS.selectedCell]: this.parentRowIsSelected,
[CSS.lastCell]: this.lastCell,
[CSS_UTILITY.rtl]: dir === "rtl",
[CSS.staticCell]: staticCell,
}}
colSpan={this.colSpan}
onBlur={this.onContainerBlur}
onFocus={this.onContainerFocus}
role="gridcell"
rowSpan={this.rowSpan}
tabIndex={this.disabled ? -1 : 0}
tabIndex={staticCell ? -1 : 0}
// eslint-disable-next-line react/jsx-sort-props -- ref should be last so node attrs/props are in sync (see https://github.com/Esri/calcite-design-system/pull/6530)
ref={(el) => (this.containerEl = el)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const CSS = {
active: "active",
selectedCell: "selected-cell",
lastCell: "last-cell",
staticCell: "static-cell",
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,19 @@
}

th {
@apply text-color-1 focus-base text-start font-medium align-top whitespace-normal;
@apply text-color-1 text-start font-medium align-top whitespace-normal;
font-size: var(--calcite-internal-table-cell-font-size);
border-inline-end: 1px solid var(--calcite-internal-table-header-border-color);
border-block-end: 1px solid var(--calcite-internal-table-header-border-color);
padding-block: calc(var(--calcite-internal-table-cell-padding) * 1.5);
padding-inline: var(--calcite-internal-table-cell-padding);
background-color: var(--calcite-internal-table-header-background);
&:focus-within {
@apply focus-inset;

&:not(.static-cell) {
@apply focus-base;
&:not(.static-cell):focus-within {
@apply focus-inset;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { connectLocalized, disconnectLocalized, LocalizedComponent } from "../..
import { Alignment, Scale, SelectionMode } from "../interfaces";
import { TableHeaderMessages } from "./assets/table-header/t9n";
import { CSS } from "./resources";
import { RowType } from "../table/interfaces";
import { RowType, TableInteractionMode } from "../table/interfaces";
import { getIconScale } from "../../utils/component";

@Component({
Expand Down Expand Up @@ -47,6 +47,9 @@ export class TableHeader implements LocalizedComponent, LoadableComponent, T9nCo
/** Specifies the number of rows the component should span. */
@Prop({ reflect: true }) rowSpan: number;

/** @internal */
@Prop() interactionMode: TableInteractionMode = "interactive";

/** @internal */
@Prop() lastCell: boolean;

Expand Down Expand Up @@ -205,7 +208,7 @@ export class TableHeader implements LocalizedComponent, LoadableComponent, T9nCo

const allSelected = this.selectedRowCount === this.bodyRowCount;
const selectionIcon = allSelected ? "check-square-f" : "check-square";

const staticCell = this.interactionMode === "static" && !this.selectionCell;
return (
<Host>
<th
Expand All @@ -217,13 +220,14 @@ export class TableHeader implements LocalizedComponent, LoadableComponent, T9nCo
[CSS.selectionCell]: this.selectionCell,
[CSS.selectedCell]: this.parentRowIsSelected,
[CSS.multipleSelectionCell]: this.selectionMode === "multiple",
[CSS.staticCell]: staticCell,
[CSS.lastCell]: this.lastCell,
}}
colSpan={this.colSpan}
role="columnheader"
rowSpan={this.rowSpan}
scope={scope}
tabIndex={0}
tabIndex={this.selectionCell ? 0 : staticCell ? -1 : 0}
// eslint-disable-next-line react/jsx-sort-props -- ref should be last so node attrs/props are in sync (see https://github.com/Esri/calcite-design-system/pull/6530)
ref={(el) => (this.containerEl = el)}
>
Expand Down
16 changes: 12 additions & 4 deletions packages/calcite-components/src/components/table-row/table-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { LocalizedComponent } from "../../utils/locale";
import { Scale, SelectionMode } from "../interfaces";
import { focusElementInGroup, FocusElementInGroupDestination } from "../../utils/dom";
import { RowType, TableRowFocusEvent } from "../table/interfaces";
import { RowType, TableInteractionMode, TableRowFocusEvent } from "../table/interfaces";
import { isActivationKey } from "../../utils/key";
import {
connectInteractive,
Expand Down Expand Up @@ -51,6 +51,9 @@ export class TableRow implements InteractiveComponent, LocalizedComponent {
/** @internal */
@Prop({ mutable: true }) cellCount: number;

/** @internal */
@Prop() interactionMode: TableInteractionMode = "interactive";

/** @internal */
@Prop() lastVisibleRow: boolean;

Expand Down Expand Up @@ -91,6 +94,7 @@ export class TableRow implements InteractiveComponent, LocalizedComponent {
@Watch("scale")
@Watch("selected")
@Watch("selectedRowCount")
@Watch("interactionMode")
handleCellChanges(): void {
if (this.tableRowEl && this.rowCells.length > 0) {
this.updateCells();
Expand Down Expand Up @@ -198,7 +202,10 @@ export class TableRow implements InteractiveComponent, LocalizedComponent {
//
//--------------------------------------------------------------------------

private keyDownHandler(event: KeyboardEvent): void {
private keyDownHandler = (event: KeyboardEvent): void => {
if (this.interactionMode !== "interactive") {
return;
}
const el = event.target as HTMLCalciteTableCellElement | HTMLCalciteTableHeaderElement;
const key = event.key;
const isControl = event.ctrlKey;
Expand Down Expand Up @@ -249,7 +256,7 @@ export class TableRow implements InteractiveComponent, LocalizedComponent {
break;
}
}
}
};

private emitTableRowFocusRequest = (
cellPosition: number,
Expand Down Expand Up @@ -284,6 +291,7 @@ export class TableRow implements InteractiveComponent, LocalizedComponent {

if (cells.length > 0) {
cells?.forEach((cell: HTMLCalciteTableCellElement | HTMLCalciteTableHeaderElement, index) => {
cell.interactionMode = this.interactionMode;
cell.positionInRow = index + 1;
cell.parentRowType = this.rowType;
cell.parentRowIsSelected = this.selected;
Expand Down Expand Up @@ -385,7 +393,7 @@ export class TableRow implements InteractiveComponent, LocalizedComponent {
aria-rowindex={this.positionAll + 1}
aria-selected={this.selected}
class={{ [CSS.lastVisibleRow]: this.lastVisibleRow }}
onKeyDown={(event) => this.keyDownHandler(event)}
onKeyDown={this.keyDownHandler}
// eslint-disable-next-line react/jsx-sort-props -- ref should be last so node attrs/props are in sync (see https://github.com/Esri/calcite-design-system/pull/6530)
ref={(el) => (this.tableRowEl = el)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export interface TableRowFocusEvent {
export type RowType = "head" | "body" | "foot";

export type TableLayout = "auto" | "fixed";

export type TableInteractionMode = "interactive" | "static";
Loading

0 comments on commit 0cb78c0

Please sign in to comment.