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

fix: block cell nav events w internal focus queue active #6754

Merged
merged 4 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all 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": "prerelease",
"comment": "block cell nav events",
"packageName": "@microsoft/fast-foundation",
"email": "[email protected]",
"dependentChangeType": "prerelease"
}
7 changes: 5 additions & 2 deletions packages/web-components/fast-foundation/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,12 @@ export function checkboxTemplate<T extends FASTCheckbox>(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;
Expand Down Expand Up @@ -412,6 +412,9 @@ export const DayFormat: {
// @public
export type DayFormat = ValuesOf<typeof DayFormat>;

// @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
//
Expand Down
46 changes: 46 additions & 0 deletions packages/web-components/fast-foundation/src/data-grid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ export const myDataGrid = DataGrid.compose({

<hr/>

### Functions

| Name | Description | Parameters | Return |
| -------------------------------- | ----------- | ------------------------ | --------------------- |
| `defaultCellFocusTargetCallback` | | `cell: FASTDataGridCell` | `HTMLElement or null` |

<hr/>



### class: `FASTDataGridRow`
Expand Down Expand Up @@ -281,6 +289,44 @@ export const myDataGrid = DataGrid.compose({
<hr/>



### 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 | | | | |

<hr/>

### Variables

| Name | Description | Type |
| ------------------- | ----------- | ---- |
| `complexCellStyles` | | |

<hr/>

### Functions

| Name | Description | Parameters | Return |
| --------------------- | ----------- | ---------- | ------------------------ |
| `registerComplexCell` | | | |
| `complexCellTemplate` | | | `ElementViewTemplate<T>` |

<hr/>


## Additional resources

* [Component explorer examples](https://explore.fast.design/components/fast-data-grid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -35,6 +44,18 @@ const defaultHeaderCellContentsTemplate: ViewTemplate<FASTDataGridCell> = html`
</template>
`;

// 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.
*
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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;
}

Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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:
scomea marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -19,3 +20,5 @@ FASTDataGrid.define({
}),
styles,
});

registerComplexCell();
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -108,3 +109,52 @@ DataGridColumnDefinitions.args = {
{ columnDataKey: "item2" },
],
};

const editCellTemplate = html`
<template>
<fast-text-field
tabIndex="-1"
value="${x => x.rowData[x.columnDefinition.columnDataKey]}"
></fast-text-field>
</template>
`;

const checkboxCellTemplate = html`
<template>
<fast-checkbox>${x => x.rowData[x.columnDefinition.columnDataKey]}</fast-checkbox>
</template>
`;

const complexCellTemplate = html`
<template>
<complex-cell
tabIndex="-1"
></complexCell>
</template>
`;

export const DataGridEditBoxes: Story<FASTDataGrid> = 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,
},
],
};
Loading
Loading