Skip to content

Commit 0cb78c0

Browse files
authored
feat(table): Add interactionMode property to control focus behavior (#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.
1 parent 80f6dad commit 0cb78c0

File tree

13 files changed

+4643
-4371
lines changed

13 files changed

+4643
-4371
lines changed

packages/calcite-components/src/components.d.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ import { StepperItemMessages } from "./components/stepper-item/assets/stepper-it
7979
import { TabID, TabLayout, TabPosition } from "./components/tabs/interfaces";
8080
import { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces";
8181
import { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n";
82-
import { RowType, TableLayout, TableRowFocusEvent } from "./components/table/interfaces";
82+
import { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent } from "./components/table/interfaces";
8383
import { TableMessages } from "./components/table/assets/table/t9n";
8484
import { TableCellMessages } from "./components/table-cell/assets/table-cell/t9n";
8585
import { TableHeaderMessages } from "./components/table-header/assets/table-header/t9n";
@@ -165,7 +165,7 @@ export { StepperItemMessages } from "./components/stepper-item/assets/stepper-it
165165
export { TabID, TabLayout, TabPosition } from "./components/tabs/interfaces";
166166
export { TabChangeEventDetail, TabCloseEventDetail } from "./components/tab/interfaces";
167167
export { TabTitleMessages } from "./components/tab-title/assets/tab-title/t9n";
168-
export { RowType, TableLayout, TableRowFocusEvent } from "./components/table/interfaces";
168+
export { RowType, TableInteractionMode, TableLayout, TableRowFocusEvent } from "./components/table/interfaces";
169169
export { TableMessages } from "./components/table/assets/table/t9n";
170170
export { TableCellMessages } from "./components/table-cell/assets/table-cell/t9n";
171171
export { TableHeaderMessages } from "./components/table-header/assets/table-header/t9n";
@@ -4648,6 +4648,10 @@ export namespace Components {
46484648
* When `true`, number values are displayed with a group separator corresponding to the language and country format.
46494649
*/
46504650
"groupSeparator": boolean;
4651+
/**
4652+
* 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.
4653+
*/
4654+
"interactionMode": TableInteractionMode;
46514655
/**
46524656
* Specifies the layout of the component.
46534657
*/
@@ -4708,6 +4712,7 @@ export namespace Components {
47084712
* When true, prevents user interaction. Notes: This prop should use the
47094713
*/
47104714
"disabled": boolean;
4715+
"interactionMode": TableInteractionMode;
47114716
"lastCell": boolean;
47124717
/**
47134718
* Use this property to override individual strings used by the component.
@@ -4752,6 +4757,7 @@ export namespace Components {
47524757
* A heading to display above description content.
47534758
*/
47544759
"heading": string;
4760+
"interactionMode": TableInteractionMode;
47554761
"lastCell": boolean;
47564762
/**
47574763
* Use this property to override individual strings used by the component.
@@ -4787,6 +4793,7 @@ export namespace Components {
47874793
* When `true`, interaction is prevented and the component is displayed with lower opacity.
47884794
*/
47894795
"disabled": boolean;
4796+
"interactionMode": TableInteractionMode;
47904797
"lastVisibleRow": boolean;
47914798
"numbered": boolean;
47924799
"positionAll": number;
@@ -12143,6 +12150,10 @@ declare namespace LocalJSX {
1214312150
* When `true`, number values are displayed with a group separator corresponding to the language and country format.
1214412151
*/
1214512152
"groupSeparator"?: boolean;
12153+
/**
12154+
* 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.
12155+
*/
12156+
"interactionMode"?: TableInteractionMode;
1214612157
/**
1214712158
* Specifies the layout of the component.
1214812159
*/
@@ -12212,6 +12223,7 @@ declare namespace LocalJSX {
1221212223
* When true, prevents user interaction. Notes: This prop should use the
1221312224
*/
1221412225
"disabled"?: boolean;
12226+
"interactionMode"?: TableInteractionMode;
1221512227
"lastCell"?: boolean;
1221612228
/**
1221712229
* Use this property to override individual strings used by the component.
@@ -12252,6 +12264,7 @@ declare namespace LocalJSX {
1225212264
* A heading to display above description content.
1225312265
*/
1225412266
"heading"?: string;
12267+
"interactionMode"?: TableInteractionMode;
1225512268
"lastCell"?: boolean;
1225612269
/**
1225712270
* Use this property to override individual strings used by the component.
@@ -12283,6 +12296,7 @@ declare namespace LocalJSX {
1228312296
* When `true`, interaction is prevented and the component is displayed with lower opacity.
1228412297
*/
1228512298
"disabled"?: boolean;
12299+
"interactionMode"?: TableInteractionMode;
1228612300
"lastVisibleRow"?: boolean;
1228712301
"numbered"?: boolean;
1228812302
"onCalciteInternalTableRowFocusRequest"?: (event: CalciteTableRowCustomEvent<TableRowFocusEvent>) => void;

packages/calcite-components/src/components/table-cell/resources.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export const CSS = {
55
selectedCell: "selected-cell",
66
assistiveText: "assistive-text",
77
lastCell: "last-cell",
8+
staticCell: "static-cell",
89
};

packages/calcite-components/src/components/table-cell/table-cell.scss

+11-5
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,18 @@
2424
}
2525

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

32-
&:focus {
33-
@apply focus-inset;
33+
&:not(.static-cell) {
34+
@apply focus-base;
35+
&:focus {
36+
@apply focus-inset;
37+
}
3438
}
35-
padding: var(--calcite-internal-table-cell-padding);
3639
}
3740

3841
td.last-cell {
@@ -56,8 +59,11 @@ td.last-cell {
5659
}
5760

5861
.selection-cell {
59-
@apply cursor-pointer text-color-3;
62+
@apply text-color-3;
6063
inset-inline-start: 2rem;
64+
&:not(.footer-cell) {
65+
@apply cursor-pointer;
66+
}
6167
}
6268

6369
.selected-cell:not(.number-cell):not(.footer-cell) {

packages/calcite-components/src/components/table-cell/table-cell.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
import { connectLocalized, disconnectLocalized, LocalizedComponent } from "../../utils/locale";
2525
import { TableCellMessages } from "./assets/table-cell/t9n";
2626
import { CSS } from "./resources";
27-
import { RowType } from "../table/interfaces";
27+
import { RowType, TableInteractionMode } from "../table/interfaces";
2828
import { getElementDir } from "../../utils/dom";
2929
import { CSS_UTILITY } from "../../utils/resources";
3030

@@ -58,6 +58,9 @@ export class TableCell
5858
/** @internal */
5959
@Prop() disabled: boolean;
6060

61+
/** @internal */
62+
@Prop() interactionMode: TableInteractionMode = "interactive";
63+
6164
/** @internal */
6265
@Prop() lastCell: boolean;
6366

@@ -212,6 +215,10 @@ export class TableCell
212215

213216
render(): VNode {
214217
const dir = getElementDir(this.el);
218+
const staticCell =
219+
this.disabled ||
220+
(this.interactionMode === "static" &&
221+
(!this.selectionCell || (this.selectionCell && this.parentRowType === "foot")));
215222

216223
return (
217224
<Host>
@@ -225,13 +232,14 @@ export class TableCell
225232
[CSS.selectedCell]: this.parentRowIsSelected,
226233
[CSS.lastCell]: this.lastCell,
227234
[CSS_UTILITY.rtl]: dir === "rtl",
235+
[CSS.staticCell]: staticCell,
228236
}}
229237
colSpan={this.colSpan}
230238
onBlur={this.onContainerBlur}
231239
onFocus={this.onContainerFocus}
232240
role="gridcell"
233241
rowSpan={this.rowSpan}
234-
tabIndex={this.disabled ? -1 : 0}
242+
tabIndex={staticCell ? -1 : 0}
235243
// 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)
236244
ref={(el) => (this.containerEl = el)}
237245
>

packages/calcite-components/src/components/table-header/resources.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export const CSS = {
1010
active: "active",
1111
selectedCell: "selected-cell",
1212
lastCell: "last-cell",
13+
staticCell: "static-cell",
1314
};

packages/calcite-components/src/components/table-header/table-header.scss

+7-3
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,19 @@
2626
}
2727

2828
th {
29-
@apply text-color-1 focus-base text-start font-medium align-top whitespace-normal;
29+
@apply text-color-1 text-start font-medium align-top whitespace-normal;
3030
font-size: var(--calcite-internal-table-cell-font-size);
3131
border-inline-end: 1px solid var(--calcite-internal-table-header-border-color);
3232
border-block-end: 1px solid var(--calcite-internal-table-header-border-color);
3333
padding-block: calc(var(--calcite-internal-table-cell-padding) * 1.5);
3434
padding-inline: var(--calcite-internal-table-cell-padding);
3535
background-color: var(--calcite-internal-table-header-background);
36-
&:focus-within {
37-
@apply focus-inset;
36+
37+
&:not(.static-cell) {
38+
@apply focus-base;
39+
&:not(.static-cell):focus-within {
40+
@apply focus-inset;
41+
}
3842
}
3943
}
4044

packages/calcite-components/src/components/table-header/table-header.tsx

+7-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { connectLocalized, disconnectLocalized, LocalizedComponent } from "../..
1616
import { Alignment, Scale, SelectionMode } from "../interfaces";
1717
import { TableHeaderMessages } from "./assets/table-header/t9n";
1818
import { CSS } from "./resources";
19-
import { RowType } from "../table/interfaces";
19+
import { RowType, TableInteractionMode } from "../table/interfaces";
2020
import { getIconScale } from "../../utils/component";
2121

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

50+
/** @internal */
51+
@Prop() interactionMode: TableInteractionMode = "interactive";
52+
5053
/** @internal */
5154
@Prop() lastCell: boolean;
5255

@@ -205,7 +208,7 @@ export class TableHeader implements LocalizedComponent, LoadableComponent, T9nCo
205208

206209
const allSelected = this.selectedRowCount === this.bodyRowCount;
207210
const selectionIcon = allSelected ? "check-square-f" : "check-square";
208-
211+
const staticCell = this.interactionMode === "static" && !this.selectionCell;
209212
return (
210213
<Host>
211214
<th
@@ -217,13 +220,14 @@ export class TableHeader implements LocalizedComponent, LoadableComponent, T9nCo
217220
[CSS.selectionCell]: this.selectionCell,
218221
[CSS.selectedCell]: this.parentRowIsSelected,
219222
[CSS.multipleSelectionCell]: this.selectionMode === "multiple",
223+
[CSS.staticCell]: staticCell,
220224
[CSS.lastCell]: this.lastCell,
221225
}}
222226
colSpan={this.colSpan}
223227
role="columnheader"
224228
rowSpan={this.rowSpan}
225229
scope={scope}
226-
tabIndex={0}
230+
tabIndex={this.selectionCell ? 0 : staticCell ? -1 : 0}
227231
// 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)
228232
ref={(el) => (this.containerEl = el)}
229233
>

packages/calcite-components/src/components/table-row/table-row.tsx

+12-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { LocalizedComponent } from "../../utils/locale";
1515
import { Scale, SelectionMode } from "../interfaces";
1616
import { focusElementInGroup, FocusElementInGroupDestination } from "../../utils/dom";
17-
import { RowType, TableRowFocusEvent } from "../table/interfaces";
17+
import { RowType, TableInteractionMode, TableRowFocusEvent } from "../table/interfaces";
1818
import { isActivationKey } from "../../utils/key";
1919
import {
2020
connectInteractive,
@@ -51,6 +51,9 @@ export class TableRow implements InteractiveComponent, LocalizedComponent {
5151
/** @internal */
5252
@Prop({ mutable: true }) cellCount: number;
5353

54+
/** @internal */
55+
@Prop() interactionMode: TableInteractionMode = "interactive";
56+
5457
/** @internal */
5558
@Prop() lastVisibleRow: boolean;
5659

@@ -91,6 +94,7 @@ export class TableRow implements InteractiveComponent, LocalizedComponent {
9194
@Watch("scale")
9295
@Watch("selected")
9396
@Watch("selectedRowCount")
97+
@Watch("interactionMode")
9498
handleCellChanges(): void {
9599
if (this.tableRowEl && this.rowCells.length > 0) {
96100
this.updateCells();
@@ -198,7 +202,10 @@ export class TableRow implements InteractiveComponent, LocalizedComponent {
198202
//
199203
//--------------------------------------------------------------------------
200204

201-
private keyDownHandler(event: KeyboardEvent): void {
205+
private keyDownHandler = (event: KeyboardEvent): void => {
206+
if (this.interactionMode !== "interactive") {
207+
return;
208+
}
202209
const el = event.target as HTMLCalciteTableCellElement | HTMLCalciteTableHeaderElement;
203210
const key = event.key;
204211
const isControl = event.ctrlKey;
@@ -249,7 +256,7 @@ export class TableRow implements InteractiveComponent, LocalizedComponent {
249256
break;
250257
}
251258
}
252-
}
259+
};
253260

254261
private emitTableRowFocusRequest = (
255262
cellPosition: number,
@@ -284,6 +291,7 @@ export class TableRow implements InteractiveComponent, LocalizedComponent {
284291

285292
if (cells.length > 0) {
286293
cells?.forEach((cell: HTMLCalciteTableCellElement | HTMLCalciteTableHeaderElement, index) => {
294+
cell.interactionMode = this.interactionMode;
287295
cell.positionInRow = index + 1;
288296
cell.parentRowType = this.rowType;
289297
cell.parentRowIsSelected = this.selected;
@@ -385,7 +393,7 @@ export class TableRow implements InteractiveComponent, LocalizedComponent {
385393
aria-rowindex={this.positionAll + 1}
386394
aria-selected={this.selected}
387395
class={{ [CSS.lastVisibleRow]: this.lastVisibleRow }}
388-
onKeyDown={(event) => this.keyDownHandler(event)}
396+
onKeyDown={this.keyDownHandler}
389397
// 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)
390398
ref={(el) => (this.tableRowEl = el)}
391399
>

packages/calcite-components/src/components/table/interfaces.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ export interface TableRowFocusEvent {
1010
export type RowType = "head" | "body" | "foot";
1111

1212
export type TableLayout = "auto" | "fixed";
13+
14+
export type TableInteractionMode = "interactive" | "static";

0 commit comments

Comments
 (0)