diff --git a/change/@microsoft-fast-foundation-680ad93b-949d-43f0-8211-46d8089a97fe.json b/change/@microsoft-fast-foundation-680ad93b-949d-43f0-8211-46d8089a97fe.json new file mode 100644 index 00000000000..c319f0136a3 --- /dev/null +++ b/change/@microsoft-fast-foundation-680ad93b-949d-43f0-8211-46d8089a97fe.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "use tabbable for shadow dom", + "packageName": "@microsoft/fast-foundation", + "email": "scomea@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-components/src/dialog/dialog.stories.ts b/packages/web-components/fast-components/src/dialog/dialog.stories.ts index 6559123d7c9..d7ac55acd92 100644 --- a/packages/web-components/fast-components/src/dialog/dialog.stories.ts +++ b/packages/web-components/fast-components/src/dialog/dialog.stories.ts @@ -1,12 +1,15 @@ import { STORY_RENDERED } from "@storybook/core-events"; import addons from "@storybook/addons"; +import { Dialog as FoundationDialog } from "@microsoft/fast-foundation"; import DialogTemplate from "./fixtures/dialog.html"; import "./index.js"; addons.getChannel().addListener(STORY_RENDERED, (name: string) => { if (name.toLowerCase().startsWith("dialog")) { const button1 = document.getElementById("button1"); - const dialog1 = document.getElementById("dialog1"); + const dialog1: FoundationDialog = document.getElementById( + "dialog1" + ) as FoundationDialog; if (button1 && dialog1) { button1.addEventListener("click", (e: MouseEvent) => { @@ -17,6 +20,35 @@ addons.getChannel().addListener(STORY_RENDERED, (name: string) => { dialog1.hidden = true; }); } + + const button2 = document.getElementById("button2"); + const dialog2: FoundationDialog = document.getElementById( + "dialog2" + ) as FoundationDialog; + + const shadowButton1: HTMLElement = document.createElement("fast-button"); + shadowButton1.textContent = "Shadow Button 1"; + shadowButton1.setAttribute("tabindex", "0"); + dialog2.dialog.prepend(shadowButton1); + + const shadowNumberField: HTMLElement = document.createElement( + "fast-number-field" + ); + dialog2.dialog.prepend(shadowNumberField); + + const shadowButton2: HTMLButtonElement = document.createElement("button"); + shadowButton2.textContent = "Shadow Button 2"; + dialog2.dialog.appendChild(shadowButton2); + + if (button2 && dialog2) { + button2.addEventListener("click", (e: MouseEvent) => { + dialog2.hidden = false; + }); + + dialog2.addEventListener("dismiss", (e: Event) => { + dialog2.hidden = true; + }); + } } }); diff --git a/packages/web-components/fast-components/src/dialog/fixtures/dialog.html b/packages/web-components/fast-components/src/dialog/fixtures/dialog.html index 4f69153d800..942a64a3c07 100644 --- a/packages/web-components/fast-components/src/dialog/fixtures/dialog.html +++ b/packages/web-components/fast-components/src/dialog/fixtures/dialog.html @@ -1,10 +1,8 @@ - Show dialog + Basic Dialog

-tab queue detected automatically - Dialog queue detected automatically

(esc to close)
+ + + With shadow dom elements + +

