Skip to content

Commit b66f676

Browse files
authored
fix: all active element access for shadow dom (#9608)
* fix: all active element access for shadow dom * fix lint * fix lint * fix types * fix lint for real * fix import
1 parent 2e8c961 commit b66f676

25 files changed

Lines changed: 241 additions & 56 deletions

File tree

eslint.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ export default [{
250250
"rsp-rules/no-react-key": [ERROR],
251251
"rsp-rules/sort-imports": [ERROR],
252252
"rsp-rules/no-non-shadow-contains": [ERROR],
253+
"rsp-rules/shadow-safe-active-element": [ERROR],
253254
"rsp-rules/faster-node-contains": [ERROR],
254255
"rulesdir/imports": [ERROR],
255256
"rulesdir/useLayoutEffectRule": [ERROR],
@@ -431,6 +432,7 @@ export default [{
431432
"rsp-rules/act-events-test": ERROR,
432433
"rsp-rules/no-getByRole-toThrow": ERROR,
433434
"rsp-rules/no-non-shadow-contains": OFF,
435+
"rsp-rules/shadow-safe-active-element": OFF,
434436
"rsp-rules/faster-node-contains": OFF,
435437
"rulesdir/imports": OFF,
436438
"monorepo/no-internal-import": OFF,
@@ -512,6 +514,7 @@ export default [{
512514
rules: {
513515
"rsp-rules/faster-node-contains": OFF,
514516
"rsp-rules/no-non-shadow-contains": OFF,
517+
"rsp-rules/shadow-safe-active-element": OFF,
515518
},
516519
}, {
517520
files: ["packages/@react-spectrum/s2/**", "packages/dev/s2-docs/**"],

packages/@react-aria/calendar/src/useCalendarCell.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {CalendarDate, isEqualDay, isSameDay, isToday} from '@internationalized/date';
1414
import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
1515
import {DOMAttributes, RefObject} from '@react-types/shared';
16-
import {focusWithoutScrolling, getScrollParent, mergeProps, scrollIntoViewport, useDeepMemo, useDescription} from '@react-aria/utils';
16+
import {focusWithoutScrolling, getActiveElement, getScrollParent, mergeProps, scrollIntoViewport, useDeepMemo, useDescription} from '@react-aria/utils';
1717
import {getEraFormat, hookData} from './utils';
1818
import {getInteractionModality, usePress} from '@react-aria/interactions';
1919
// @ts-ignore
@@ -300,7 +300,7 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
300300
// Also only scroll into view if the cell actually got focused.
301301
// There are some cases where the cell might be disabled or inside,
302302
// an inert container and we don't want to scroll then.
303-
if (getInteractionModality() !== 'pointer' && document.activeElement === ref.current) {
303+
if (getInteractionModality() !== 'pointer' && getActiveElement() === ref.current) {
304304
scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
305305
}
306306
}

packages/@react-aria/datepicker/src/useDateSegment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {CalendarDate, toCalendar} from '@internationalized/date';
1414
import {DateFieldState, DateSegment} from '@react-stately/datepicker';
15-
import {getScrollParent, isIOS, isMac, mergeProps, nodeContains, scrollIntoViewport, useEvent, useId, useLabels, useLayoutEffect} from '@react-aria/utils';
15+
import {getActiveElement, getScrollParent, isIOS, isMac, mergeProps, nodeContains, scrollIntoViewport, useEvent, useId, useLabels, useLayoutEffect} from '@react-aria/utils';
1616
import {hookData} from './useDateField';
1717
import {NumberParser} from '@internationalized/number';
1818
import React, {CSSProperties, useMemo, useRef} from 'react';
@@ -311,7 +311,7 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
311311
let element = ref.current;
312312
return () => {
313313
// If the focused segment is removed, focus the previous one, or the next one if there was no previous one.
314-
if (document.activeElement === element) {
314+
if (getActiveElement() === element) {
315315
let prev = focusManager.focusPrevious();
316316
if (!prev) {
317317
focusManager.focusNext();

packages/@react-aria/dialog/src/useDialog.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {AriaDialogProps} from '@react-types/dialog';
1414
import {DOMAttributes, FocusableElement, RefObject} from '@react-types/shared';
15-
import {filterDOMProps, isFocusWithin, useSlotId} from '@react-aria/utils';
15+
import {filterDOMProps, getActiveElement, isFocusWithin, useSlotId} from '@react-aria/utils';
1616
import {focusSafely} from '@react-aria/interactions';
1717
import {useEffect, useRef} from 'react';
1818
import {useOverlayFocusContain} from '@react-aria/overlays';
@@ -48,7 +48,7 @@ export function useDialog(props: AriaDialogProps, ref: RefObject<FocusableElemen
4848
// is to wait for half a second, then blur and re-focus the dialog.
4949
let timeout = setTimeout(() => {
5050
// Check that the dialog is still focused, or focused was lost to the body.
51-
if (document.activeElement === ref.current || document.activeElement === document.body) {
51+
if (getActiveElement() === ref.current || getActiveElement() === document.body) {
5252
isRefocusing.current = true;
5353
if (ref.current) {
5454
ref.current.blur();

packages/@react-aria/dnd/src/DragManager.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
import {announce} from '@react-aria/live-announcer';
1414
import {ariaHideOutside} from '@react-aria/overlays';
1515
import {DragEndEvent, DragItem, DropActivateEvent, DropEnterEvent, DropEvent, DropExitEvent, DropItem, DropOperation, DropTarget as DroppableCollectionTarget, FocusableElement} from '@react-types/shared';
16+
import {getActiveElement, isVirtualClick, isVirtualPointerEvent, nodeContains} from '@react-aria/utils';
1617
import {getDragModality, getTypes} from './utils';
17-
import {isVirtualClick, isVirtualPointerEvent, nodeContains} from '@react-aria/utils';
1818
import type {LocalizedStringFormatter} from '@internationalized/string';
1919
import {RefObject, useEffect, useState} from 'react';
2020

@@ -570,7 +570,7 @@ class DragSession {
570570
// Re-trigger focus event on active element, since it will not have received it during dragging (see cancelEvent).
571571
// This corrects state such as whether focus ring should appear.
572572
// useDroppableCollection handles this itself, so this is only for standalone drop zones.
573-
document.activeElement?.dispatchEvent(new FocusEvent('focusin', {bubbles: true}));
573+
getActiveElement()?.dispatchEvent(new FocusEvent('focusin', {bubbles: true}));
574574
}
575575

576576
this.setCurrentDropTarget(null);
@@ -584,7 +584,7 @@ class DragSession {
584584
}
585585

586586
// Re-trigger focus event on active element, since it will not have received it during dragging (see cancelEvent).
587-
document.activeElement?.dispatchEvent(new FocusEvent('focusin', {bubbles: true}));
587+
getActiveElement()?.dispatchEvent(new FocusEvent('focusin', {bubbles: true}));
588588

589589
announce(this.stringFormatter.format('dropCanceled'));
590590
}

packages/@react-aria/grid/src/useGridCell.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
import {DOMAttributes, FocusableElement, Key, RefObject} from '@react-types/shared';
1414
import {focusSafely, isFocusVisible} from '@react-aria/interactions';
15+
import {getActiveElement, getScrollParent, isFocusWithin, mergeProps, nodeContains, scrollIntoViewport} from '@react-aria/utils';
1516
import {getFocusableTreeWalker} from '@react-aria/focus';
16-
import {getScrollParent, isFocusWithin, mergeProps, nodeContains, scrollIntoViewport} from '@react-aria/utils';
1717
import {GridCollection, GridNode} from '@react-types/grid';
1818
import {gridMap} from './utils';
1919
import {GridState} from '@react-stately/grid';
@@ -75,7 +75,7 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
7575
let treeWalker = getFocusableTreeWalker(ref.current);
7676
if (focusMode === 'child') {
7777
// If focus is already on a focusable child within the cell, early return so we don't shift focus
78-
if (isFocusWithin(ref.current) && ref.current !== document.activeElement) {
78+
if (isFocusWithin(ref.current) && ref.current !== getActiveElement()) {
7979
return;
8080
}
8181

@@ -109,12 +109,13 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
109109
});
110110

111111
let onKeyDownCapture = (e: ReactKeyboardEvent) => {
112-
if (!nodeContains(e.currentTarget, e.target as Element) || state.isKeyboardNavigationDisabled || !ref.current || !document.activeElement) {
112+
let activeElement = getActiveElement();
113+
if (!nodeContains(e.currentTarget, e.target as Element) || state.isKeyboardNavigationDisabled || !ref.current || !activeElement) {
113114
return;
114115
}
115116

116117
let walker = getFocusableTreeWalker(ref.current);
117-
walker.currentNode = document.activeElement;
118+
walker.currentNode = activeElement;
118119

119120
switch (e.key) {
120121
case 'ArrowLeft': {
@@ -244,7 +245,7 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
244245
// If the cell itself is focused, wait a frame so that focus finishes propagatating
245246
// up to the tree, and move focus to a focusable child if possible.
246247
requestAnimationFrame(() => {
247-
if (focusMode === 'child' && document.activeElement === ref.current) {
248+
if (focusMode === 'child' && getActiveElement() === ref.current) {
248249
focus();
249250
}
250251
});

packages/@react-aria/gridlist/src/useGridListItem.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {chain, getScrollParent, isFocusWithin, mergeProps, nodeContains, scrollIntoViewport, useSlotId, useSyntheticLinkProps} from '@react-aria/utils';
13+
import {chain, getActiveElement, getScrollParent, isFocusWithin, mergeProps, nodeContains, scrollIntoViewport, useSlotId, useSyntheticLinkProps} from '@react-aria/utils';
1414
import {DOMAttributes, FocusableElement, Key, RefObject, Node as RSNode} from '@react-types/shared';
1515
import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
1616
import {getRowId, listMap} from './utils';
@@ -131,14 +131,15 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
131131
});
132132

133133
let onKeyDownCapture = (e: ReactKeyboardEvent) => {
134-
if (!nodeContains(e.currentTarget, e.target as Element) || !ref.current || !document.activeElement) {
134+
let activeElement = getActiveElement();
135+
if (!nodeContains(e.currentTarget, e.target as Element) || !ref.current || !activeElement) {
135136
return;
136137
}
137138

138139
let walker = getFocusableTreeWalker(ref.current);
139-
walker.currentNode = document.activeElement;
140+
walker.currentNode = activeElement;
140141

141-
if ('expandedKeys' in state && document.activeElement === ref.current) {
142+
if ('expandedKeys' in state && activeElement === ref.current) {
142143
if ((e.key === EXPANSION_KEYS['expand'][direction]) && state.selectionManager.focusedKey === node.key && hasChildRows && !state.expandedKeys.has(node.key)) {
143144
state.toggleKey(node.key);
144145
e.stopPropagation();
@@ -244,7 +245,8 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
244245
};
245246

246247
let onKeyDown = (e) => {
247-
if (!nodeContains(e.currentTarget, e.target as Element) || !ref.current || !document.activeElement) {
248+
let activeElement = getActiveElement();
249+
if (!nodeContains(e.currentTarget, e.target as Element) || !ref.current || !activeElement) {
248250
return;
249251
}
250252

@@ -254,7 +256,7 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
254256
// If there is another focusable element within this item, stop propagation so the tab key
255257
// is handled by the browser and not by useSelectableCollection (which would take us out of the list).
256258
let walker = getFocusableTreeWalker(ref.current, {tabbable: true});
257-
walker.currentNode = document.activeElement;
259+
walker.currentNode = activeElement;
258260
let next = e.shiftKey ? walker.previousNode() : walker.nextNode();
259261

260262
if (next) {

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// NOTICE file in the root directory of this source tree.
1616
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
1717

18-
import {getOwnerDocument, getOwnerWindow, isMac, isVirtualClick, openLink} from '@react-aria/utils';
18+
import {getActiveElement, getOwnerDocument, getOwnerWindow, isMac, isVirtualClick, openLink} from '@react-aria/utils';
1919
import {ignoreFocusEvent} from './utils';
2020
import {PointerType} from '@react-types/shared';
2121
import {useEffect, useState} from 'react';
@@ -310,10 +310,11 @@ function isKeyboardFocusEvent(isTextInput: boolean, modality: Modality, e: Handl
310310

311311
// For keyboard events that occur on a non-input element that will move focus into input element (aka ArrowLeft going from Datepicker button to the main input group)
312312
// we need to rely on the user passing isTextInput into here. This way we can skip toggling focus visiblity for said input element
313+
let activeElement = getActiveElement(document);
313314
isTextInput = isTextInput ||
314-
(document.activeElement instanceof IHTMLInputElement && !nonTextInputTypes.has(document.activeElement.type)) ||
315-
document.activeElement instanceof IHTMLTextAreaElement ||
316-
(document.activeElement instanceof IHTMLElement && document.activeElement.isContentEditable);
315+
(activeElement instanceof IHTMLInputElement && !nonTextInputTypes.has(activeElement.type)) ||
316+
activeElement instanceof IHTMLTextAreaElement ||
317+
(activeElement instanceof IHTMLElement && activeElement.isContentEditable);
317318
return !(isTextInput && modality === 'keyboard' && e instanceof IKeyboardEvent && !FOCUS_VISIBLE_INPUT_KEYS[e.key]);
318319
}
319320

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {FocusableElement} from '@react-types/shared';
14-
import {focusWithoutScrolling, getOwnerWindow, isFocusable, useLayoutEffect} from '@react-aria/utils';
14+
import {focusWithoutScrolling, getActiveElement, getOwnerWindow, isFocusable, useLayoutEffect} from '@react-aria/utils';
1515
import {FocusEvent as ReactFocusEvent, SyntheticEvent, useCallback, useRef} from 'react';
1616

1717
// Turn a native event into a React synthetic event.
@@ -84,7 +84,7 @@ export function useSyntheticBlurEvent<Target extends Element = Element>(onBlur:
8484
stateRef.current.observer = new MutationObserver(() => {
8585
if (stateRef.current.isFocused && target.disabled) {
8686
stateRef.current.observer?.disconnect();
87-
let relatedTargetEl = target === document.activeElement ? null : document.activeElement;
87+
let relatedTargetEl = target === getActiveElement() ? null : getActiveElement();
8888
target.dispatchEvent(new FocusEvent('blur', {relatedTarget: relatedTargetEl}));
8989
target.dispatchEvent(new FocusEvent('focusout', {bubbles: true, relatedTarget: relatedTargetEl}));
9090
}

packages/@react-aria/menu/src/useSubmenuTrigger.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {AriaMenuItemProps} from './useMenuItem';
1414
import {AriaMenuOptions} from './useMenu';
1515
import type {AriaPopoverProps, OverlayProps} from '@react-aria/overlays';
1616
import {FocusableElement, FocusStrategy, KeyboardEvent, Node, PressEvent, RefObject} from '@react-types/shared';
17-
import {focusWithoutScrolling, isFocusWithin, nodeContains, useEvent, useId, useLayoutEffect} from '@react-aria/utils';
17+
import {focusWithoutScrolling, getActiveElement, isFocusWithin, nodeContains, useEvent, useId, useLayoutEffect} from '@react-aria/utils';
1818
import type {SubmenuTriggerState} from '@react-stately/menu';
1919
import {useCallback, useRef} from 'react';
2020
import {useLocale} from '@react-aria/i18n';
@@ -159,7 +159,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
159159
onSubmenuOpen('first');
160160
}
161161

162-
if (type === 'menu' && !!submenuRef?.current && document.activeElement === ref?.current) {
162+
if (type === 'menu' && !!submenuRef?.current && getActiveElement() === ref?.current) {
163163
focusWithoutScrolling(submenuRef.current);
164164
}
165165
} else if (state.isOpen) {
@@ -178,7 +178,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
178178
onSubmenuOpen('first');
179179
}
180180

181-
if (type === 'menu' && !!submenuRef?.current && document.activeElement === ref?.current) {
181+
if (type === 'menu' && !!submenuRef?.current && getActiveElement() === ref?.current) {
182182
focusWithoutScrolling(submenuRef.current);
183183
}
184184
} else if (state.isOpen) {

0 commit comments

Comments
 (0)