Skip to content

Commit 393c698

Browse files
committed
Add feature flag and fix a couple probable bugs
1 parent 4e42519 commit 393c698

File tree

14 files changed

+75
-57
lines changed

14 files changed

+75
-57
lines changed

packages/@react-aria/focus/src/FocusScope.tsx

+13-11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import {
1414
createShadowTreeWalker,
1515
getActiveElement,
16+
getEventTarget,
1617
getOwnerDocument,
1718
isAndroid,
1819
isChrome,
@@ -342,13 +343,13 @@ function useFocusContainment(scopeRef: RefObject<Element[] | null>, contain?: bo
342343
}
343344
};
344345

345-
let onFocus = (e) => {
346+
let onFocus: EventListener = (e) => {
346347
// If focusing an element in a child scope of the currently active scope, the child becomes active.
347348
// Moving out of the active scope to an ancestor is not allowed.
348-
if ((!activeScope || isAncestorScope(activeScope, scopeRef)) && isElementInScope(e.target, scopeRef.current)) {
349+
if ((!activeScope || isAncestorScope(activeScope, scopeRef)) && isElementInScope(getEventTarget(e) as Element, scopeRef.current)) {
349350
activeScope = scopeRef;
350-
focusedNode.current = e.target;
351-
} else if (shouldContainFocus(scopeRef) && !isElementInChildScope(e.target, scopeRef)) {
351+
focusedNode.current = getEventTarget(e) as FocusableElement;
352+
} else if (shouldContainFocus(scopeRef) && !isElementInChildScope(getEventTarget(e) as Element, scopeRef)) {
352353
// If a focus event occurs outside the active scope (e.g. user tabs from browser location bar),
353354
// restore focus to the previously focused node or the first tabbable element in the active scope.
354355
if (focusedNode.current) {
@@ -357,11 +358,11 @@ function useFocusContainment(scopeRef: RefObject<Element[] | null>, contain?: bo
357358
focusFirstInScope(activeScope.current);
358359
}
359360
} else if (shouldContainFocus(scopeRef)) {
360-
focusedNode.current = e.target;
361+
focusedNode.current = getEventTarget(e) as FocusableElement;
361362
}
362363
};
363364

364-
let onBlur = (e) => {
365+
let onBlur: EventListener = (e) => {
365366
// Firefox doesn't shift focus back to the Dialog properly without this
366367
if (raf.current) {
367368
cancelAnimationFrame(raf.current);
@@ -377,8 +378,9 @@ function useFocusContainment(scopeRef: RefObject<Element[] | null>, contain?: bo
377378
let activeElement = getActiveElement(ownerDocument);
378379
if (!shouldSkipFocusRestore && activeElement && shouldContainFocus(scopeRef) && !isElementInChildScope(activeElement, scopeRef)) {
379380
activeScope = scopeRef;
380-
if (e.target.isConnected) {
381-
focusedNode.current = e.target;
381+
let target = getEventTarget(e) as FocusableElement;
382+
if (target && target.isConnected) {
383+
focusedNode.current = target;
382384
focusedNode.current?.focus();
383385
} else if (activeScope.current) {
384386
focusFirstInScope(activeScope.current);
@@ -521,7 +523,7 @@ function useActiveScopeTracker(scopeRef: RefObject<Element[] | null>, restore?:
521523
const ownerDocument = getOwnerDocument(scope ? scope[0] : undefined);
522524

523525
let onFocus = (e) => {
524-
let target = e.target as Element;
526+
let target = getEventTarget(e) as Element;
525527
if (isElementInScope(target, scopeRef.current)) {
526528
activeScope = scopeRef;
527529
} else if (!isElementInAnyScope(target)) {
@@ -735,7 +737,7 @@ function restoreFocusToElement(node: FocusableElement) {
735737
* Create a [TreeWalker]{@link https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker}
736738
* that matches all focusable/tabbable elements.
737739
*/
738-
export function getFocusableTreeWalker(root: Element, opts?: FocusManagerOptions, scope?: Element[]): ShadowTreeWalker {
740+
export function getFocusableTreeWalker(root: Element, opts?: FocusManagerOptions, scope?: Element[]): ShadowTreeWalker | TreeWalker {
739741
let filter = opts?.tabbable ? isTabbable : isFocusable;
740742

741743
// Ensure that root is an Element or fall back appropriately
@@ -863,7 +865,7 @@ export function createFocusManager(ref: RefObject<Element | null>, defaultOption
863865
};
864866
}
865867

866-
function last(walker: ShadowTreeWalker) {
868+
function last(walker: ShadowTreeWalker | TreeWalker) {
867869
let next: FocusableElement | undefined = undefined;
868870
let last: FocusableElement;
869871
do {

packages/@react-aria/focus/test/FocusScope.test.js

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import {act, createShadowRoot, fireEvent, pointerMap, render, waitFor} from '@react-spectrum/test-utils-internal';
1414
import {defaultTheme} from '@adobe/react-spectrum';
1515
import {DialogContainer} from '@react-spectrum/dialog';
16+
import {enableShadowDOM} from '@react-stately/flags';
1617
import {FocusScope, useFocusManager} from '../';
1718
import {focusScopeTree} from '../src/FocusScope';
1819
import {Provider} from '@react-spectrum/provider';
@@ -1723,6 +1724,7 @@ describe('FocusScope with Shadow DOM', function () {
17231724
let user;
17241725

17251726
beforeAll(() => {
1727+
enableShadowDOM();
17261728
user = userEvent.setup({delay: null, pointerMap});
17271729
});
17281730

packages/@react-aria/interactions/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"dependencies": {
2525
"@react-aria/ssr": "^3.9.7",
2626
"@react-aria/utils": "^3.27.0",
27+
"@react-stately/flags": "^3.0.5",
2728
"@react-types/shared": "^3.27.0",
2829
"@swc/helpers": "^0.5.0"
2930
},

packages/@react-aria/interactions/src/useFocus.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import {DOMAttributes, FocusableElement, FocusEvents} from '@react-types/shared';
1919
import {FocusEvent, useCallback} from 'react';
20-
import {getActiveElement, getOwnerDocument} from '@react-aria/utils';
20+
import {getActiveElement, getEventTarget, getOwnerDocument} from '@react-aria/utils';
2121
import {useSyntheticBlurEvent} from './utils';
2222

2323
export interface FocusProps<Target = FocusableElement> extends FocusEvents<Target> {
@@ -65,7 +65,7 @@ export function useFocus<Target extends FocusableElement = FocusableElement>(pro
6565

6666
const ownerDocument = getOwnerDocument(e.target);
6767
const activeElement = ownerDocument ? getActiveElement(ownerDocument) : getActiveElement();
68-
if (e.target === e.currentTarget && activeElement === e.target) {
68+
if (e.target === e.currentTarget && activeElement === getEventTarget(e.nativeEvent)) {
6969
if (onFocusProp) {
7070
onFocusProp(e);
7171
}

packages/@react-aria/interactions/src/useFocusWithin.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import {DOMAttributes} from '@react-types/shared';
1919
import {FocusEvent, useCallback, useRef} from 'react';
20-
import {getActiveElement, getOwnerDocument} from '@react-aria/utils';
20+
import {getActiveElement, getEventTarget, getOwnerDocument} from '@react-aria/utils';
2121
import {useSyntheticBlurEvent} from './utils';
2222

2323
export interface FocusWithinProps {
@@ -73,7 +73,7 @@ export function useFocusWithin(props: FocusWithinProps): FocusWithinResult {
7373
// focus handler already moved focus somewhere else.
7474
const ownerDocument = getOwnerDocument(e.target);
7575
const activeElement = getActiveElement(ownerDocument);
76-
if (!state.current.isFocusWithin && activeElement === e.target) {
76+
if (!state.current.isFocusWithin && activeElement === getEventTarget(e.nativeEvent)) {
7777
if (onFocusWithin) {
7878
onFocusWithin(e);
7979
}

packages/@react-aria/interactions/test/useFocus.test.js

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
import {act, createShadowRoot, render, waitFor} from '@react-spectrum/test-utils-internal';
14+
import {enableShadowDOM} from '@react-stately/flags';
1415
import React from 'react';
1516
import ReactDOM from 'react-dom';
1617
import {useFocus} from '../';
@@ -156,6 +157,9 @@ describe('useFocus', function () {
156157
});
157158

158159
describe('useFocus with Shadow DOM', function () {
160+
beforeAll(() => {
161+
enableShadowDOM();
162+
});
159163
it('handles focus events', function () {
160164
const {shadowRoot, shadowHost} = createShadowRoot();
161165
const events = [];

packages/@react-aria/interactions/test/usePress.test.js

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import {act, createShadowRoot, fireEvent, installMouseEvent, installPointerEvent, render, waitFor} from '@react-spectrum/test-utils-internal';
1414
import {ActionButton} from '@react-spectrum/button';
1515
import {Dialog, DialogTrigger} from '@react-spectrum/dialog';
16+
import {enableShadowDOM} from '@react-stately/flags';
1617
import MatchMediaMock from 'jest-matchmedia-mock';
1718
import {Provider} from '@react-spectrum/provider';
1819
import React from 'react';
@@ -3809,6 +3810,7 @@ describe('usePress', function () {
38093810
}
38103811

38113812
beforeAll(() => {
3813+
enableShadowDOM();
38123814
jest.useFakeTimers();
38133815
});
38143816

packages/@react-aria/utils/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
},
2424
"dependencies": {
2525
"@react-aria/ssr": "^3.9.7",
26+
"@react-stately/flags": "^3.0.5",
2627
"@react-stately/utils": "^3.10.5",
2728
"@react-types/shared": "^3.27.0",
2829
"@swc/helpers": "^0.5.0",

packages/@react-aria/utils/src/shadowdom/DOMFunctions.ts

+18-41
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
// Source: https://github.com/microsoft/tabster/blob/a89fc5d7e332d48f68d03b1ca6e344489d1c3898/src/Shadowdomize/DOMFunctions.ts#L16
22

33
import {isShadowRoot} from '../domHelpers';
4+
import {shadowDOM} from '@react-stately/flags';
45

6+
/**
7+
* ShadowDOM safe version of Node.contains.
8+
*/
59
export function nodeContains(
610
node: Node | null | undefined,
711
otherNode: Node | null | undefined
812
): boolean {
13+
if (!shadowDOM()) {
14+
return otherNode && node ? node.contains(otherNode) : false;
15+
}
16+
917
if (!node || !otherNode) {
1018
return false;
1119
}
@@ -32,7 +40,13 @@ export function nodeContains(
3240
return false;
3341
}
3442

43+
/**
44+
* ShadowDOM safe version of document.activeElement.
45+
*/
3546
export const getActiveElement = (doc: Document = document) => {
47+
if (!shadowDOM()) {
48+
return doc.activeElement;
49+
}
3650
let activeElement: Element | null = doc.activeElement;
3751

3852
while (activeElement && 'shadowRoot' in activeElement &&
@@ -43,48 +57,11 @@ export const getActiveElement = (doc: Document = document) => {
4357
return activeElement;
4458
};
4559

46-
export function getLastChild(node: Node | null | undefined): ChildNode | null {
47-
if (!node) {
48-
return null;
49-
}
50-
51-
if (!node.lastChild && (node as Element).shadowRoot) {
52-
return getLastChild((node as Element).shadowRoot);
53-
}
54-
55-
return node.lastChild;
56-
}
57-
58-
export function getPreviousSibling(
59-
node: Node | null | undefined
60-
): ChildNode | null {
61-
if (!node) {
62-
return null;
63-
}
64-
65-
let sibling = node.previousSibling;
66-
67-
if (!sibling && node.parentElement?.shadowRoot) {
68-
sibling = getLastChild(node.parentElement.shadowRoot);
69-
}
70-
71-
return sibling;
72-
}
73-
74-
export function getLastElementChild(
75-
element: Element | null | undefined
76-
): Element | null {
77-
let child = getLastChild(element);
78-
79-
while (child && child.nodeType !== Node.ELEMENT_NODE) {
80-
child = getPreviousSibling(child);
81-
}
82-
83-
return child as Element | null;
84-
}
85-
60+
/**
61+
* ShadowDOM safe version of event.target.
62+
*/
8663
export function getEventTarget(event): Element {
87-
if (event.target.shadowRoot) {
64+
if (shadowDOM() && event.target.shadowRoot) {
8865
if (event.composedPath) {
8966
return event.composedPath()[0];
9067
}

packages/@react-aria/utils/src/shadowdom/ShadowTreeWalker.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// https://github.com/microsoft/tabster/blob/a89fc5d7e332d48f68d03b1ca6e344489d1c3898/src/Shadowdomize/ShadowTreeWalker.ts
22

33
import {nodeContains} from './DOMFunctions';
4+
import {shadowDOM} from '@react-stately/flags';
45

56
export class ShadowTreeWalker implements TreeWalker {
67
public readonly filter: NodeFilter | null;
@@ -302,11 +303,17 @@ export class ShadowTreeWalker implements TreeWalker {
302303
}
303304
}
304305

306+
/**
307+
* ShadowDOM safe version of document.createTreeWalker.
308+
*/
305309
export function createShadowTreeWalker(
306310
doc: Document,
307311
root: Node,
308312
whatToShow?: number,
309313
filter?: NodeFilter | null
310314
) {
311-
return new ShadowTreeWalker(doc, root, whatToShow, filter);
315+
if (shadowDOM()) {
316+
return new ShadowTreeWalker(doc, root, whatToShow, filter);
317+
}
318+
return doc.createTreeWalker(root, whatToShow, filter);
312319
}

packages/@react-aria/utils/test/domHelpers.test.js

+7
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@
1212

1313

1414
import {act} from 'react-dom/test-utils';
15+
import {enableShadowDOM} from '@react-stately/flags';
1516
import {getActiveElement, getOwnerWindow} from '../';
1617

1718
describe('getOwnerWindow', () => {
19+
beforeAll(() => {
20+
enableShadowDOM();
21+
});
1822
test.each([null, undefined])('returns the window if the argument is %p', (value) => {
1923
expect(getOwnerWindow(value)).toBe(window);
2024
});
@@ -43,6 +47,9 @@ describe('getOwnerWindow', () => {
4347
});
4448

4549
describe('getActiveElement', () => {
50+
beforeAll(() => {
51+
enableShadowDOM();
52+
});
4653
it('returns the body as the active element by default', () => {
4754
act(() => {document.body.focus();}); // Ensure the body is focused, clearing any specific active element
4855
expect(getActiveElement()).toBe(document.body);

packages/@react-aria/utils/test/shadowTreeWalker.test.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
1212

1313
import {createShadowRoot, render} from '@react-spectrum/test-utils-internal';
1414
import {createShadowTreeWalker} from '../src';
15+
import {enableShadowDOM} from '@react-stately/flags';
1516
import React from 'react';
1617
import ReactDOM from 'react-dom';
1718

1819
describe('ShadowTreeWalker', () => {
20+
beforeAll(() => {
21+
enableShadowDOM();
22+
});
1923
describe('Shadow free', () => {
2024
it('walks through the dom', () => {
2125
render(

packages/@react-stately/flags/src/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
let _tableNestedRows = false;
14+
let _shadowDOM = false;
1415

1516
export function enableTableNestedRows() {
1617
_tableNestedRows = true;
@@ -19,3 +20,11 @@ export function enableTableNestedRows() {
1920
export function tableNestedRows() {
2021
return _tableNestedRows;
2122
}
23+
24+
export function enableShadowDOM() {
25+
_shadowDOM = true;
26+
}
27+
28+
export function shadowDOM() {
29+
return _shadowDOM;
30+
}

yarn.lock

+2
Original file line numberDiff line numberDiff line change
@@ -6251,6 +6251,7 @@ __metadata:
62516251
dependencies:
62526252
"@react-aria/ssr": "npm:^3.9.7"
62536253
"@react-aria/utils": "npm:^3.27.0"
6254+
"@react-stately/flags": "npm:^3.0.5"
62546255
"@react-types/shared": "npm:^3.27.0"
62556256
"@swc/helpers": "npm:^0.5.0"
62566257
peerDependencies:
@@ -6807,6 +6808,7 @@ __metadata:
68076808
resolution: "@react-aria/utils@workspace:packages/@react-aria/utils"
68086809
dependencies:
68096810
"@react-aria/ssr": "npm:^3.9.7"
6811+
"@react-stately/flags": "npm:^3.0.5"
68106812
"@react-stately/utils": "npm:^3.10.5"
68116813
"@react-types/shared": "npm:^3.27.0"
68126814
"@swc/helpers": "npm:^0.5.0"

0 commit comments

Comments
 (0)