+ + diff --git a/packages/web-components/fast-foundation/package.json b/packages/web-components/fast-foundation/package.json index f56eb4045b4..71d7158abc4 100644 --- a/packages/web-components/fast-foundation/package.json +++ b/packages/web-components/fast-foundation/package.json @@ -97,7 +97,7 @@ "dependencies": { "@microsoft/fast-element": "^1.10.1", "@microsoft/fast-web-utilities": "^5.4.1", - "tabbable": "^5.2.0", + "tabbable": "^5.3.1", "tslib": "^1.13.0" }, "customElements": "dist/custom-elements.json" diff --git a/packages/web-components/fast-foundation/src/button/button.ts b/packages/web-components/fast-foundation/src/button/button.ts index 306625205ef..425d8f9c313 100644 --- a/packages/web-components/fast-foundation/src/button/button.ts +++ b/packages/web-components/fast-foundation/src/button/button.ts @@ -162,7 +162,6 @@ export class Button extends FormAssociatedButton { super.connectedCallback(); this.proxy.setAttribute("type", this.type); - this.handleUnsupportedDelegatesFocus(); const elements = Array.from(this.control?.children) as HTMLSpanElement[]; if (elements) { @@ -229,24 +228,6 @@ export class Button extends FormAssociatedButton { }; public control: HTMLButtonElement; - - /** - * Overrides the focus call for where delegatesFocus is unsupported. - * This check works for Chrome, Edge Chromium, FireFox, and Safari - * Relevant PR on the Firefox browser: https://phabricator.services.mozilla.com/D123858 - */ - private handleUnsupportedDelegatesFocus = () => { - // Check to see if delegatesFocus is supported - if ( - window.ShadowRoot && - !window.ShadowRoot.prototype.hasOwnProperty("delegatesFocus") && - this.$fastController.definition.shadowOptions?.delegatesFocus - ) { - this.focus = () => { - this.control.focus(); - }; - } - }; } /** diff --git a/packages/web-components/fast-foundation/src/dialog/README.md b/packages/web-components/fast-foundation/src/dialog/README.md index f6febd35bfe..41483185c54 100644 --- a/packages/web-components/fast-foundation/src/dialog/README.md +++ b/packages/web-components/fast-foundation/src/dialog/README.md @@ -57,9 +57,9 @@ export const myDialog = Dialog.compose({ #### Superclass -| Name | Module | Package | -| ------------------- | --------------------------------------------- | ------- | -| `FoundationElement` | /src/foundation-element/foundation-element.js | | +| Name | Module | Package | +| ------------------- | -------------------------------- | ------- | +| `FoundationElement` | /src/foundation-element/index.js | | #### Fields diff --git a/packages/web-components/fast-foundation/src/dialog/dialog.ts b/packages/web-components/fast-foundation/src/dialog/dialog.ts index b62f43e564e..69f2c79635a 100644 --- a/packages/web-components/fast-foundation/src/dialog/dialog.ts +++ b/packages/web-components/fast-foundation/src/dialog/dialog.ts @@ -1,7 +1,7 @@ import { attr, DOM, Notifier, Observable } from "@microsoft/fast-element"; import { keyEscape, keyTab } from "@microsoft/fast-web-utilities"; -import { isTabbable } from "tabbable"; -import { FoundationElement } from "../foundation-element/foundation-element.js"; +import { FocusableElement, tabbable } from "tabbable"; +import { FoundationElement } from "../foundation-element/index.js"; /** * A Switch Custom HTML Element. @@ -194,7 +194,7 @@ export class Dialog extends FoundationElement { return; } - const bounds: (HTMLElement | SVGElement)[] = this.getTabQueueBounds(); + const bounds: FocusableElement[] = this.getTabQueueBounds(); if (bounds.length === 0) { return; @@ -207,10 +207,12 @@ export class Dialog extends FoundationElement { return; } - if (e.shiftKey && e.target === bounds[0]) { + const composed: EventTarget[] = e.composedPath(); + + if (e.shiftKey && composed.includes(bounds[0])) { bounds[bounds.length - 1].focus(); e.preventDefault(); - } else if (!e.shiftKey && e.target === bounds[bounds.length - 1]) { + } else if (!e.shiftKey && composed.includes(bounds[bounds.length - 1])) { bounds[0].focus(); e.preventDefault(); } @@ -218,17 +220,17 @@ export class Dialog extends FoundationElement { return; }; - private getTabQueueBounds = (): (HTMLElement | SVGElement)[] => { - const bounds: HTMLElement[] = []; - - return Dialog.reduceTabbableItems(bounds, this); + private getTabQueueBounds = (): FocusableElement[] => { + return tabbable(this, { + getShadowRoot: true, + }); }; /** * focus on first element of tab queue */ private focusFirstElement = (): void => { - const bounds: (HTMLElement | SVGElement)[] = this.getTabQueueBounds(); + const bounds: FocusableElement[] = this.getTabQueueBounds(); if (bounds.length > 0) { bounds[0].focus(); @@ -254,7 +256,7 @@ export class Dialog extends FoundationElement { }; /** - * + * Updates trap focus state * * @internal */ @@ -279,63 +281,4 @@ export class Dialog extends FoundationElement { document.removeEventListener("focusin", this.handleDocumentFocus); } }; - - /** - * Reduce a collection to only its focusable elements. - * - * @param elements - Collection of elements to reduce - * @param element - The current element - * - * @internal - */ - private static reduceTabbableItems( - elements: HTMLElement[], - element: FoundationElement & HTMLElement - ): HTMLElement[] { - if (element.getAttribute("tabindex") === "-1") { - return elements; - } - - if ( - isTabbable(element) || - (Dialog.isFocusableFastElement(element) && Dialog.hasTabbableShadow(element)) - ) { - elements.push(element); - return elements; - } - - if (element.childElementCount) { - return elements.concat( - Array.from(element.children).reduce(Dialog.reduceTabbableItems, []) - ); - } - - return elements; - } - - /** - * Test if element is focusable fast element - * - * @param element - The element to check - * - * @internal - */ - private static isFocusableFastElement( - element: FoundationElement & HTMLElement - ): boolean { - return !!element.$fastController?.definition.shadowOptions?.delegatesFocus; - } - - /** - * Test if the element has a focusable shadow - * - * @param element - The element to check - * - * @internal - */ - private static hasTabbableShadow(element: FoundationElement & HTMLElement) { - return Array.from(element.shadowRoot?.querySelectorAll("*") ?? []).some(x => { - return isTabbable(x); - }); - } } diff --git a/packages/web-components/fast-foundation/src/toolbar/toolbar.ts b/packages/web-components/fast-foundation/src/toolbar/toolbar.ts index d8c0a9b29e6..2c98d870426 100644 --- a/packages/web-components/fast-foundation/src/toolbar/toolbar.ts +++ b/packages/web-components/fast-foundation/src/toolbar/toolbar.ts @@ -1,6 +1,6 @@ -import { attr, FASTElement, observable, Observable } from "@microsoft/fast-element"; +import { attr, observable, Observable } from "@microsoft/fast-element"; import { ArrowKeys, Direction, limit, Orientation } from "@microsoft/fast-web-utilities"; -import { isFocusable } from "tabbable"; +import { focusable, FocusableElement } from "tabbable"; import { FoundationElement, FoundationElementDefinition, @@ -93,7 +93,7 @@ export class Toolbar extends FoundationElement { * * @internal */ - private focusableElements: HTMLElement[]; + private focusableElements: FocusableElement[]; /** * The orientation of the toolbar. @@ -233,10 +233,9 @@ export class Toolbar extends FoundationElement { * @internal */ protected reduceFocusableElements(): void { - this.focusableElements = this.allSlottedItems.reduce( - Toolbar.reduceFocusableItems, - [] - ); + this.focusableElements = focusable(this, { + getShadowRoot: true, + }); this.setFocusableElements(); } @@ -252,44 +251,6 @@ export class Toolbar extends FoundationElement { this.focusableElements[this.activeIndex]?.focus(); } - /** - * Reduce a collection to only its focusable elements. - * - * @param elements - Collection of elements to reduce - * @param element - The current element - * - * @internal - */ - private static reduceFocusableItems( - elements: HTMLElement[], - element: FASTElement & HTMLElement - ): HTMLElement[] { - const isRoleRadio = element.getAttribute("role") === "radio"; - const isFocusableFastElement = - element.$fastController?.definition.shadowOptions?.delegatesFocus; - const hasFocusableShadow = Array.from( - element.shadowRoot?.querySelectorAll("*") ?? [] - ).some(x => isFocusable(x)); - - if ( - isFocusable(element) || - isRoleRadio || - isFocusableFastElement || - hasFocusableShadow - ) { - elements.push(element); - return elements; - } - - if (element.childElementCount) { - return elements.concat( - Array.from(element.children).reduce(Toolbar.reduceFocusableItems, []) - ); - } - - return elements; - } - /** * @internal */ diff --git a/yarn.lock b/yarn.lock index 3d09d123990..07b987dccc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23165,10 +23165,10 @@ tabbable@^4.0.0: resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261" integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ== -tabbable@^5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.2.1.tgz#e3fda7367ddbb172dcda9f871c0fdb36d1c4cd9c" - integrity sha512-40pEZ2mhjaZzK0BnI+QGNjJO8UYx9pP5v7BGe17SORTO0OEuuaAwQTkAp8whcZvqon44wKFOikD+Al11K3JICQ== +tabbable@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.1.tgz#059f2a19b829efce2a0ec05785a47dd3bcd0a25b" + integrity sha512-NtO7I7eoAHR+JwwcNsi/PipamtAEebYDnur/k9wM6n238HHy/+1O4+7Zx7e/JaDAbKJPlIFYsfsV/6tPqTOQvg== table@^5.2.3: version "5.4.6"