diff --git a/change/@microsoft-fast-foundation-015b56ec-8d8e-4573-9572-9822996f1245.json b/change/@microsoft-fast-foundation-015b56ec-8d8e-4573-9572-9822996f1245.json new file mode 100644 index 00000000000..8b97a828a64 --- /dev/null +++ b/change/@microsoft-fast-foundation-015b56ec-8d8e-4573-9572-9822996f1245.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "comparisons to document.activeElement consider shadowRoot", + "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 3e92a91d4ae..03c8ffc9bdf 100644 --- a/packages/web-components/fast-foundation/docs/api-report.md +++ b/packages/web-components/fast-foundation/docs/api-report.md @@ -2363,6 +2363,9 @@ export type GenerateHeaderOptions = ValuesOf; // @public export const getDirection: (rootNode: HTMLElement) => Direction; +// @public (undocumented) +export function getRootActiveElement(element: Element): Element | null; + // @public export const hidden = ":host([hidden]){display:none}"; diff --git a/packages/web-components/fast-foundation/src/combobox/combobox.ts b/packages/web-components/fast-foundation/src/combobox/combobox.ts index 68a978eafda..dd037883a64 100644 --- a/packages/web-components/fast-foundation/src/combobox/combobox.ts +++ b/packages/web-components/fast-foundation/src/combobox/combobox.ts @@ -6,6 +6,7 @@ import type { FASTListboxOption } from "../listbox-option/listbox-option.js"; import { DelegatesARIAListbox } from "../listbox/listbox.js"; import { StartEnd } from "../patterns/start-end.js"; import type { StartEndOptions } from "../patterns/start-end.js"; +import { getRootActiveElement } from "../utilities/index.js"; import { applyMixins } from "../utilities/apply-mixins.js"; import { FormAssociatedCombobox } from "./combobox.form-associated.js"; import { ComboboxAutocomplete } from "./combobox.options.js"; @@ -337,7 +338,7 @@ export class FASTCombobox extends FormAssociatedCombobox { * Overrides: `Listbox.focusAndScrollOptionIntoView` */ protected focusAndScrollOptionIntoView(): void { - if (this.contains(document.activeElement)) { + if (this.contains(getRootActiveElement(this))) { this.control.focus(); if (this.firstSelectedOption) { requestAnimationFrame(() => { 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 030e64c51b7..f2d322fc61b 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 @@ -17,6 +17,7 @@ import { keyPageUp, } from "@microsoft/fast-web-utilities"; import { isFocusable } from "tabbable"; +import { getRootActiveElement } from "../utilities/index.js"; import type { ColumnDefinition } from "./data-grid.js"; import { DataGridCellTypes } from "./data-grid.options.js"; @@ -200,7 +201,8 @@ export class FASTDataGridCell extends FASTElement { } public handleFocusout(e: FocusEvent): void { - if (this !== document.activeElement && !this.contains(document.activeElement)) { + const activeElement: Element | null = getRootActiveElement(this); + if (this !== activeElement && !this.contains(activeElement)) { this.isActiveCell = false; } } @@ -230,7 +232,7 @@ export class FASTDataGridCell extends FASTElement { return; } - const rootActiveElement: Element | null = this.getRootActiveElement(); + const rootActiveElement: Element | null = getRootActiveElement(this); switch (e.key) { case keyEnter: @@ -292,16 +294,6 @@ export class FASTDataGridCell extends FASTElement { } } - private getRootActiveElement(): Element | null { - const rootNode = this.getRootNode(); - - if (rootNode instanceof ShadowRoot) { - return rootNode.activeElement; - } - - return document.activeElement; - } - private updateCellView(): void { this.disconnectCellView(); 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 1b2297f4471..9d505d440a9 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 @@ -20,6 +20,7 @@ import { keyPageDown, keyPageUp, } from "@microsoft/fast-web-utilities"; +import { getRootActiveElement } from "../utilities/index.js"; import type { FASTDataGridCell } from "./data-grid-cell.js"; import type { FASTDataGridRow } from "./data-grid-row.js"; import { @@ -173,12 +174,10 @@ export class FASTDataGrid extends FASTElement { if (this.noTabbing) { this.setAttribute("tabIndex", "-1"); } else { + const activeElement: Element | null = getRootActiveElement(this); this.setAttribute( "tabIndex", - this.contains(document.activeElement) || - this === document.activeElement - ? "-1" - : "0" + this.contains(activeElement) || this === activeElement ? "-1" : "0" ); } } @@ -908,9 +907,10 @@ export class FASTDataGrid extends FASTElement { }; private queueFocusUpdate(): void { + const activeElement: Element | null = getRootActiveElement(this); if ( this.isUpdatingFocus && - (this.contains(document.activeElement) || this === document.activeElement) + (this.contains(activeElement) || this === activeElement) ) { return; } diff --git a/packages/web-components/fast-foundation/src/dialog/dialog.ts b/packages/web-components/fast-foundation/src/dialog/dialog.ts index 26bc77c1b3b..ab27d31fe67 100644 --- a/packages/web-components/fast-foundation/src/dialog/dialog.ts +++ b/packages/web-components/fast-foundation/src/dialog/dialog.ts @@ -7,6 +7,7 @@ import { } from "@microsoft/fast-element"; import { keyEscape, keyTab } from "@microsoft/fast-web-utilities"; import { isTabbable } from "tabbable"; +import { getRootActiveElement } from "../utilities/index.js"; /** * A Switch Custom HTML Element. @@ -274,7 +275,7 @@ export class FASTDialog extends FASTElement { // Add an event listener for focusin events if we are trapping focus document.addEventListener("focusin", this.handleDocumentFocus); Updates.enqueue(() => { - if (this.shouldForceFocus(document.activeElement)) { + if (this.shouldForceFocus(getRootActiveElement(this))) { this.focusFirstElement(); } }); diff --git a/packages/web-components/fast-foundation/src/listbox/listbox.ts b/packages/web-components/fast-foundation/src/listbox/listbox.ts index be57467bf90..4eca9cb0714 100644 --- a/packages/web-components/fast-foundation/src/listbox/listbox.ts +++ b/packages/web-components/fast-foundation/src/listbox/listbox.ts @@ -11,6 +11,7 @@ import { keyTab, uniqueId, } from "@microsoft/fast-web-utilities"; +import { getRootActiveElement } from "../utilities/index.js"; import { FASTListboxOption, isListboxOption } from "../listbox-option/listbox-option.js"; import { ARIAGlobalStatesAndProperties } from "../patterns/index.js"; import { applyMixins } from "../utilities/apply-mixins.js"; @@ -198,7 +199,7 @@ export abstract class FASTListbox extends FASTElement { // function is typically called from the `openChanged` observer, `DOM.queueUpdate` // causes the calls to be grouped into the same frame. To prevent this, // `requestAnimationFrame` is used instead of `DOM.queueUpdate`. - if (this.contains(document.activeElement) && optionToFocus !== null) { + if (this.contains(getRootActiveElement(this)) && optionToFocus !== null) { optionToFocus.focus(); requestAnimationFrame(() => { optionToFocus.scrollIntoView({ block: "nearest" }); @@ -409,7 +410,7 @@ export abstract class FASTListbox extends FASTElement { * @internal */ public mousedownHandler(e: MouseEvent): boolean | void { - this.shouldSkipFocus = !this.contains(document.activeElement); + this.shouldSkipFocus = !this.contains(getRootActiveElement(this)); return true; } diff --git a/packages/web-components/fast-foundation/src/picker/picker.ts b/packages/web-components/fast-foundation/src/picker/picker.ts index 769f2ef9465..aae789897bb 100644 --- a/packages/web-components/fast-foundation/src/picker/picker.ts +++ b/packages/web-components/fast-foundation/src/picker/picker.ts @@ -32,6 +32,7 @@ import { FlyoutPosTop, FlyoutPosTopFill, } from "../anchored-region/index.js"; +import { getRootActiveElement } from "../utilities/index.js"; import { FASTPickerListItem } from "./picker-list-item.js"; import type { FASTPickerList } from "./picker-list.js"; import { FASTPickerMenuOption } from "./picker-menu-option.js"; @@ -556,7 +557,7 @@ export class FASTPicker extends FormAssociatedPicker { return; } - if (open && this.getRootActiveElement() === this.inputElement) { + if (open && getRootActiveElement(this) === this.inputElement) { this.flyoutOpen = open; Updates.enqueue(() => { if (this.menuElement !== undefined) { @@ -605,7 +606,7 @@ export class FASTPicker extends FormAssociatedPicker { if (e.defaultPrevented) { return false; } - const activeElement = this.getRootActiveElement(); + const activeElement = getRootActiveElement(this); switch (e.key) { // TODO: what should "home" and "end" keys do, exactly? // @@ -811,7 +812,7 @@ export class FASTPicker extends FormAssociatedPicker { this.maxSelected !== 0 && this.selectedItems.length >= this.maxSelected ) { - if (this.getRootActiveElement() === this.inputElement) { + if (getRootActiveElement(this) === this.inputElement) { const selectedItemInstances: Element[] = Array.from( this.listElement.querySelectorAll("[role='listitem']") ); @@ -825,16 +826,6 @@ export class FASTPicker extends FormAssociatedPicker { } } - private getRootActiveElement(): Element | null { - const rootNode = this.getRootNode(); - - if (rootNode instanceof ShadowRoot) { - return rootNode.activeElement; - } - - return document.activeElement; - } - /** * A list item has been invoked. */ @@ -901,7 +892,7 @@ export class FASTPicker extends FormAssociatedPicker { this.listElement.querySelectorAll("[role='listitem']") ); - const activeElement = this.getRootActiveElement(); + const activeElement = getRootActiveElement(this); if (activeElement !== null) { let currentFocusedItemIndex: number = selectedItemsAsElements.indexOf(activeElement); diff --git a/packages/web-components/fast-foundation/src/tooltip/tooltip.ts b/packages/web-components/fast-foundation/src/tooltip/tooltip.ts index 4951f7bda1f..28edc5a93c9 100644 --- a/packages/web-components/fast-foundation/src/tooltip/tooltip.ts +++ b/packages/web-components/fast-foundation/src/tooltip/tooltip.ts @@ -9,6 +9,7 @@ import { Updates, } from "@microsoft/fast-element"; import { keyEscape, uniqueId } from "@microsoft/fast-web-utilities"; +import { getRootActiveElement } from "../utilities/index.js"; import { TooltipPlacement } from "./tooltip.options.js"; /** @@ -171,7 +172,7 @@ export class FASTTooltip extends FASTElement { * @internal */ private mouseoverAnchorHandler = (): void => { - if (!document.activeElement?.isSameNode(this.anchorElement)) { + if (!getRootActiveElement(this)?.isSameNode(this.anchorElement)) { this.showTooltip(); } }; @@ -183,7 +184,7 @@ export class FASTTooltip extends FASTElement { */ private mouseoutAnchorHandler = (e: MouseEvent): void => { if ( - !document.activeElement?.isSameNode(this.anchorElement) && + !getRootActiveElement(this)?.isSameNode(this.anchorElement) && !this.isSameNode(e.relatedTarget as HTMLElement) ) { this.hideTooltip(); diff --git a/packages/web-components/fast-foundation/src/utilities/index.ts b/packages/web-components/fast-foundation/src/utilities/index.ts index 3acd79ebc11..f9e419f2964 100644 --- a/packages/web-components/fast-foundation/src/utilities/index.ts +++ b/packages/web-components/fast-foundation/src/utilities/index.ts @@ -18,3 +18,4 @@ export { export { ValuesOf } from "./typings.js"; export { whitespaceFilter } from "./whitespace-filter.js"; export { staticallyCompose, StaticallyComposableHTML } from "./template-helpers.js"; +export { getRootActiveElement } from "./root-active-element.js"; diff --git a/packages/web-components/fast-foundation/src/utilities/root-active-element.ts b/packages/web-components/fast-foundation/src/utilities/root-active-element.ts new file mode 100644 index 00000000000..85999915cbc --- /dev/null +++ b/packages/web-components/fast-foundation/src/utilities/root-active-element.ts @@ -0,0 +1,10 @@ +// returns the active element in the shadow context of the element in question. +export function getRootActiveElement(element: Element): Element | null { + const rootNode = element.getRootNode(); + + if (rootNode instanceof ShadowRoot) { + return rootNode.activeElement; + } + + return document.activeElement; +}