diff --git a/change/@fluentui-dom-utilities-781d59ca-cc21-4853-8971-45459939f85d.json b/change/@fluentui-dom-utilities-781d59ca-cc21-4853-8971-45459939f85d.json new file mode 100644 index 00000000000000..eb6aa4897f80ee --- /dev/null +++ b/change/@fluentui-dom-utilities-781d59ca-cc21-4853-8971-45459939f85d.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add shadow DOM support for DOM APIs", + "packageName": "@fluentui/dom-utilities", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-focus-a8531b74-9b98-4cb7-8806-cae9890d42ed.json b/change/@fluentui-react-focus-a8531b74-9b98-4cb7-8806-cae9890d42ed.json new file mode 100644 index 00000000000000..b64655b38e2fec --- /dev/null +++ b/change/@fluentui-react-focus-a8531b74-9b98-4cb7-8806-cae9890d42ed.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add shadow DOM support when traversing the DOM", + "packageName": "@fluentui/react-focus", + "email": "seanmonahan@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/dom-utilities/etc/dom-utilities.api.md b/packages/dom-utilities/etc/dom-utilities.api.md index 24373f59c4fcc0..89379ee2bf3f55 100644 --- a/packages/dom-utilities/etc/dom-utilities.api.md +++ b/packages/dom-utilities/etc/dom-utilities.api.md @@ -16,9 +16,15 @@ export function elementContainsAttribute(element: HTMLElement, attribute: string // @public export function findElementRecursive(element: HTMLElement | null, matchFunction: (element: HTMLElement) => boolean): HTMLElement | null; +// @public (undocumented) +export const getActiveElement: (doc: Document) => Element | null; + // @public export function getChildren(parent: HTMLElement, allowVirtualChildren?: boolean): HTMLElement[]; +// @public (undocumented) +export const getEventTarget: (event: Event) => HTMLElement | null; + // @public export function getParent(child: HTMLElement, allowVirtualParents?: boolean): HTMLElement | null; @@ -46,7 +52,6 @@ export function setPortalAttribute(element: HTMLElement): void; // @public export function setVirtualParent(child: HTMLElement, parent: HTMLElement | null): void; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/dom-utilities/src/getActiveElement.ts b/packages/dom-utilities/src/getActiveElement.ts new file mode 100644 index 00000000000000..508efba43cb1f2 --- /dev/null +++ b/packages/dom-utilities/src/getActiveElement.ts @@ -0,0 +1,9 @@ +export const getActiveElement = (doc: Document): Element | null => { + let ae = doc.activeElement; + + while (ae?.shadowRoot) { + ae = ae.shadowRoot.activeElement; + } + + return ae; +}; diff --git a/packages/dom-utilities/src/getEventTarget.ts b/packages/dom-utilities/src/getEventTarget.ts new file mode 100644 index 00000000000000..c9afac3176c4c5 --- /dev/null +++ b/packages/dom-utilities/src/getEventTarget.ts @@ -0,0 +1,8 @@ +export const getEventTarget = (event: Event): HTMLElement | null => { + let target = event.target as HTMLElement; + if (target && target.shadowRoot) { + target = event.composedPath()[0] as HTMLElement; + } + + return target; +}; diff --git a/packages/dom-utilities/src/getParent.ts b/packages/dom-utilities/src/getParent.ts index 9bc8841fbf521d..79698010351045 100644 --- a/packages/dom-utilities/src/getParent.ts +++ b/packages/dom-utilities/src/getParent.ts @@ -7,8 +7,28 @@ import { getVirtualParent } from './getVirtualParent'; * @public */ export function getParent(child: HTMLElement, allowVirtualParents: boolean = true): HTMLElement | null { - return ( - child && - ((allowVirtualParents && getVirtualParent(child)) || (child.parentNode && (child.parentNode as HTMLElement))) - ); + if (!child) { + return null; + } + + const parent = allowVirtualParents && getVirtualParent(child); + + if (parent) { + return parent; + } + + // Support looking for parents in shadow DOM + if ( + typeof (child as HTMLSlotElement).assignedElements !== 'function' && + (child as HTMLElement).assignedSlot?.parentNode + ) { + // Element is slotted + return (child as HTMLElement).assignedSlot as HTMLElement; + } else if (child.parentNode?.nodeType === 11) { + // nodeType 11 is DOCUMENT_FRAGMENT + // Element is in shadow root + return (child.parentNode as ShadowRoot).host as HTMLElement; + } else { + return child.parentNode as HTMLElement; + } } diff --git a/packages/dom-utilities/src/index.ts b/packages/dom-utilities/src/index.ts index 29a7dd4ffadbc0..9b226dc459d9d6 100644 --- a/packages/dom-utilities/src/index.ts +++ b/packages/dom-utilities/src/index.ts @@ -1,13 +1,15 @@ -export * from './IVirtualElement'; -export * from './elementContains'; -export * from './elementContainsAttribute'; -export * from './findElementRecursive'; -export * from './getChildren'; -export * from './getParent'; -export * from './getVirtualParent'; -export * from './isVirtualElement'; -export * from './portalContainsElement'; -export * from './setPortalAttribute'; -export * from './setVirtualParent'; +export type { IVirtualElement } from './IVirtualElement'; +export { elementContains } from './elementContains'; +export { elementContainsAttribute } from './elementContainsAttribute'; +export { findElementRecursive } from './findElementRecursive'; +export { getActiveElement } from './getActiveElement'; +export { getChildren } from './getChildren'; +export { getEventTarget } from './getEventTarget'; +export { getParent } from './getParent'; +export { getVirtualParent } from './getVirtualParent'; +export { isVirtualElement } from './isVirtualElement'; +export { portalContainsElement } from './portalContainsElement'; +export { DATA_PORTAL_ATTRIBUTE, setPortalAttribute } from './setPortalAttribute'; +export { setVirtualParent } from './setVirtualParent'; import './version'; diff --git a/packages/react-charting/src/components/DonutChart/__snapshots__/DonutChartRTL.test.tsx.snap b/packages/react-charting/src/components/DonutChart/__snapshots__/DonutChartRTL.test.tsx.snap index 4cf9bb7ff8e646..4f1761fabaa468 100644 --- a/packages/react-charting/src/components/DonutChart/__snapshots__/DonutChartRTL.test.tsx.snap +++ b/packages/react-charting/src/components/DonutChart/__snapshots__/DonutChartRTL.test.tsx.snap @@ -205,7 +205,7 @@ exports[`Donut chart interactions Should hide callout on mouse leave 1`] = ` opacity: 0.8; } > - +
- - - + + +
@@ -743,9 +743,9 @@ exports[`HorizontalBarChartWithAxis snapShot testing renders hideLegend correctl x={40} y={4} /> - - - + + + @@ -872,9 +872,9 @@ exports[`HorizontalBarChartWithAxis snapShot testing renders showToolTipForYAxis x={40} y={14.25} /> - - - + + + @@ -1479,9 +1479,9 @@ exports[`HorizontalBarChartWithAxis snapShot testing renders showYAxisLables cor x={40} y={14.25} /> - - - + + + diff --git a/packages/react-charting/src/components/HorizontalBarChartWithAxis/__snapshots__/HorizontalBarChartWithAxisRTL.test.tsx.snap b/packages/react-charting/src/components/HorizontalBarChartWithAxis/__snapshots__/HorizontalBarChartWithAxisRTL.test.tsx.snap index ea3e12d86fd5b2..93de8942dddfa0 100644 --- a/packages/react-charting/src/components/HorizontalBarChartWithAxis/__snapshots__/HorizontalBarChartWithAxisRTL.test.tsx.snap +++ b/packages/react-charting/src/components/HorizontalBarChartWithAxis/__snapshots__/HorizontalBarChartWithAxisRTL.test.tsx.snap @@ -219,6 +219,20 @@ exports[`Horizontal bar chart with axis - Screen resolution Should remain unchan @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="0" @@ -305,6 +319,20 @@ exports[`Horizontal bar chart with axis - Screen resolution Should remain unchan @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -391,6 +419,20 @@ exports[`Horizontal bar chart with axis - Screen resolution Should remain unchan @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -477,6 +519,20 @@ exports[`Horizontal bar chart with axis - Screen resolution Should remain unchan @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -746,6 +802,20 @@ exports[`Horizontal bar chart with axis - Screen resolution Should remain unchan @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="0" @@ -832,6 +902,20 @@ exports[`Horizontal bar chart with axis - Screen resolution Should remain unchan @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -918,6 +1002,20 @@ exports[`Horizontal bar chart with axis - Screen resolution Should remain unchan @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -1004,6 +1102,20 @@ exports[`Horizontal bar chart with axis - Screen resolution Should remain unchan @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -1286,6 +1398,20 @@ exports[`Horizontal bar chart with axis - Theme Should reflect theme change 1`] @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #a19f9d; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #a19f9d; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #a19f9d; + } data-is-focusable="true" role="option" tabindex="0" @@ -1372,6 +1498,20 @@ exports[`Horizontal bar chart with axis - Theme Should reflect theme change 1`] @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #a19f9d; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #a19f9d; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #a19f9d; + } data-is-focusable="true" role="option" tabindex="-1" @@ -1458,6 +1598,20 @@ exports[`Horizontal bar chart with axis - Theme Should reflect theme change 1`] @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #a19f9d; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #a19f9d; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #a19f9d; + } data-is-focusable="true" role="option" tabindex="-1" @@ -1544,6 +1698,20 @@ exports[`Horizontal bar chart with axis - Theme Should reflect theme change 1`] @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #a19f9d; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #a19f9d; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #a19f9d; + } data-is-focusable="true" role="option" tabindex="-1" @@ -1814,6 +1982,20 @@ exports[`Horizontal bar chart with axis rendering Should render the Horizontal b @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="0" @@ -1900,6 +2082,20 @@ exports[`Horizontal bar chart with axis rendering Should render the Horizontal b @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -1986,6 +2182,20 @@ exports[`Horizontal bar chart with axis rendering Should render the Horizontal b @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -2072,6 +2282,20 @@ exports[`Horizontal bar chart with axis rendering Should render the Horizontal b @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -2337,6 +2561,20 @@ exports[`Horizontal bar chart with axis rendering Should render the Horizontal b @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="0" @@ -2421,6 +2659,20 @@ exports[`Horizontal bar chart with axis rendering Should render the Horizontal b @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -2505,6 +2757,20 @@ exports[`Horizontal bar chart with axis rendering Should render the Horizontal b @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -2589,6 +2855,20 @@ exports[`Horizontal bar chart with axis rendering Should render the Horizontal b @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -3272,6 +3552,20 @@ exports[`HorizontalBarChartWithAxis - mouse events Should render callout correct @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="0" @@ -3358,6 +3652,20 @@ exports[`HorizontalBarChartWithAxis - mouse events Should render callout correct @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -3444,6 +3752,20 @@ exports[`HorizontalBarChartWithAxis - mouse events Should render callout correct @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -3530,6 +3852,20 @@ exports[`HorizontalBarChartWithAxis - mouse events Should render callout correct @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" diff --git a/packages/react-charting/src/components/LineChart/__snapshots__/LineChartRTL.test.tsx.snap b/packages/react-charting/src/components/LineChart/__snapshots__/LineChartRTL.test.tsx.snap index 6a03af3b7877ac..a1eca66b8375d4 100644 --- a/packages/react-charting/src/components/LineChart/__snapshots__/LineChartRTL.test.tsx.snap +++ b/packages/react-charting/src/components/LineChart/__snapshots__/LineChartRTL.test.tsx.snap @@ -2135,6 +2135,20 @@ exports[`Screen resolution Should remain unchanged on zoom in 1`] = ` @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="0" @@ -2221,6 +2235,20 @@ exports[`Screen resolution Should remain unchanged on zoom in 1`] = ` @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -2307,6 +2335,20 @@ exports[`Screen resolution Should remain unchanged on zoom in 1`] = ` @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" diff --git a/packages/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChart.test.tsx.snap b/packages/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChart.test.tsx.snap index 2caeb768545218..cbfe863ae2432f 100644 --- a/packages/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChart.test.tsx.snap +++ b/packages/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChart.test.tsx.snap @@ -104,9 +104,9 @@ exports[`VerticalBarChart snapShot testing Should not render bar labels 1`] = ` transform="translate(40, 0)" /> - - - + + + @@ -599,9 +599,9 @@ exports[`VerticalBarChart snapShot testing renders VerticalBarChart correctly 1` transform="translate(40, 0)" /> - - - + + + @@ -1094,9 +1094,9 @@ exports[`VerticalBarChart snapShot testing renders enabledLegendsWrapLines corre transform="translate(40, 0)" /> - - - + + + @@ -1569,9 +1569,9 @@ exports[`VerticalBarChart snapShot testing renders hideLegend correctly 1`] = ` transform="translate(40, 0)" /> - - - + + + @@ -1682,9 +1682,9 @@ exports[`VerticalBarChart snapShot testing renders hideTooltip correctly 1`] = ` transform="translate(40, 0)" /> - - - + + + @@ -2177,9 +2177,9 @@ exports[`VerticalBarChart snapShot testing renders showXAxisLablesTooltip correc transform="translate(40, 0)" /> - - - + + + @@ -2672,9 +2672,9 @@ exports[`VerticalBarChart snapShot testing renders wrapXAxisLables correctly 1`] transform="translate(40, 0)" /> - - - + + + @@ -3167,9 +3167,9 @@ exports[`VerticalBarChart snapShot testing renders yAxisTickFormat correctly 1`] transform="translate(40, 0)" /> - - - + + + diff --git a/packages/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChartRTL.test.tsx.snap b/packages/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChartRTL.test.tsx.snap index 6c5cd6ca2372fc..9e5508a25af950 100644 --- a/packages/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChartRTL.test.tsx.snap +++ b/packages/react-charting/src/components/VerticalBarChart/__snapshots__/VerticalBarChartRTL.test.tsx.snap @@ -98,9 +98,9 @@ exports[`Screen resolution Should remain unchanged on zoom in 1`] = ` transform="translate(40, 0)" /> - - - + + + @@ -564,9 +564,9 @@ exports[`Screen resolution Should remain unchanged on zoom out 1`] = ` transform="translate(40, 0)" /> - - - + + + @@ -1043,9 +1043,9 @@ exports[`Should reflect theme change 1`] = ` transform="translate(40, 0)" /> - - - + + + @@ -1521,9 +1521,9 @@ exports[`Vertical bar chart re-rendering Should re-render the vertical bar chart transform="translate(40, 0)" /> - - - + + + @@ -1987,9 +1987,9 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with transform="translate(40, 0)" /> - - - + + + @@ -2453,10 +2453,10 @@ exports[`Vertical bar chart rendering Should render the vertical bar chart with transform="translate(40, 0)" /> - - - - + + + + @@ -3425,6 +3425,20 @@ exports[`VerticalBarChart - mouse events Should render callout correctly on mous @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="0" @@ -3511,6 +3525,20 @@ exports[`VerticalBarChart - mouse events Should render callout correctly on mous @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -3597,6 +3625,20 @@ exports[`VerticalBarChart - mouse events Should render callout correctly on mous @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -4212,6 +4254,20 @@ exports[`VerticalBarChart - mouse events Should render customized callout on mou @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="0" @@ -4298,6 +4354,20 @@ exports[`VerticalBarChart - mouse events Should render customized callout on mou @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -4384,6 +4454,20 @@ exports[`VerticalBarChart - mouse events Should render customized callout on mou @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" diff --git a/packages/react-charting/src/components/VerticalStackedBarChart/__snapshots__/VerticalStackedBarChartRTL.test.tsx.snap b/packages/react-charting/src/components/VerticalStackedBarChart/__snapshots__/VerticalStackedBarChartRTL.test.tsx.snap index 6b1b55b3ec8594..d043a5f00c04a8 100644 --- a/packages/react-charting/src/components/VerticalStackedBarChart/__snapshots__/VerticalStackedBarChartRTL.test.tsx.snap +++ b/packages/react-charting/src/components/VerticalStackedBarChart/__snapshots__/VerticalStackedBarChartRTL.test.tsx.snap @@ -868,6 +868,20 @@ exports[`Vertical stacked bar chart - Screen resolution Should remain unchanged @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="0" @@ -954,6 +968,20 @@ exports[`Vertical stacked bar chart - Screen resolution Should remain unchanged @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -1040,6 +1068,20 @@ exports[`Vertical stacked bar chart - Screen resolution Should remain unchanged @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -1126,6 +1168,20 @@ exports[`Vertical stacked bar chart - Screen resolution Should remain unchanged @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -3042,6 +3098,20 @@ exports[`VerticalStackedBarChart - mouse events Should render callout correctly @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="0" @@ -3128,6 +3198,20 @@ exports[`VerticalStackedBarChart - mouse events Should render callout correctly @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -3742,6 +3826,20 @@ exports[`VerticalStackedBarChart - mouse events Should render customized callout @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="0" @@ -3828,6 +3926,20 @@ exports[`VerticalStackedBarChart - mouse events Should render customized callout @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -4474,6 +4586,20 @@ exports[`VerticalStackedBarChart - mouse events Should render customized callout @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="0" @@ -4560,6 +4686,20 @@ exports[`VerticalStackedBarChart - mouse events Should render customized callout @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" @@ -4646,6 +4786,20 @@ exports[`VerticalStackedBarChart - mouse events Should render customized callout @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){.ms-Fabric--isFocusVisible &:focus:after { outline-color: #605e5c; } + :host(.ms-Fabric--isFocusVisible) &:focus:after { + border: 1px solid transparent; + bottom: 1px; + content: ""; + left: 1px; + outline: 1px solid #605e5c; + position: absolute; + right: 1px; + top: 1px; + z-index: 1; + } + @media screen and (-ms-high-contrast: active), screen and (forced-colors: active){:host(.ms-Fabric--isFocusVisible) &:focus:after { + outline-color: #605e5c; + } data-is-focusable="true" role="option" tabindex="-1" diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.Box.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.Box.Example.tsx new file mode 100644 index 00000000000000..89c5d56a778da5 --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.Box.Example.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusTrapZoneBoxExample } from '../FocusTrapZone/FocusTrapZone.Box.Example'; + +export const ShadowDOMFocusTrapZoneBoxExample: React.FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.BoxClick.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.BoxClick.Example.tsx new file mode 100644 index 00000000000000..f2e3b35f1607a4 --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.BoxClick.Example.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusTrapZoneBoxClickExample } from '../FocusTrapZone/FocusTrapZone.Box.Click.Example'; + +export const ShadowDOMFocusTrapZoneBoxClickExample: React.FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.ChildShadowDom.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.ChildShadowDom.Example.tsx new file mode 100644 index 00000000000000..318c88203dd5ab --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.ChildShadowDom.Example.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusTrapZone, Toggle } from '@fluentui/react'; + +export const ShadowDOMFocusTrapZoneChildShadowDomExample: React.FunctionComponent = () => { + const [disabled, setDisabled] = React.useState(true); + const buttonsConstructed = React.useRef(false); + + const setRef = (node: HTMLElement | null) => { + if (node && buttonsConstructed.current === false) { + const btns = Array.from(node.querySelectorAll('.demo-button')); + + btns.forEach((btn, i) => { + // Ideally this would be a custom element but because of the way + // Stories are transpiled, classes are converted to ES5 which breaks + // custom elements (they require ES6 classes). + // This approach gives us a shadow root to demonstrate the feature. + const shadowRoot = btn.attachShadow({ mode: 'open' }); + + shadowRoot.innerHTML = ``; + }); + + buttonsConstructed.current = true; + } + }; + + return ( + + setDisabled(!disabled)} + label="Enable trap zone" + onText="On (toggle to exit)" + offText="Off" + /> + +
+
+
+
+ + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.DialogInPanel.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.DialogInPanel.Example.tsx new file mode 100644 index 00000000000000..8fec30a225478b --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.DialogInPanel.Example.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusTrapZoneDialogInPanelExample } from '../FocusTrapZone/FocusTrapZone.DialogInPanel.Example'; + +export const ShadowDOMFocusTrapZoneDialogInPanelExample: React.FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.FocusOnCustomElement.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.FocusOnCustomElement.Example.tsx new file mode 100644 index 00000000000000..aede2623a22b2a --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.FocusOnCustomElement.Example.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusTrapZoneBoxCustomElementExample } from '../FocusTrapZone/FocusTrapZone.Box.FocusOnCustomElement.Example'; + +export const ShadowDOMFocusTrapZoneBoxCustomElementExample: React.FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.FocusZone.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.FocusZone.Example.tsx new file mode 100644 index 00000000000000..a6336f8f3e87d9 --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.FocusZone.Example.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusTrapZoneFocusZoneExample } from '../FocusTrapZone/FocusTrapZone.FocusZone.Example'; + +export const ShadowDOMFocusTrapZoneFocusZoneExample: React.FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.Nested.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.Nested.Example.tsx new file mode 100644 index 00000000000000..adba04df00a0ed --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.Nested.Example.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusTrapZoneNestedExample } from '../FocusTrapZone/FocusTrapZone.Nested.Example'; + +export const ShadowDOMFocusTrapZoneNestedExample: React.FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.NestedChildShadowDom.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.NestedChildShadowDom.Example.tsx new file mode 100644 index 00000000000000..6b9b0cdc4771f8 --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.NestedChildShadowDom.Example.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusTrapZone, Toggle } from '@fluentui/react'; + +type ZoneProps = { + zoneNumber: number; + depth: number; +}; + +const Zone: React.FC = ({ zoneNumber, depth, children }) => { + const [disabled, setDisabled] = React.useState(true); + const buttonsConstructed = React.useRef(false); + + const style = React.useMemo(() => { + return { + marginLeft: 10 * depth, + border: `2px solid ${!disabled ? '#ababab' : 'transparent'}`, + padding: 10, + }; + }, [depth, disabled]); + + const setRef = (node: HTMLElement | null) => { + if (node && buttonsConstructed.current === false) { + const btns = Array.from(node.querySelectorAll(':scope > .demo-button')); + + btns.forEach((btn, i) => { + // Ideally this would be a custom element but because of the way + // Stories are transpiled, classes are converted to ES5 which breaks + // custom elements (they require ES6 classes). + // This approach gives us a shadow root to demonstrate the feature. + const shadowRoot = btn.attachShadow({ mode: 'open' }); + + shadowRoot.innerHTML = ``; + }); + + buttonsConstructed.current = true; + } + }; + + return ( +
+ setDisabled(!disabled)} + label={`Enable trap zone ${zoneNumber}`} + onText="On (toggle to exit)" + offText="Off" + /> + +
+
+
+
+ {children} + +
+ ); +}; + +export const ShadowDOMFocusTrapZoneNestedChildShadowDomExample: React.FunctionComponent = () => { + return ( + + + + + + + + + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.Disabled.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.Disabled.Example.tsx new file mode 100644 index 00000000000000..75c5a0ceaeceae --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.Disabled.Example.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusZoneDisabledExample } from '../../react-focus/FocusZone/FocusZone.Disabled.Example'; + +export const ShadowDOMFocusZoneDisabledExample: React.FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.HorizontalMenu.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.HorizontalMenu.Example.tsx new file mode 100644 index 00000000000000..447f786efa2cb2 --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.HorizontalMenu.Example.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusZoneHorizontalMenuExample } from '../../react-focus/FocusZone/FocusZone.HorizontalMenu.Example'; + +export const ShadowDOMFocusZoneHorizontalMenuExample: React.FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.HorizontalMenuShadowDom.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.HorizontalMenuShadowDom.Example.tsx new file mode 100644 index 00000000000000..bd9b8f5b2b5bab --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.HorizontalMenuShadowDom.Example.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusZone, FocusZoneDirection } from '@fluentui/react-focus'; + +export const ShadowDOMFocusZoneHorizontalMenuShadowDomExample: React.FunctionComponent = () => { + const buttonsConstructed = React.useRef(false); + + const setRef = (node: HTMLElement | null) => { + if (node && buttonsConstructed.current === false) { + const btns = Array.from(node.querySelectorAll('.demo-button')); + + btns.forEach((btn, i) => { + // Ideally this would be a custom element but because of the way + // Stories are transpiled, classes are converted to ES5 which breaks + // custom elements (they require ES6 classes). + // This approach gives us a shadow root to demonstrate the feature. + const shadowRoot = btn.attachShadow({ mode: 'open', delegatesFocus: true }); + + shadowRoot.innerHTML = ``; + }); + + buttonsConstructed.current = true; + } + }; + + return ( + + +
+
+
+
+ + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.List.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.List.Example.tsx new file mode 100644 index 00000000000000..5a5463608ec2be --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.List.Example.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusZoneListExample } from '../../react-focus/FocusZone/FocusZone.List.Example'; + +export const ShadowDOMFocusZoneListExample: React.FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.Photos.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.Photos.Example.tsx new file mode 100644 index 00000000000000..08a0647b04e9ab --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.Photos.Example.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusZonePhotosExample } from '../../react-focus/FocusZone/FocusZone.Photos.Example'; + +export const ShadowDOMFocusZonePhotosExample: React.FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.Tabbable.Example.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.Tabbable.Example.tsx new file mode 100644 index 00000000000000..f1ddacd6f9dfda --- /dev/null +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.Tabbable.Example.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { Shadow } from './ShadowHelper'; +import { FocusZoneTabbableExample } from '../../react-focus/FocusZone/FocusZone.Tabbable.Example'; + +export const ShadowDOMFocusZoneTabbableExample: React.FunctionComponent = () => { + return ( + + + + ); +}; diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.doc.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.doc.tsx index f3aef77f1987bb..a61025548b2042 100644 --- a/packages/react-examples/src/react/ShadowDOM/ShadowDOM.doc.tsx +++ b/packages/react-examples/src/react/ShadowDOM/ShadowDOM.doc.tsx @@ -20,6 +20,19 @@ import { ShadowDOMDropdownExample } from './ShadowDOM.Dropdown.Example'; import { ShadowDOMExtendedPeoplePickerExample } from './ShadowDOM.ExtendedPeoplePicker.Example'; import { ShadowDOMFacepileExample } from './ShadowDOM.Facepile.Example'; import { ShadowDOMFloatingPeoplePickerExample } from './ShadowDOM.FloatingPeoplePicker.Example'; +import { ShadowDOMFocusTrapZoneBoxExample } from './ShadowDOM.FocusTrapZone.Box.Example'; +import { ShadowDOMFocusTrapZoneBoxClickExample } from './ShadowDOM.FocusTrapZone.BoxClick.Example'; +import { ShadowDOMFocusTrapZoneDialogInPanelExample } from './ShadowDOM.FocusTrapZone.DialogInPanel.Example'; +import { ShadowDOMFocusTrapZoneBoxCustomElementExample } from './ShadowDOM.FocusTrapZone.FocusOnCustomElement.Example'; +import { ShadowDOMFocusTrapZoneFocusZoneExample } from './ShadowDOM.FocusTrapZone.FocusZone.Example'; +import { ShadowDOMFocusTrapZoneNestedExample } from './ShadowDOM.FocusTrapZone.Nested.Example'; +import { ShadowDOMFocusTrapZoneChildShadowDomExample } from './ShadowDOM.FocusTrapZone.ChildShadowDom.Example'; +import { ShadowDOMFocusTrapZoneNestedChildShadowDomExample } from './ShadowDOM.FocusTrapZone.NestedChildShadowDom.Example'; +import { ShadowDOMFocusZoneDisabledExample } from './ShadowDOM.FocusZone.Disabled.Example'; +import { ShadowDOMFocusZoneHorizontalMenuExample } from './ShadowDOM.FocusZone.HorizontalMenu.Example'; +import { ShadowDOMFocusZoneListExample } from './ShadowDOM.FocusZone.List.Example'; +import { ShadowDOMFocusZonePhotosExample } from './ShadowDOM.FocusZone.Photos.Example'; +import { ShadowDOMFocusZoneTabbableExample } from './ShadowDOM.FocusZone.Tabbable.Example'; import { ShadowDOMGroupedListExample } from './ShadowDOM.GroupedList.Example'; import { ShadowDOMHoverCardExample } from './ShadowDOM.HoverCard.Example'; import { ShadowDOMIconExample } from './ShadowDOM.Icon.Example'; @@ -135,6 +148,45 @@ const ShadowDOMFacepileExampleCode = const ShadowDOMFloatingPeoplePickerExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FloatingPeoplePicker.Example.tsx') as string; +const ShadowDOMFocusTrapZoneBoxExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.Box.Example.tsx') as string; + +const ShadowDOMFocusTrapZoneBoxClickExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.BoxClick.Example.tsx') as string; + +const ShadowDOMFocusTrapZoneDialogInPanelExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.DialogInPanel.Example.tsx') as string; + +const ShadowDOMFocusTrapZoneBoxCustomElementExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.FocusOnCustomElement.Example.tsx') as string; + +const ShadowDOMFocusTrapZoneFocusZoneExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.FocusZone.Example.tsx') as string; + +const ShadowDOMFocusTrapZoneNestedExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.Nested.Example.tsx') as string; + +const ShadowDOMFocusTrapZoneChildShadowDomExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.ChildShadowDom.Example.tsx') as string; + +const ShadowDOMFocusTrapZoneNestedChildShadowDomExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusTrapZone.NestedChildShadowDom.Example.tsx') as string; + +const ShadowDOMFocusZoneDisabledExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.Disabled.Example.tsx') as string; + +const ShadowDOMFocusZoneHorizontalMenuExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.HorizontalMenu.Example.tsx') as string; + +const ShadowDOMFocusZoneListExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.List.Example.tsx') as string; + +const ShadowDOMFocusZonePhotosExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.Photos.Example.tsx') as string; + +const ShadowDOMFocusZoneTabbableExampleCode = + require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.FocusZone.Tabbable.Example.tsx') as string; + const ShadowDOMGroupedListExampleCode = require('!raw-loader?esModule=false!@fluentui/react-examples/src/react/ShadowDOM/ShadowDOM.GroupedList.Example.tsx') as string; @@ -399,6 +451,71 @@ export const ShadowDOMPageProps = (): IDocPageProps => ({ code: ShadowDOMFloatingPeoplePickerExampleCode, view: , }, + { + title: 'FocusTrapZoneBox', + code: ShadowDOMFocusTrapZoneBoxExampleCode, + view: , + }, + { + title: 'FocusTrapZoneBoxClick', + code: ShadowDOMFocusTrapZoneBoxClickExampleCode, + view: , + }, + { + title: 'FocusTrapZoneDialogInPanel', + code: ShadowDOMFocusTrapZoneDialogInPanelExampleCode, + view: , + }, + { + title: 'FocusTrapZoneFocusOnCustomElement', + code: ShadowDOMFocusTrapZoneBoxCustomElementExampleCode, + view: , + }, + { + title: 'FocusTrapZoneFocusZone', + code: ShadowDOMFocusTrapZoneFocusZoneExampleCode, + view: , + }, + { + title: 'FocusTrapZoneNested', + code: ShadowDOMFocusTrapZoneNestedExampleCode, + view: , + }, + { + title: 'FocusTrapZoneChildShadowDom', + code: ShadowDOMFocusTrapZoneChildShadowDomExampleCode, + view: , + }, + { + title: 'FocusTrapZoneNestedChildShadowDom', + code: ShadowDOMFocusTrapZoneNestedChildShadowDomExampleCode, + view: , + }, + { + title: 'FocusZoneDisabled', + code: ShadowDOMFocusZoneDisabledExampleCode, + view: , + }, + { + title: 'FocusZoneHorizontalMenu', + code: ShadowDOMFocusZoneHorizontalMenuExampleCode, + view: , + }, + { + title: 'FocusZoneList', + code: ShadowDOMFocusZoneListExampleCode, + view: , + }, + { + title: 'FocusZonePhotos', + code: ShadowDOMFocusZonePhotosExampleCode, + view: , + }, + { + title: 'FocusZoneTabbable', + code: ShadowDOMFocusZoneTabbableExampleCode, + view: , + }, { title: 'GroupedList', code: ShadowDOMGroupedListExampleCode, diff --git a/packages/react-examples/src/react/ShadowDOM/ShadowHelper.tsx b/packages/react-examples/src/react/ShadowDOM/ShadowHelper.tsx index 5a02ae5818804c..e47cf461569d56 100644 --- a/packages/react-examples/src/react/ShadowDOM/ShadowHelper.tsx +++ b/packages/react-examples/src/react/ShadowDOM/ShadowHelper.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { MergeStylesRootProvider, MergeStylesShadowRootProvider } from '@fluentui/react'; +import { FocusRectsProvider, MergeStylesRootProvider, MergeStylesShadowRootProvider } from '@fluentui/react'; import root from 'react-shadow'; export type ShadowProps = { @@ -21,9 +21,13 @@ export const Shadow: React.FC = ({ window, children }) => { return ( - - {children} - + + + + {children} + + + ); }; diff --git a/packages/react-focus/etc/react-focus.api.md b/packages/react-focus/etc/react-focus.api.md index bb83efb7bd7a68..420c1dc237d940 100644 --- a/packages/react-focus/etc/react-focus.api.md +++ b/packages/react-focus/etc/react-focus.api.md @@ -5,6 +5,7 @@ ```ts import type { IRefObject } from '@fluentui/utilities'; +import { MergeStylesShadowRootContextValue } from '@fluentui/utilities/lib/shadowDom/MergeStylesShadowRootContext'; import type { Point } from '@fluentui/utilities'; import * as React_2 from 'react'; @@ -20,6 +21,8 @@ export class FocusZone extends React_2.Component implements IFo // (undocumented) componentWillUnmount(): void; // (undocumented) + static contextType: React_2.Context; + // (undocumented) get defaultFocusElement(): HTMLElement | null; // (undocumented) static defaultProps: IFocusZoneProps; diff --git a/packages/react-focus/src/components/FocusZone/FocusZone.tsx b/packages/react-focus/src/components/FocusZone/FocusZone.tsx index a8ae33009abcc4..77597c7c73c3ea 100644 --- a/packages/react-focus/src/components/FocusZone/FocusZone.tsx +++ b/packages/react-focus/src/components/FocusZone/FocusZone.tsx @@ -24,6 +24,7 @@ import { findScrollableParent, createMergedRef, isElementVisibleAndNotHidden, + MergeStylesShadowRootContext, } from '@fluentui/utilities'; import { mergeStyles } from '@fluentui/merge-styles'; import { getTheme } from '@fluentui/style-utilities'; @@ -110,6 +111,8 @@ const ALLOWED_INPUT_TYPES = ['text', 'number', 'password', 'email', 'tel', 'url' const ALLOW_VIRTUAL_ELEMENTS = false; export class FocusZone extends React.Component implements IFocusZone { + public static contextType = MergeStylesShadowRootContext; + public static defaultProps: IFocusZoneProps = { isCircularNavigation: false, direction: FocusZoneDirection.bidirectional, @@ -148,6 +151,8 @@ export class FocusZone extends React.Component implements IFocu private _shouldRaiseClicksOnEnter: boolean; private _shouldRaiseClicksOnSpace: boolean; + private _inShadowRoot: boolean; + /** Used for testing purposes only. */ public static getOuterZones(): number { return _outerZones.size; @@ -197,6 +202,8 @@ export class FocusZone extends React.Component implements IFocu public componentDidMount(): void { const { current: root } = this._root; + this._inShadowRoot = !!this.context?.shadowRoot; + _allInstances[this._id] = this; if (root) { @@ -238,6 +245,7 @@ export class FocusZone extends React.Component implements IFocu public componentDidUpdate(): void { const { current: root } = this._root; const doc = this._getDocument(); + this._inShadowRoot = !!this.context?.shadowRoot; // If either _activeElement or _defaultFocusElement are no longer contained by _root, // reset those variables (and update tab indexes) to avoid memory leaks @@ -362,7 +370,7 @@ export class FocusZone extends React.Component implements IFocu !forceIntoFirstElement && this._activeElement && elementContains(this._root.current, this._activeElement) && - isElementTabbable(this._activeElement) && + isElementTabbable(this._activeElement, undefined, this._inShadowRoot) && (!bypassHiddenElements || isElementVisibleAndNotHidden(this._activeElement)) ) { this._activeElement.focus(); @@ -489,7 +497,10 @@ export class FocusZone extends React.Component implements IFocu let parentElement = ev.target as HTMLElement; while (parentElement && parentElement !== this._root.current) { - if (isElementTabbable(parentElement) && this._isImmediateDescendantOfZone(parentElement)) { + if ( + isElementTabbable(parentElement, undefined, this._inShadowRoot) && + this._isImmediateDescendantOfZone(parentElement) + ) { newActiveElement = parentElement; break; } @@ -506,7 +517,7 @@ export class FocusZone extends React.Component implements IFocu defaultTabbableElement(this._root.current); // try to focus defaultTabbable element - if (maybeElementToFocus && isElementTabbable(maybeElementToFocus)) { + if (maybeElementToFocus && isElementTabbable(maybeElementToFocus, undefined, this._inShadowRoot)) { newActiveElement = maybeElementToFocus; maybeElementToFocus.focus(); } else { @@ -606,7 +617,7 @@ export class FocusZone extends React.Component implements IFocu while (path.length) { target = path.pop() as HTMLElement; - if (target && isElementTabbable(target)) { + if (target && isElementTabbable(target, undefined, this._inShadowRoot)) { this._setActiveElement(target, true); } @@ -1330,7 +1341,7 @@ export class FocusZone extends React.Component implements IFocu // If active element changes state to disabled, set it to null. // Otherwise, we lose keyboard accessibility to other elements in focus zone. - if (this._activeElement && !isElementTabbable(this._activeElement)) { + if (this._activeElement && !isElementTabbable(this._activeElement, undefined, this._inShadowRoot)) { this._activeElement = null; } @@ -1345,7 +1356,7 @@ export class FocusZone extends React.Component implements IFocu child.setAttribute(TABINDEX, '-1'); } - if (isElementTabbable(child)) { + if (isElementTabbable(child, undefined, this._inShadowRoot)) { if (this.props.disabled) { child.setAttribute(TABINDEX, '-1'); } else if ( diff --git a/packages/react/src/Utilities.ts b/packages/react/src/Utilities.ts index 14d5396471fe77..b6b50f744c7dda 100644 --- a/packages/react/src/Utilities.ts +++ b/packages/react/src/Utilities.ts @@ -68,10 +68,12 @@ export { focusFirstChild, formProperties, format, + getActiveElement, getChildren, getDistanceBetweenPoints, getDocument, getElementIndexPath, + getEventTarget, getFirstFocusable, getFirstTabbable, getFirstVisibleElementFromSelector, diff --git a/packages/react/src/components/FocusTrapZone/FocusTrapZone.tsx b/packages/react/src/components/FocusTrapZone/FocusTrapZone.tsx index b4b6c6e9a75ef6..1b406e624121e9 100644 --- a/packages/react/src/components/FocusTrapZone/FocusTrapZone.tsx +++ b/packages/react/src/components/FocusTrapZone/FocusTrapZone.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; import { + getActiveElement, + getEventTarget, elementContains, getNativeProps, divProperties, @@ -10,6 +12,7 @@ import { getPropsWithDefaults, modalize, on, + useHasMergeStylesShadowRootContext, } from '../../Utilities'; import { useId, useConst, useMergedRefs, useEventCallback, usePrevious, useUnmount } from '@fluentui/react-hooks'; import { useDocument } from '../../WindowProvider'; @@ -62,6 +65,7 @@ export const FocusTrapZone: React.FunctionComponent & { const lastBumper = React.useRef(null); const mergedRootRef = useMergedRefs(root, ref) as React.Ref; const doc = useDocument(); + const inShadow = useHasMergeStylesShadowRootContext(); const isFirstRender = usePrevious(false) ?? true; @@ -147,6 +151,10 @@ export const FocusTrapZone: React.FunctionComponent & { false, false, true, + undefined, + undefined, + undefined, + inShadow, ); } @@ -163,8 +171,8 @@ export const FocusTrapZone: React.FunctionComponent & { const nextFocusable = isFirstBumper === internalState.hasFocus - ? getLastTabbable(root.current, lastBumper.current!, true, false) - : getFirstTabbable(root.current, firstBumper.current!, true, false); + ? getLastTabbable(root.current, lastBumper.current!, true, false, inShadow) + : getFirstTabbable(root.current, firstBumper.current!, true, false, inShadow); if (nextFocusable) { if (nextFocusable === firstBumper.current || nextFocusable === lastBumper.current) { @@ -187,7 +195,7 @@ export const FocusTrapZone: React.FunctionComponent & { // even when it's not. Using document.activeElement is another way // for us to be able to get what the relatedTarget without relying // on the event - relatedTarget = doc!.activeElement as Element; + relatedTarget = getActiveElement(doc!) as Element; } if (!elementContains(root.current, relatedTarget as HTMLElement)) { internalState.hasFocus = false; @@ -209,7 +217,7 @@ export const FocusTrapZone: React.FunctionComponent & { if (ev.target !== ev.currentTarget && !(ev.target === firstBumper.current || ev.target === lastBumper.current)) { // every time focus changes within the trap zone, remember the focused element so that // it can be restored if focus leaves the pane and returns via keystroke (i.e. via a call to this.focus(true)) - internalState.previouslyFocusedElementInTrapZone = ev.target as HTMLElement; + internalState.previouslyFocusedElementInTrapZone = getEventTarget(ev.nativeEvent) as HTMLElement; } }; @@ -221,12 +229,16 @@ export const FocusTrapZone: React.FunctionComponent & { return; } + // Do not use getActiveElement() here. + // When the FTZ is in shadow DOM focus returns to the + // shadow host rather than body so we need to be + // able to inspect that const activeElement = doc.activeElement as HTMLElement; if ( !disableRestoreFocus && typeof elementToFocusOnDismiss?.focus === 'function' && // only restore focus if the current focused element is within the FTZ, or if nothing is focused - (elementContains(root.current, activeElement) || activeElement === doc.body) + (elementContains(root.current, activeElement) || activeElement === doc.body || activeElement.shadowRoot) ) { focusElementAsync(elementToFocusOnDismiss); } @@ -239,11 +251,11 @@ export const FocusTrapZone: React.FunctionComponent & { return; } if (internalState.focusStackId === FocusTrapZone.focusStack!.slice(-1)[0]) { - const targetElement = ev.target as HTMLElement | null; + const targetElement = getEventTarget(ev); if (targetElement && !elementContains(root.current, targetElement)) { - if (doc && doc.activeElement === doc.body) { + if (doc && getActiveElement(doc) === doc.body) { setTimeout(() => { - if (doc && doc.activeElement === doc.body) { + if (doc && getActiveElement(doc) === doc.body) { focusFTZ(); internalState.hasFocus = true; // set focus here since we stop event propagation } @@ -287,7 +299,7 @@ export const FocusTrapZone: React.FunctionComponent & { // Transition from forceFocusInsideTrap / FTZ disabled to enabled (or initial mount) FocusTrapZone.focusStack!.push(internalState.focusStackId); - const elementToFocusOnDismiss = props.elementToFocusOnDismiss || (doc!.activeElement as HTMLElement | null); + const elementToFocusOnDismiss = props.elementToFocusOnDismiss || (getActiveElement(doc!) as HTMLElement | null); if (!disableFirstFocus && !elementContains(root.current, elementToFocusOnDismiss)) { focusFTZ(); diff --git a/packages/utilities/etc/utilities.api.md b/packages/utilities/etc/utilities.api.md index 88b77c71fba5c9..6baf063273d003 100644 --- a/packages/utilities/etc/utilities.api.md +++ b/packages/utilities/etc/utilities.api.md @@ -9,7 +9,9 @@ import { elementContains } from '@fluentui/dom-utilities'; import { elementContainsAttribute } from '@fluentui/dom-utilities'; import type { ExtendedCSSStyleSheet } from '@fluentui/merge-styles'; import { findElementRecursive } from '@fluentui/dom-utilities'; +import { getActiveElement } from '@fluentui/dom-utilities'; import { getChildren } from '@fluentui/dom-utilities'; +import { getEventTarget } from '@fluentui/dom-utilities'; import { getParent } from '@fluentui/dom-utilities'; import { getVirtualParent } from '@fluentui/dom-utilities'; import type { IProcessedStyleSet } from '@fluentui/merge-styles'; @@ -296,7 +298,7 @@ export function focusAsync(element: HTMLElement | { } | undefined | null): void; // @public -export function focusFirstChild(rootElement: HTMLElement, bypassHiddenElements?: boolean): boolean; +export function focusFirstChild(rootElement: HTMLElement, bypassHiddenElements?: boolean, includeShadowRoots?: boolean): boolean; // @public export const FocusRects: React_2.FunctionComponent<{ @@ -321,6 +323,8 @@ export function format(s: string, ...values: any[]): string; // @public export const formProperties: Record; +export { getActiveElement } + export { getChildren } // @public @@ -332,11 +336,13 @@ export function getDocument(rootElement?: HTMLElement | null): Document | undefi // @public export function getElementIndexPath(fromElement: HTMLElement, toElement: HTMLElement): number[]; +export { getEventTarget } + // @public -export function getFirstFocusable(rootElement: HTMLElement, currentElement: HTMLElement, includeElementsInFocusZones?: boolean): HTMLElement | null; +export function getFirstFocusable(rootElement: HTMLElement, currentElement: HTMLElement, includeElementsInFocusZones?: boolean, includeShadowRoots?: boolean): HTMLElement | null; // @public -export function getFirstTabbable(rootElement: HTMLElement, currentElement: HTMLElement, includeElementsInFocusZones?: boolean, checkNode?: boolean): HTMLElement | null; +export function getFirstTabbable(rootElement: HTMLElement, currentElement: HTMLElement, includeElementsInFocusZones?: boolean, checkNode?: boolean, includeShadowRoots?: boolean): HTMLElement | null; // @public export function getFirstVisibleElementFromSelector(selector: string): Element | undefined; @@ -354,10 +360,10 @@ export function getInitials(displayName: string | undefined | null, isRtl: boole export function getLanguage(persistenceType?: 'localStorage' | 'sessionStorage' | 'none'): string | null; // @public -export function getLastFocusable(rootElement: HTMLElement, currentElement: HTMLElement, includeElementsInFocusZones?: boolean): HTMLElement | null; +export function getLastFocusable(rootElement: HTMLElement, currentElement: HTMLElement, includeElementsInFocusZones?: boolean, includeShadowRoots?: boolean): HTMLElement | null; // @public -export function getLastTabbable(rootElement: HTMLElement, currentElement: HTMLElement, includeElementsInFocusZones?: boolean, checkNode?: boolean): HTMLElement | null; +export function getLastTabbable(rootElement: HTMLElement, currentElement: HTMLElement, includeElementsInFocusZones?: boolean, checkNode?: boolean, includeShadowRoots?: boolean): HTMLElement | null; // @public export function getNativeElementProps>(tagName: string, props: {}, excludedPropNames?: string[]): TAttributes; @@ -366,12 +372,12 @@ export function getNativeElementProps>(props: Record, allowedPropNames: string[] | Record, excludedPropNames?: string[]): T; // @public -export function getNextElement(rootElement: HTMLElement, currentElement: HTMLElement | null, checkNode?: boolean, suppressParentTraversal?: boolean, suppressChildTraversal?: boolean, includeElementsInFocusZones?: boolean, allowFocusRoot?: boolean, tabbable?: boolean, bypassHiddenElements?: boolean): HTMLElement | null; +export function getNextElement(rootElement: HTMLElement, currentElement: HTMLElement | null, checkNode?: boolean, suppressParentTraversal?: boolean, suppressChildTraversal?: boolean, includeElementsInFocusZones?: boolean, allowFocusRoot?: boolean, tabbable?: boolean, bypassHiddenElements?: boolean, includeShadowRoots?: boolean): HTMLElement | null; export { getParent } // @public -export function getPreviousElement(rootElement: HTMLElement, currentElement: HTMLElement | null, checkNode?: boolean, suppressParentTraversal?: boolean, traverseChildren?: boolean, includeElementsInFocusZones?: boolean, allowFocusRoot?: boolean, tabbable?: boolean): HTMLElement | null; +export function getPreviousElement(rootElement: HTMLElement, currentElement: HTMLElement | null, checkNode?: boolean, suppressParentTraversal?: boolean, traverseChildren?: boolean, includeElementsInFocusZones?: boolean, allowFocusRoot?: boolean, tabbable?: boolean, includeShadowRoots?: boolean): HTMLElement | null; // @public export function getPropsWithDefaults(defaultProps: Partial, propsWithoutDefaults: TProps): TProps; @@ -807,7 +813,7 @@ export function isElementFocusSubZone(element?: HTMLElement): boolean; export function isElementFocusZone(element?: HTMLElement): boolean; // @public -export function isElementTabbable(element: HTMLElement, checkTabIndex?: boolean): boolean; +export function isElementTabbable(element: HTMLElement, checkTabIndex?: boolean, checkShadowRoot?: boolean): boolean; // @public export function isElementVisible(element: HTMLElement | undefined | null): boolean; @@ -1021,6 +1027,11 @@ export function mergeSettings(oldSettings?: ISettings, newSettings?: ISettings | // @public export const MergeStylesRootProvider: React_2.FC; +// Warning: (ae-forgotten-export) The symbol "MergeStylesShadowRootContextValue" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export const MergeStylesShadowRootContext: React_2.Context; + // Warning: (ae-forgotten-export) The symbol "MergeStylesShadowRootProviderProps" needs to be exported by the entry point index.d.ts // // @public @@ -1294,8 +1305,6 @@ export const useIsomorphicLayoutEffect: typeof React_2.useEffect; // @public export const useMergeStylesRootStylesheets: () => Map; -// Warning: (ae-forgotten-export) The symbol "MergeStylesShadowRootContextValue" needs to be exported by the entry point index.d.ts -// // @public export const useMergeStylesShadowRootContext: () => MergeStylesShadowRootContextValue | undefined; diff --git a/packages/utilities/src/dom.ts b/packages/utilities/src/dom.ts index a1ceab1d134723..8738039b4d6ff5 100644 --- a/packages/utilities/src/dom.ts +++ b/packages/utilities/src/dom.ts @@ -2,8 +2,10 @@ export type { IVirtualElement } from './dom/IVirtualElement'; export { elementContains } from './dom/elementContains'; export { elementContainsAttribute } from './dom/elementContainsAttribute'; export { findElementRecursive } from './dom/findElementRecursive'; +export { getActiveElement } from './dom/getActiveElement'; export { getChildren } from './dom/getChildren'; export { getDocument } from './dom/getDocument'; +export { getEventTarget } from './dom/getEventTarget'; export { getFirstVisibleElementFromSelector } from './dom/getFirstVisibleElementFromSelector'; export { getParent } from './dom/getParent'; export { getRect } from './dom/getRect'; diff --git a/packages/utilities/src/dom/getActiveElement.ts b/packages/utilities/src/dom/getActiveElement.ts new file mode 100644 index 00000000000000..2d5cba107205f2 --- /dev/null +++ b/packages/utilities/src/dom/getActiveElement.ts @@ -0,0 +1 @@ +export { getActiveElement } from '@fluentui/dom-utilities'; diff --git a/packages/utilities/src/dom/getEventTarget.ts b/packages/utilities/src/dom/getEventTarget.ts new file mode 100644 index 00000000000000..d08503065355ce --- /dev/null +++ b/packages/utilities/src/dom/getEventTarget.ts @@ -0,0 +1 @@ +export { getEventTarget } from '@fluentui/dom-utilities'; diff --git a/packages/utilities/src/focus.test.tsx b/packages/utilities/src/focus.test.tsx index 3c20ef0f3fa286..1084976d414c9a 100644 --- a/packages/utilities/src/focus.test.tsx +++ b/packages/utilities/src/focus.test.tsx @@ -19,6 +19,35 @@ function renderIntoDocument(element: React.ReactElement<{}>, container: HTMLElem return renderedDOM; } +// JSDOM does not currently set `delegatesFocus` +// https://github.com/jsdom/jsdom/blob/b7683ed68ebe259cd2c68e5faf12d484a785f45f/lib/jsdom/living/nodes/Element-impl.js#L420-L424 +function createDivWithShadowRoot(initOptions: ShadowRootInit): HTMLElement { + const div = { + getAttribute: (qualifiedName: string): string | null => null, + shadowRoot: { + mode: initOptions.mode, + delegatesFocus: initOptions.delegatesFocus, + }, + }; + + return div as HTMLElement; +} + +function makeShadowDiv(innerHTML: string): React.FC { + const ShadowDiv = () => { + const setRef = (node: HTMLElement | null) => { + if (node) { + node.attachShadow({ mode: 'open' }); + node.shadowRoot!.innerHTML = innerHTML; + } + }; + + return
; + }; + + return ShadowDiv; +} + describe('isElementVisible', () => { let testContainer: HTMLElement | undefined; @@ -153,6 +182,25 @@ describe('isElementTabbable', () => { expect(isElementTabbable(button, true)).toEqual(false); }); + + it('returns true for elements with shadowRoot.delegatesFocus=true', () => { + const div = createDivWithShadowRoot({ mode: 'open', delegatesFocus: true }); + + expect(div.shadowRoot?.delegatesFocus).toEqual(true); + expect(isElementTabbable(div)).toEqual(true); + }); + + it('returns true for elements with shadowRoot.delegatesFocus=false', () => { + const div = createDivWithShadowRoot({ mode: 'open' }); + + expect(isElementTabbable(div)).toEqual(false); + }); + + it('returns true for elements with shadowRoot.delegatesFocus=true when set to ignore shadow roots', () => { + const div = createDivWithShadowRoot({ mode: 'open', delegatesFocus: true }); + + expect(isElementTabbable(div, undefined, false)).toEqual(false); + }); }); describe('focusAsync', () => { @@ -339,6 +387,36 @@ describe('getFirstTabbable', () => { expect(getFirstTabbable(parent, buttonA, true, false)).toEqual(buttonB); }); + it('focuses on the next tabbable item in shadow DOM', () => { + testContainer = createTestContainer(); + + const innerHTML = ` + + + + `; + const ShadowDiv = makeShadowDiv(innerHTML); + + const container = renderIntoDocument( +
+ +
, + testContainer, + ); + + const parent = container.querySelector('.parent') as HTMLElement; + const buttonA = parent?.shadowRoot?.querySelector('.a') as HTMLElement; + const buttonB = parent?.shadowRoot?.querySelector('.b') as HTMLElement; + + expect(getFirstTabbable(parent, buttonA, true, false, true)).toEqual(buttonB); + }); + it('does not focus on an item with tabIndex of -1', () => { testContainer = createTestContainer(); const container = renderIntoDocument( @@ -401,6 +479,36 @@ describe('getLastTabbable', () => { expect(getLastTabbable(parent, buttonC, true, false)).toEqual(buttonB); }); + it('focuses on the last tabbable item in shadow DOM', () => { + testContainer = createTestContainer(); + + const innerHTML = ` + + + + `; + const ShadowDiv = makeShadowDiv(innerHTML); + + const container = renderIntoDocument( +
+ +
, + testContainer, + ); + + const parent = container.querySelector('.parent') as HTMLElement; + const buttonB = parent?.shadowRoot?.querySelector('.b') as HTMLElement; + const buttonC = parent?.shadowRoot?.querySelector('.c') as HTMLElement; + + expect(getLastTabbable(parent, buttonC, true, false, true)).toEqual(buttonB); + }); + it('does not focus on an item with tabIndex of -1', () => { testContainer = createTestContainer(); const container = renderIntoDocument( diff --git a/packages/utilities/src/focus.ts b/packages/utilities/src/focus.ts index e3ab748badb23b..62d053b862091d 100644 --- a/packages/utilities/src/focus.ts +++ b/packages/utilities/src/focus.ts @@ -18,6 +18,7 @@ export function getFirstFocusable( rootElement: HTMLElement, currentElement: HTMLElement, includeElementsInFocusZones?: boolean, + includeShadowRoots?: boolean, ): HTMLElement | null { return getNextElement( rootElement, @@ -26,6 +27,10 @@ export function getFirstFocusable( false /*suppressParentTraversal*/, false /*suppressChildTraversal*/, includeElementsInFocusZones, + undefined, + undefined, + undefined, + includeShadowRoots, ); } @@ -38,6 +43,7 @@ export function getLastFocusable( rootElement: HTMLElement, currentElement: HTMLElement, includeElementsInFocusZones?: boolean, + includeShadowRoots?: boolean, ): HTMLElement | null { return getPreviousElement( rootElement, @@ -46,6 +52,9 @@ export function getLastFocusable( false /*suppressParentTraversal*/, true /*traverseChildren*/, includeElementsInFocusZones, + undefined, + undefined, + includeShadowRoots, ); } @@ -64,6 +73,7 @@ export function getFirstTabbable( currentElement: HTMLElement, includeElementsInFocusZones?: boolean, checkNode: boolean = true, + includeShadowRoots?: boolean, ): HTMLElement | null { return getNextElement( rootElement, @@ -74,6 +84,8 @@ export function getFirstTabbable( includeElementsInFocusZones, false /*allowFocusRoot*/, true /*tabbable*/, + undefined, + includeShadowRoots, ); } @@ -92,6 +104,7 @@ export function getLastTabbable( currentElement: HTMLElement, includeElementsInFocusZones?: boolean, checkNode: boolean = true, + includeShadowRoots?: boolean, ): HTMLElement | null { return getPreviousElement( rootElement, @@ -102,6 +115,7 @@ export function getLastTabbable( includeElementsInFocusZones, false /*allowFocusRoot*/, true /*tabbable*/, + includeShadowRoots, ); } @@ -113,7 +127,11 @@ export function getLastTabbable( * @param bypassHiddenElements - If true, focus will be not be set on hidden elements. * @returns True if focus was set, false if it was not. */ -export function focusFirstChild(rootElement: HTMLElement, bypassHiddenElements?: boolean): boolean { +export function focusFirstChild( + rootElement: HTMLElement, + bypassHiddenElements?: boolean, + includeShadowRoots?: boolean, +): boolean { let element: HTMLElement | null = getNextElement( rootElement, rootElement, @@ -124,6 +142,7 @@ export function focusFirstChild(rootElement: HTMLElement, bypassHiddenElements?: undefined, undefined, bypassHiddenElements, + includeShadowRoots, ); if (element) { @@ -148,6 +167,7 @@ export function getPreviousElement( includeElementsInFocusZones?: boolean, allowFocusRoot?: boolean, tabbable?: boolean, + includeShadowRoots?: boolean, ): HTMLElement | null { if (!currentElement || (!allowFocusRoot && currentElement === rootElement)) { return null; @@ -161,19 +181,23 @@ export function getPreviousElement( isCurrentElementVisible && (includeElementsInFocusZones || !(isElementFocusZone(currentElement) || isElementFocusSubZone(currentElement))) ) { + const lastElementChild = (currentElement.lastElementChild || + (includeShadowRoots && currentElement.shadowRoot?.lastElementChild)) as HTMLElement; + const childMatch = getPreviousElement( rootElement, - currentElement.lastElementChild as HTMLElement, + lastElementChild, true, true, true, includeElementsInFocusZones, allowFocusRoot, tabbable, + includeShadowRoots, ); if (childMatch) { - if ((tabbable && isElementTabbable(childMatch, true)) || !tabbable) { + if ((tabbable && isElementTabbable(childMatch, true, includeShadowRoots)) || !tabbable) { return childMatch; } @@ -186,6 +210,7 @@ export function getPreviousElement( includeElementsInFocusZones, allowFocusRoot, tabbable, + includeShadowRoots, ); if (childMatchSiblingMatch) { return childMatchSiblingMatch; @@ -207,6 +232,7 @@ export function getPreviousElement( includeElementsInFocusZones, allowFocusRoot, tabbable, + includeShadowRoots, ); if (childMatchParentMatch) { @@ -219,7 +245,7 @@ export function getPreviousElement( } // Check the current node, if it's not the first traversal. - if (checkNode && isCurrentElementVisible && isElementTabbable(currentElement, tabbable)) { + if (checkNode && isCurrentElementVisible && isElementTabbable(currentElement, tabbable, includeShadowRoots)) { return currentElement; } @@ -233,6 +259,7 @@ export function getPreviousElement( includeElementsInFocusZones, allowFocusRoot, tabbable, + includeShadowRoots, ); if (siblingMatch) { @@ -250,6 +277,7 @@ export function getPreviousElement( includeElementsInFocusZones, allowFocusRoot, tabbable, + includeShadowRoots, ); } @@ -273,6 +301,7 @@ export function getNextElement( allowFocusRoot?: boolean, tabbable?: boolean, bypassHiddenElements?: boolean, + includeShadowRoots?: boolean, ): HTMLElement | null { if (!currentElement || (currentElement === rootElement && suppressChildTraversal && !allowFocusRoot)) { return null; @@ -283,7 +312,7 @@ export function getNextElement( let isCurrentElementVisible = checkElementVisibility(currentElement); // Check the current node, if it's not the first traversal. - if (checkNode && isCurrentElementVisible && isElementTabbable(currentElement, tabbable)) { + if (checkNode && isCurrentElementVisible && isElementTabbable(currentElement, tabbable, includeShadowRoots)) { return currentElement; } @@ -293,9 +322,12 @@ export function getNextElement( isCurrentElementVisible && (includeElementsInFocusZones || !(isElementFocusZone(currentElement) || isElementFocusSubZone(currentElement))) ) { + const firstElementchild = (currentElement.firstElementChild || + (includeShadowRoots && currentElement.shadowRoot?.firstElementChild)) as HTMLElement; + const childMatch = getNextElement( rootElement, - currentElement.firstElementChild as HTMLElement, + firstElementchild, true, true, false, @@ -303,6 +335,7 @@ export function getNextElement( allowFocusRoot, tabbable, bypassHiddenElements, + includeShadowRoots, ); if (childMatch) { @@ -325,6 +358,7 @@ export function getNextElement( allowFocusRoot, tabbable, bypassHiddenElements, + includeShadowRoots, ); if (siblingMatch) { @@ -342,6 +376,7 @@ export function getNextElement( allowFocusRoot, tabbable, bypassHiddenElements, + includeShadowRoots, ); } @@ -398,7 +433,11 @@ export function isElementVisibleAndNotHidden(element: HTMLElement | undefined | * * @public */ -export function isElementTabbable(element: HTMLElement, checkTabIndex?: boolean): boolean { +export function isElementTabbable( + element: HTMLElement, + checkTabIndex?: boolean, + checkShadowRoot: boolean = true, +): boolean { // If this element is null or is disabled, it is not considered tabbable. if (!element || (element as HTMLButtonElement).disabled) { return false; @@ -417,6 +456,7 @@ export function isElementTabbable(element: HTMLElement, checkTabIndex?: boolean) let isFocusableAttribute = element.getAttribute ? element.getAttribute(IS_FOCUSABLE_ATTRIBUTE) : null; let isTabIndexSet = tabIndexAttributeValue !== null && tabIndex >= 0; + let delegatesFocus = checkShadowRoot && element.shadowRoot ? !!element.shadowRoot.delegatesFocus : false; const result = !!element && @@ -427,7 +467,8 @@ export function isElementTabbable(element: HTMLElement, checkTabIndex?: boolean) element.tagName === 'TEXTAREA' || element.tagName === 'SELECT' || isFocusableAttribute === 'true' || - isTabIndexSet); + isTabIndexSet || + delegatesFocus); return checkTabIndex ? tabIndex !== -1 && result : result; } diff --git a/packages/utilities/src/index.ts b/packages/utilities/src/index.ts index 508d80d2012ee0..cdc08863809e29 100644 --- a/packages/utilities/src/index.ts +++ b/packages/utilities/src/index.ts @@ -83,8 +83,10 @@ export { elementContains, elementContainsAttribute, findElementRecursive, + getActiveElement, getChildren, getDocument, + getEventTarget, getFirstVisibleElementFromSelector, getParent, getRect, @@ -237,6 +239,7 @@ import './version'; export type { IStyleFunctionOrObject, Omit } from '@fluentui/merge-styles'; export { + MergeStylesShadowRootContext, MergeStylesShadowRootProvider, useAdoptedStylesheet, useHasMergeStylesShadowRootContext, diff --git a/packages/utilities/src/shadowDom/MergeStylesShadowRootContext.tsx b/packages/utilities/src/shadowDom/MergeStylesShadowRootContext.tsx index db64ef7be0f227..115db9ec0f0777 100644 --- a/packages/utilities/src/shadowDom/MergeStylesShadowRootContext.tsx +++ b/packages/utilities/src/shadowDom/MergeStylesShadowRootContext.tsx @@ -24,7 +24,9 @@ export type MergeStylesShadowRootContextValue = { shadowRoot?: ShadowRoot | null; }; -const MergeStylesShadowRootContext = React.createContext(undefined); +export const MergeStylesShadowRootContext = React.createContext( + undefined, +); export type MergeStylesShadowRootProviderProps = { /**