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
+
+
+
+
+ With shadow dom elements
+ Button A
+
+
+
+ A checkbox
+
+
+ One
+ Three
+
+
+ (esc to close)
+
+
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"