From a38e6b166765bb5290b0e6cf55feffdb16a8cbb4 Mon Sep 17 00:00:00 2001 From: Stephane Comeau Date: Thu, 10 Aug 2023 18:46:37 -0700 Subject: [PATCH] fix: block cell nav events w internal focus queue active (#6754) * block cell nav events * Change files * better default focus fn * update to lates --- ...-8be8ec30-b786-4058-b066-dacf147547af.json | 7 ++ .../fast-foundation/docs/api-report.md | 7 +- .../fast-foundation/src/data-grid/README.md | 46 ++++++++++ .../src/data-grid/data-grid-cell.ts | 88 +++++++++++++++---- .../src/data-grid/data-grid.ts | 4 +- .../data-grid/stories/data-grid.register.ts | 3 + .../data-grid/stories/data-grid.stories.ts | 50 +++++++++++ .../stories/examples/complex-cell.ts | 72 +++++++++++++++ 8 files changed, 257 insertions(+), 20 deletions(-) create mode 100644 change/@microsoft-fast-foundation-8be8ec30-b786-4058-b066-dacf147547af.json create mode 100644 packages/web-components/fast-foundation/src/data-grid/stories/examples/complex-cell.ts diff --git a/change/@microsoft-fast-foundation-8be8ec30-b786-4058-b066-dacf147547af.json b/change/@microsoft-fast-foundation-8be8ec30-b786-4058-b066-dacf147547af.json new file mode 100644 index 00000000000..0e1ffea1258 --- /dev/null +++ b/change/@microsoft-fast-foundation-8be8ec30-b786-4058-b066-dacf147547af.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "block cell nav events", + "packageName": "@microsoft/fast-foundation", + "email": "stephcomeau@msn.com", + "dependentChangeType": "prerelease" +} diff --git a/packages/web-components/fast-foundation/docs/api-report.md b/packages/web-components/fast-foundation/docs/api-report.md index 2f911d7778b..171ff0c75c6 100644 --- a/packages/web-components/fast-foundation/docs/api-report.md +++ b/packages/web-components/fast-foundation/docs/api-report.md @@ -264,12 +264,12 @@ export function checkboxTemplate(options?: CheckboxOptio // @public export interface ColumnDefinition { - cellFocusTargetCallback?: (cell: FASTDataGridCell) => HTMLElement; + cellFocusTargetCallback?: (cell: FASTDataGridCell) => HTMLElement | null; cellInternalFocusQueue?: boolean; cellTemplate?: ViewTemplate | SyntheticViewTemplate; columnDataKey: string; gridColumn?: string; - headerCellFocusTargetCallback?: (cell: FASTDataGridCell) => HTMLElement; + headerCellFocusTargetCallback?: (cell: FASTDataGridCell) => HTMLElement | null; headerCellInternalFocusQueue?: boolean; headerCellTemplate?: ViewTemplate | SyntheticViewTemplate; isRowHeader?: boolean; @@ -412,6 +412,9 @@ export const DayFormat: { // @public export type DayFormat = ValuesOf; +// @public (undocumented) +export const defaultCellFocusTargetCallback: (cell: FASTDataGridCell) => HTMLElement | null; + // Warning: (ae-different-release-tags) This symbol has another declaration with a different release tag // Warning: (ae-internal-mixed-release-tag) Mixed release tags are not allowed for "DelegatesARIAButton" because one of its declarations is marked as @internal // diff --git a/packages/web-components/fast-foundation/src/data-grid/README.md b/packages/web-components/fast-foundation/src/data-grid/README.md index 4942e54290e..102235a5a7d 100644 --- a/packages/web-components/fast-foundation/src/data-grid/README.md +++ b/packages/web-components/fast-foundation/src/data-grid/README.md @@ -144,6 +144,14 @@ export const myDataGrid = DataGrid.compose({
+### Functions + +| Name | Description | Parameters | Return | +| -------------------------------- | ----------- | ------------------------ | --------------------- | +| `defaultCellFocusTargetCallback` | | `cell: FASTDataGridCell` | `HTMLElement or null` | + +
+ ### class: `FASTDataGridRow` @@ -281,6 +289,44 @@ export const myDataGrid = DataGrid.compose({
+ +### class: `ComplexCell` + +#### Superclass + +| Name | Module | Package | +| ------------- | ------ | ----------------------- | +| `FASTElement` | | @microsoft/fast-element | + +#### Fields + +| Name | Privacy | Type | Default | Description | Inherited From | +| --------------- | ------- | ------------------- | ------- | ----------- | -------------- | +| `buttonA` | public | `HTMLButtonElement` | | | | +| `buttonB` | public | `HTMLButtonElement` | | | | +| `handleFocus` | public | | | | | +| `handleKeyDown` | public | | | | | + +
+ +### Variables + +| Name | Description | Type | +| ------------------- | ----------- | ---- | +| `complexCellStyles` | | | + +
+ +### Functions + +| Name | Description | Parameters | Return | +| --------------------- | ----------- | ---------- | ------------------------ | +| `registerComplexCell` | | | | +| `complexCellTemplate` | | | `ElementViewTemplate` | + +
+ + ## Additional resources * [Component explorer examples](https://explore.fast.design/components/fast-data-grid) diff --git a/packages/web-components/fast-foundation/src/data-grid/data-grid-cell.ts b/packages/web-components/fast-foundation/src/data-grid/data-grid-cell.ts index 4cfecf8ec23..030e64c51b7 100644 --- a/packages/web-components/fast-foundation/src/data-grid/data-grid-cell.ts +++ b/packages/web-components/fast-foundation/src/data-grid/data-grid-cell.ts @@ -4,10 +4,19 @@ import { eventFocusIn, eventFocusOut, eventKeyDown, + keyArrowDown, + keyArrowLeft, + keyArrowRight, + keyArrowUp, + keyEnd, keyEnter, keyEscape, keyFunction2, + keyHome, + keyPageDown, + keyPageUp, } from "@microsoft/fast-web-utilities"; +import { isFocusable } from "tabbable"; import type { ColumnDefinition } from "./data-grid.js"; import { DataGridCellTypes } from "./data-grid.options.js"; @@ -35,6 +44,18 @@ const defaultHeaderCellContentsTemplate: ViewTemplate = html` `; +// basic focusTargetCallback that returns the first child of the cell +export const defaultCellFocusTargetCallback = ( + cell: FASTDataGridCell +): HTMLElement | null => { + for (let i = 0; i < cell.children.length; i++) { + if (isFocusable(cell.children[i])) { + return cell.children[i] as HTMLElement; + } + } + return null; +}; + /** * A Data Grid Cell Custom HTML Element. * @@ -151,7 +172,7 @@ export class FASTDataGridCell extends FASTElement { "function" ) { // move focus to the focus target - const focusTarget: HTMLElement = + const focusTarget: HTMLElement | null = this.columnDefinition.headerCellFocusTargetCallback(this); if (focusTarget !== null) { focusTarget.focus(); @@ -166,7 +187,7 @@ export class FASTDataGridCell extends FASTElement { typeof this.columnDefinition.cellFocusTargetCallback === "function" ) { // move focus to the focus target - const focusTarget: HTMLElement = + const focusTarget: HTMLElement | null = this.columnDefinition.cellFocusTargetCallback(this); if (focusTarget !== null) { focusTarget.focus(); @@ -184,25 +205,37 @@ export class FASTDataGridCell extends FASTElement { } } + private hasInternalFocusQueue(): boolean { + if (this.columnDefinition === null) { + return false; + } + if ( + (this.cellType === DataGridCellTypes.default && + this.columnDefinition.cellInternalFocusQueue) || + (this.cellType === DataGridCellTypes.columnHeader && + this.columnDefinition.headerCellInternalFocusQueue) + ) { + return true; + } + return false; + } + public handleKeydown(e: KeyboardEvent): void { + // if the cell does not have an internal focus queue we can ignore keystrokes if ( e.defaultPrevented || this.columnDefinition === null || - (this.cellType === DataGridCellTypes.default && - this.columnDefinition.cellInternalFocusQueue !== true) || - (this.cellType === DataGridCellTypes.columnHeader && - this.columnDefinition.headerCellInternalFocusQueue !== true) + !this.hasInternalFocusQueue() ) { return; } + const rootActiveElement: Element | null = this.getRootActiveElement(); + switch (e.key) { case keyEnter: case keyFunction2: - if ( - this.contains(document.activeElement) && - document.activeElement !== this - ) { + if (this.contains(rootActiveElement) && rootActiveElement !== this) { return; } @@ -212,7 +245,7 @@ export class FASTDataGridCell extends FASTElement { this.columnDefinition.headerCellFocusTargetCallback !== undefined ) { - const focusTarget: HTMLElement = + const focusTarget: HTMLElement | null = this.columnDefinition.headerCellFocusTargetCallback(this); if (focusTarget !== null) { focusTarget.focus(); @@ -223,7 +256,7 @@ export class FASTDataGridCell extends FASTElement { default: if (this.columnDefinition.cellFocusTargetCallback !== undefined) { - const focusTarget: HTMLElement = + const focusTarget: HTMLElement | null = this.columnDefinition.cellFocusTargetCallback(this); if (focusTarget !== null) { focusTarget.focus(); @@ -235,15 +268,38 @@ export class FASTDataGridCell extends FASTElement { break; case keyEscape: - if ( - this.contains(document.activeElement) && - document.activeElement !== this - ) { + if (this.contains(rootActiveElement) && rootActiveElement !== this) { this.focus(); e.preventDefault(); } break; + + // stop any unhandled grid nav events that may bubble from the cell + // when internal navigation is active. + // note: preventDefault would also block arrow keys in input elements + case keyArrowDown: + case keyArrowLeft: + case keyArrowRight: + case keyArrowUp: + case keyEnd: + case keyHome: + case keyPageDown: + case keyPageUp: + if (this.contains(rootActiveElement) && rootActiveElement !== this) { + e.stopPropagation(); + } + break; + } + } + + private getRootActiveElement(): Element | null { + const rootNode = this.getRootNode(); + + if (rootNode instanceof ShadowRoot) { + return rootNode.activeElement; } + + return document.activeElement; } private updateCellView(): void { diff --git a/packages/web-components/fast-foundation/src/data-grid/data-grid.ts b/packages/web-components/fast-foundation/src/data-grid/data-grid.ts index e8c1d269caf..c0d00582a08 100644 --- a/packages/web-components/fast-foundation/src/data-grid/data-grid.ts +++ b/packages/web-components/fast-foundation/src/data-grid/data-grid.ts @@ -78,7 +78,7 @@ export interface ColumnDefinition { * focus directly to the checkbox. * When headerCellInternalFocusQueue is true this function is called when the user hits Enter or F2 */ - headerCellFocusTargetCallback?: (cell: FASTDataGridCell) => HTMLElement; + headerCellFocusTargetCallback?: (cell: FASTDataGridCell) => HTMLElement | null; /** * cell template @@ -98,7 +98,7 @@ export interface ColumnDefinition { * When cellInternalFocusQueue is true this function is called when the user hits Enter or F2 */ - cellFocusTargetCallback?: (cell: FASTDataGridCell) => HTMLElement; + cellFocusTargetCallback?: (cell: FASTDataGridCell) => HTMLElement | null; /** * Whether this column is the row header diff --git a/packages/web-components/fast-foundation/src/data-grid/stories/data-grid.register.ts b/packages/web-components/fast-foundation/src/data-grid/stories/data-grid.register.ts index 115539e05f5..ceb48e71e42 100644 --- a/packages/web-components/fast-foundation/src/data-grid/stories/data-grid.register.ts +++ b/packages/web-components/fast-foundation/src/data-grid/stories/data-grid.register.ts @@ -1,6 +1,7 @@ import { css } from "@microsoft/fast-element"; import { FASTDataGrid } from "../data-grid.js"; import { dataGridTemplate } from "../data-grid.template.js"; +import { registerComplexCell } from "./examples/complex-cell.js"; const styles = css` :host { @@ -19,3 +20,5 @@ FASTDataGrid.define({ }), styles, }); + +registerComplexCell(); diff --git a/packages/web-components/fast-foundation/src/data-grid/stories/data-grid.stories.ts b/packages/web-components/fast-foundation/src/data-grid/stories/data-grid.stories.ts index eff2510aec6..7f707662f04 100644 --- a/packages/web-components/fast-foundation/src/data-grid/stories/data-grid.stories.ts +++ b/packages/web-components/fast-foundation/src/data-grid/stories/data-grid.stories.ts @@ -1,6 +1,7 @@ import { html } from "@microsoft/fast-element"; import type { Meta, Story, StoryArgs } from "../../__test__/helpers.js"; import { renderComponent } from "../../__test__/helpers.js"; +import { defaultCellFocusTargetCallback } from "../data-grid-cell.js"; import { DataGridSelectionBehavior, DataGridSelectionMode, @@ -108,3 +109,52 @@ DataGridColumnDefinitions.args = { { columnDataKey: "item2" }, ], }; + +const editCellTemplate = html` + +`; + +const checkboxCellTemplate = html` + +`; + +const complexCellTemplate = html` + +`; + +export const DataGridEditBoxes: Story = renderComponent(storyTemplate).bind( + {} +); +DataGridEditBoxes.args = { + columnDefinitions: [ + { columnDataKey: "rowId" }, + { + columnDataKey: "item1", + cellTemplate: checkboxCellTemplate, + cellFocusTargetCallback: defaultCellFocusTargetCallback, + }, + { + columnDataKey: "item2", + cellInternalFocusQueue: true, + cellTemplate: editCellTemplate, + cellFocusTargetCallback: defaultCellFocusTargetCallback, + }, + { + columnDataKey: "item3", + cellInternalFocusQueue: true, + cellTemplate: complexCellTemplate, + cellFocusTargetCallback: defaultCellFocusTargetCallback, + }, + ], +}; diff --git a/packages/web-components/fast-foundation/src/data-grid/stories/examples/complex-cell.ts b/packages/web-components/fast-foundation/src/data-grid/stories/examples/complex-cell.ts new file mode 100644 index 00000000000..5a0c6fc089d --- /dev/null +++ b/packages/web-components/fast-foundation/src/data-grid/stories/examples/complex-cell.ts @@ -0,0 +1,72 @@ +import { + css, + ElementViewTemplate, + FASTElement, + html, + ref, +} from "@microsoft/fast-element"; +import { eventFocus, keyArrowLeft, keyArrowRight } from "@microsoft/fast-web-utilities"; + +export function registerComplexCell() { + ComplexCell.define({ + name: "complex-cell", + template: complexCellTemplate(), + styles: complexCellStyles, + }); +} + +export class ComplexCell extends FASTElement { + public buttonA: HTMLButtonElement; + public buttonB: HTMLButtonElement; + + public connectedCallback(): void { + super.connectedCallback(); + this.addEventListener(eventFocus, this.handleFocus); + } + + public disconnectedCallback(): void { + super.disconnectedCallback(); + } + + public handleFocus = (e: FocusEvent): void => { + this.buttonA.focus(); + }; + + public handleKeyDown = (e: KeyboardEvent): boolean => { + if (e.key !== keyArrowLeft && e.key !== keyArrowRight) { + return true; + } + if (e.target === this.buttonA) { + this.buttonB.focus(); + } else { + this.buttonA.focus(); + } + return false; + }; +} + +export function complexCellTemplate(): ElementViewTemplate { + return html` + + `; +} + +export const complexCellStyles = css` + :host { + } +`;