Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ export default [{
"rsp-rules/no-react-key": [ERROR],
"rsp-rules/sort-imports": [ERROR],
"rsp-rules/no-non-shadow-contains": [ERROR],
"rsp-rules/shadow-safe-active-element": [ERROR],
"rulesdir/imports": [ERROR],
"rulesdir/useLayoutEffectRule": [ERROR],
"rulesdir/pure-render": [ERROR],
Expand Down Expand Up @@ -430,6 +431,7 @@ export default [{
"rsp-rules/act-events-test": ERROR,
"rsp-rules/no-getByRole-toThrow": ERROR,
"rsp-rules/no-non-shadow-contains": OFF,
"rsp-rules/shadow-safe-active-element": OFF,
"rulesdir/imports": OFF,
"monorepo/no-internal-import": OFF,
"jsdoc/require-jsdoc": OFF
Expand Down Expand Up @@ -509,6 +511,7 @@ export default [{

rules: {
"rsp-rules/no-non-shadow-contains": OFF,
"rsp-rules/shadow-safe-active-element": OFF,
},
}, {
files: ["packages/@react-spectrum/s2/**", "packages/dev/s2-docs/**"],
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/calendar/src/useCalendarCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import {CalendarDate, isEqualDay, isSameDay, isToday} from '@internationalized/date';
import {CalendarState, RangeCalendarState} from '@react-stately/calendar';
import {DOMAttributes, RefObject} from '@react-types/shared';
import {focusWithoutScrolling, getScrollParent, mergeProps, scrollIntoViewport, useDeepMemo, useDescription} from '@react-aria/utils';
import {focusWithoutScrolling, getActiveElement, getScrollParent, mergeProps, scrollIntoViewport, useDeepMemo, useDescription} from '@react-aria/utils';
import {getEraFormat, hookData} from './utils';
import {getInteractionModality, usePress} from '@react-aria/interactions';
// @ts-ignore
Expand Down Expand Up @@ -291,7 +291,7 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
// Also only scroll into view if the cell actually got focused.
// There are some cases where the cell might be disabled or inside,
// an inert container and we don't want to scroll then.
if (getInteractionModality() !== 'pointer' && document.activeElement === ref.current) {
if (getInteractionModality() !== 'pointer' && getActiveElement() === ref.current) {
scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/calendar/src/useRangeCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import {AriaRangeCalendarProps, DateValue} from '@react-types/calendar';
import {CalendarAria, useCalendarBase} from './useCalendarBase';
import {FocusableElement, RefObject} from '@react-types/shared';
import {nodeContains, useEvent} from '@react-aria/utils';
import {getActiveElement, nodeContains, useEvent} from '@react-aria/utils';
import {RangeCalendarState} from '@react-stately/calendar';
import {useRef} from 'react';

Expand Down Expand Up @@ -52,7 +52,7 @@ export function useRangeCalendar<T extends DateValue>(props: AriaRangeCalendarPr
let target = e.target as Element;
if (
ref.current &&
nodeContains(ref.current, document.activeElement) &&
nodeContains(ref.current, getActiveElement()) &&
(!nodeContains(ref.current, target) || !target.closest('button, [role="button"]'))
) {
state.selectFocusedDate();
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/datepicker/src/useDateSegment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import {CalendarDate, toCalendar} from '@internationalized/date';
import {DateFieldState, DateSegment} from '@react-stately/datepicker';
import {getScrollParent, isIOS, isMac, mergeProps, nodeContains, scrollIntoViewport, useEvent, useId, useLabels, useLayoutEffect} from '@react-aria/utils';
import {getActiveElement, getScrollParent, isIOS, isMac, mergeProps, nodeContains, scrollIntoViewport, useEvent, useId, useLabels, useLayoutEffect} from '@react-aria/utils';
import {hookData} from './useDateField';
import {NumberParser} from '@internationalized/number';
import React, {CSSProperties, useMemo, useRef} from 'react';
Expand Down Expand Up @@ -311,7 +311,7 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
let element = ref.current;
return () => {
// If the focused segment is removed, focus the previous one, or the next one if there was no previous one.
if (document.activeElement === element) {
if (getActiveElement() === element) {
let prev = focusManager.focusPrevious();
if (!prev) {
focusManager.focusNext();
Expand Down
6 changes: 3 additions & 3 deletions packages/@react-aria/dialog/src/useDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import {AriaDialogProps} from '@react-types/dialog';
import {DOMAttributes, FocusableElement, RefObject} from '@react-types/shared';
import {filterDOMProps, nodeContains, useSlotId} from '@react-aria/utils';
import {filterDOMProps, getActiveElement, nodeContains, useSlotId} from '@react-aria/utils';
import {focusSafely} from '@react-aria/interactions';
import {useEffect, useRef} from 'react';
import {useOverlayFocusContain} from '@react-aria/overlays';
Expand Down Expand Up @@ -40,15 +40,15 @@ export function useDialog(props: AriaDialogProps, ref: RefObject<FocusableElemen

// Focus the dialog itself on mount, unless a child element is already focused.
useEffect(() => {
if (ref.current && !nodeContains(ref.current, document.activeElement)) {
if (ref.current && !nodeContains(ref.current, getActiveElement())) {
focusSafely(ref.current);

// Safari on iOS does not move the VoiceOver cursor to the dialog
// or announce that it has opened until it has rendered. A workaround
// is to wait for half a second, then blur and re-focus the dialog.
let timeout = setTimeout(() => {
// Check that the dialog is still focused, or focused was lost to the body.
if (document.activeElement === ref.current || document.activeElement === document.body) {
if (getActiveElement() === ref.current || getActiveElement() === document.body) {
isRefocusing.current = true;
if (ref.current) {
ref.current.blur();
Expand Down
6 changes: 3 additions & 3 deletions packages/@react-aria/dnd/src/DragManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
import {announce} from '@react-aria/live-announcer';
import {ariaHideOutside} from '@react-aria/overlays';
import {DragEndEvent, DragItem, DropActivateEvent, DropEnterEvent, DropEvent, DropExitEvent, DropItem, DropOperation, DropTarget as DroppableCollectionTarget, FocusableElement} from '@react-types/shared';
import {getActiveElement, isVirtualClick, isVirtualPointerEvent, nodeContains} from '@react-aria/utils';
import {getDragModality, getTypes} from './utils';
import {isVirtualClick, isVirtualPointerEvent, nodeContains} from '@react-aria/utils';
import type {LocalizedStringFormatter} from '@internationalized/string';
import {RefObject, useEffect, useState} from 'react';

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

this.setCurrentDropTarget(null);
Expand All @@ -584,7 +584,7 @@ class DragSession {
}

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

announce(this.stringFormatter.format('dropCanceled'));
}
Expand Down
13 changes: 7 additions & 6 deletions packages/@react-aria/grid/src/useGridCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

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

Expand All @@ -90,7 +90,7 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps

if (
(keyWhenFocused.current != null && node.key !== keyWhenFocused.current) ||
!nodeContains(ref.current, document.activeElement)
!nodeContains(ref.current, getActiveElement())
) {
focusSafely(ref.current);
}
Expand All @@ -109,12 +109,13 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
});

let onKeyDownCapture = (e: ReactKeyboardEvent) => {
if (!nodeContains(e.currentTarget, e.target as Element) || state.isKeyboardNavigationDisabled || !ref.current || !document.activeElement) {
let activeElement = getActiveElement();
if (!nodeContains(e.currentTarget, e.target as Element) || state.isKeyboardNavigationDisabled || !ref.current || !activeElement) {
return;
}

let walker = getFocusableTreeWalker(ref.current);
walker.currentNode = document.activeElement;
walker.currentNode = activeElement;

switch (e.key) {
case 'ArrowLeft': {
Expand Down Expand Up @@ -244,7 +245,7 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
// If the cell itself is focused, wait a frame so that focus finishes propagatating
// up to the tree, and move focus to a focusable child if possible.
requestAnimationFrame(() => {
if (focusMode === 'child' && document.activeElement === ref.current) {
if (focusMode === 'child' && getActiveElement() === ref.current) {
focus();
}
});
Expand Down
16 changes: 9 additions & 7 deletions packages/@react-aria/gridlist/src/useGridListItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import {chain, getScrollParent, mergeProps, nodeContains, scrollIntoViewport, useSlotId, useSyntheticLinkProps} from '@react-aria/utils';
import {chain, getActiveElement, getScrollParent, mergeProps, nodeContains, scrollIntoViewport, useSlotId, useSyntheticLinkProps} from '@react-aria/utils';
import {DOMAttributes, FocusableElement, Key, RefObject, Node as RSNode} from '@react-types/shared';
import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
import {getRowId, listMap} from './utils';
Expand Down Expand Up @@ -79,7 +79,7 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
if (
ref.current !== null &&
((keyWhenFocused.current != null && node.key !== keyWhenFocused.current) ||
!nodeContains(ref.current, document.activeElement))
!nodeContains(ref.current, getActiveElement()))
) {
focusSafely(ref.current);
}
Expand Down Expand Up @@ -131,14 +131,15 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
});

let onKeyDownCapture = (e: ReactKeyboardEvent) => {
if (!nodeContains(e.currentTarget, e.target as Element) || !ref.current || !document.activeElement) {
let activeElement = getActiveElement();
if (!nodeContains(e.currentTarget, e.target as Element) || !ref.current || !activeElement) {
return;
}

let walker = getFocusableTreeWalker(ref.current);
walker.currentNode = document.activeElement;
walker.currentNode = activeElement;

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

let onKeyDown = (e) => {
if (!nodeContains(e.currentTarget, e.target as Element) || !ref.current || !document.activeElement) {
let activeElement = getActiveElement();
if (!nodeContains(e.currentTarget, e.target as Element) || !ref.current || !activeElement) {
return;
}

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

if (next) {
Expand Down
9 changes: 5 additions & 4 deletions packages/@react-aria/interactions/src/useFocusVisible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// NOTICE file in the root directory of this source tree.
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions

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

// 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)
// we need to rely on the user passing isTextInput into here. This way we can skip toggling focus visiblity for said input element
let activeElement = getActiveElement(document);
isTextInput = isTextInput ||
(document.activeElement instanceof IHTMLInputElement && !nonTextInputTypes.has(document.activeElement.type)) ||
document.activeElement instanceof IHTMLTextAreaElement ||
(document.activeElement instanceof IHTMLElement && document.activeElement.isContentEditable);
(activeElement instanceof IHTMLInputElement && !nonTextInputTypes.has(activeElement.type)) ||
activeElement instanceof IHTMLTextAreaElement ||
(activeElement instanceof IHTMLElement && activeElement.isContentEditable);
return !(isTextInput && modality === 'keyboard' && e instanceof IKeyboardEvent && !FOCUS_VISIBLE_INPUT_KEYS[e.key]);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/interactions/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

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

// Turn a native event into a React synthetic event.
Expand Down Expand Up @@ -84,7 +84,7 @@ export function useSyntheticBlurEvent<Target extends Element = Element>(onBlur:
stateRef.current.observer = new MutationObserver(() => {
if (stateRef.current.isFocused && target.disabled) {
stateRef.current.observer?.disconnect();
let relatedTargetEl = target === document.activeElement ? null : document.activeElement;
let relatedTargetEl = target === getActiveElement() ? null : getActiveElement();
target.dispatchEvent(new FocusEvent('blur', {relatedTarget: relatedTargetEl}));
target.dispatchEvent(new FocusEvent('focusout', {bubbles: true, relatedTarget: relatedTargetEl}));
}
Expand Down
8 changes: 4 additions & 4 deletions packages/@react-aria/menu/src/useSubmenuTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {AriaMenuItemProps} from './useMenuItem';
import {AriaMenuOptions} from './useMenu';
import type {AriaPopoverProps, OverlayProps} from '@react-aria/overlays';
import {FocusableElement, FocusStrategy, KeyboardEvent, Node, PressEvent, RefObject} from '@react-types/shared';
import {focusWithoutScrolling, nodeContains, useEvent, useId, useLayoutEffect} from '@react-aria/utils';
import {focusWithoutScrolling, getActiveElement, nodeContains, useEvent, useId, useLayoutEffect} from '@react-aria/utils';
import type {SubmenuTriggerState} from '@react-stately/menu';
import {useCallback, useRef} from 'react';
import {useLocale} from '@react-aria/i18n';
Expand Down Expand Up @@ -100,7 +100,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
let submenuKeyDown = (e: KeyboardEvent) => {
// If focus is not within the menu, assume virtual focus is being used.
// This means some other input element is also within the popover, so we shouldn't close the menu.
if (!nodeContains(e.currentTarget, document.activeElement)) {
if (!nodeContains(e.currentTarget, getActiveElement())) {
return;
}

Expand Down Expand Up @@ -159,7 +159,7 @@ export function useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: Subm
onSubmenuOpen('first');
}

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

if (type === 'menu' && !!submenuRef?.current && document.activeElement === ref?.current) {
if (type === 'menu' && !!submenuRef?.current && getActiveElement() === ref?.current) {
focusWithoutScrolling(submenuRef.current);
}
} else if (state.isOpen) {
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/numberfield/src/useNumberField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import {AriaButtonProps} from '@react-types/button';
import {AriaNumberFieldProps} from '@react-types/numberfield';
import {chain, filterDOMProps, isAndroid, isIOS, isIPhone, mergeProps, useFormReset, useId} from '@react-aria/utils';
import {chain, filterDOMProps, getActiveElement, isAndroid, isIOS, isIPhone, mergeProps, useFormReset, useId} from '@react-aria/utils';
import {DOMAttributes, GroupDOMAttributes, TextInputDOMProps, ValidationResult} from '@react-types/shared';
import {
InputHTMLAttributes,
Expand Down Expand Up @@ -254,7 +254,7 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
let onButtonPressStart = (e) => {
// If focus is already on the input, keep it there so we don't hide the
// software keyboard when tapping the increment/decrement buttons.
if (document.activeElement === inputRef.current) {
if (getActiveElement() === inputRef.current) {
return;
}

Expand Down
Loading