Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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