Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ parcel build packages/@react-{spectrum,aria,stately}/*/ packages/@internationali
make: *** [build] Segmentation fault: 11
```

It's likely that you are using a different version of Node.js. Please use Node.js 18. When changing the node version, delete `node_modules` and re-run `yarn install`
It's likely that you are using a different version of Node.js. Please use Node.js 22. When changing the node version, delete `node_modules` and re-run `yarn install`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package.json has:

    "@types/node@npm:*": "^22",
    "@types/node@npm:^18.0.0": "^22",
    "@types/node@npm:>= 8": "^22",

This implies that the recommended Node.js version has changed. If true, documentation should update as well.


> `yarn start` fails.

Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -297,5 +297,9 @@
},
"locales": [
"en-US"
]
],
"volta": {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Volta has been extremely helpful in my projects to standardize versions of node and yarn used by my developers. Hopefully this isn't an untoward addition to package.json.

"node": "22.20.0",
"yarn": "4.2.2"
}
}
8 changes: 4 additions & 4 deletions packages/@react-aria/actiongroup/src/useActionGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
import {AriaActionGroupProps} from '@react-types/actiongroup';
import {createFocusManager} from '@react-aria/focus';
import {DOMAttributes, FocusableElement, Orientation, RefObject} from '@react-types/shared';
import {filterDOMProps, useLayoutEffect} from '@react-aria/utils';
import {filterDOMProps, getEventTarget, nodeContains, useLayoutEffect} from '@react-aria/utils';
import {ListState} from '@react-stately/list';
import {useLocale} from '@react-aria/i18n';
import {useState} from 'react';
import {useState, KeyboardEvent} from 'react';

const BUTTON_GROUP_ROLES = {
'none': 'toolbar',
Expand Down Expand Up @@ -47,8 +47,8 @@ export function useActionGroup<T>(props: AriaActionGroupProps<T>, state: ListSta
let {direction} = useLocale();
let focusManager = createFocusManager(ref);
let flipDirection = direction === 'rtl' && orientation === 'horizontal';
let onKeyDown = (e) => {
if (!e.currentTarget.contains(e.target)) {
let onKeyDown = (e: KeyboardEvent<FocusableElement>) => {
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
return;
}

Expand Down
22 changes: 12 additions & 10 deletions packages/@react-aria/autocomplete/src/useAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import {AriaLabelingProps, BaseEvent, DOMProps, FocusableElement, FocusEvents, KeyboardEvents, Node, RefObject, ValueBase} from '@react-types/shared';
import {AriaTextFieldProps} from '@react-aria/textfield';
import {AutocompleteProps, AutocompleteState} from '@react-stately/autocomplete';
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useId, useLabels, useObjectRef} from '@react-aria/utils';
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getEventTarget, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useId, useLabels, useObjectRef} from '@react-aria/utils';
import {dispatchVirtualBlur, dispatchVirtualFocus, getVirtuallyFocusedElement, moveVirtualFocus} from '@react-aria/focus';
import {getInteractionModality} from '@react-aria/interactions';
// @ts-ignore
Expand Down Expand Up @@ -112,7 +112,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
inputRef.current.focus();
}

let target = e.target as Element | null;
let target = getEventTarget(e) as Element | null;
if (e.isTrusted || !target || queuedActiveDescendant.current === target.id) {
return;
}
Expand Down Expand Up @@ -221,7 +221,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
let keyDownTarget = useRef<Element | null>(null);
// For textfield specific keydown operations
let onKeyDown = (e: BaseEvent<ReactKeyboardEvent<any>>) => {
keyDownTarget.current = e.target as Element;
keyDownTarget.current = getEventTarget(e) as Element;
if (e.nativeEvent.isComposing) {
return;
}
Expand Down Expand Up @@ -325,7 +325,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
// Dispatch simulated key up events for things like triggering links in listbox
// Make sure to stop the propagation of the input keyup event so that the simulated keyup/down pair
// is detected by usePress instead of the original keyup originating from the input
if (e.target === keyDownTarget.current) {
if (getEventTarget(e) === keyDownTarget.current) {
e.stopImmediatePropagation();
let focusedNodeId = queuedActiveDescendant.current;
if (focusedNodeId == null) {
Expand Down Expand Up @@ -382,12 +382,14 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut

let curFocusedNode = queuedActiveDescendant.current ? document.getElementById(queuedActiveDescendant.current) : null;
if (curFocusedNode) {
let target = e.target;
queueMicrotask(() => {
// instead of focusing the last focused node, just focus the collection instead and have the collection handle what item to focus via useSelectableCollection/Item
dispatchVirtualBlur(target, collectionRef.current);
dispatchVirtualFocus(collectionRef.current!, target);
});
let target = getEventTarget(e);
if (target instanceof Element) {
queueMicrotask(() => {
// instead of focusing the last focused node, just focus the collection instead and have the collection handle what item to focus via useSelectableCollection/Item
dispatchVirtualBlur(target, collectionRef.current);
dispatchVirtualFocus(collectionRef.current!, target);
});
}
}
};

Expand Down
10 changes: 6 additions & 4 deletions packages/@react-aria/calendar/src/useCalendarCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
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, getEventTarget, getScrollParent, mergeProps, scrollIntoViewport, useDeepMemo, useDescription} from '@react-aria/utils';
import {getEraFormat, hookData} from './utils';
import {getInteractionModality, usePress} from '@react-aria/interactions';
import {getActiveElement} from '@react-aria/utils';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {useDateFormatter, useLocalizedStringFormatter} from '@react-aria/i18n';
Expand Down Expand Up @@ -291,7 +292,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(document) === ref.current) {
scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
}
}
Expand Down Expand Up @@ -334,11 +335,12 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
}
},
onPointerDown(e) {
const eventTarget = getEventTarget(e);
// This is necessary on touch devices to allow dragging
// outside the original pressed element.
// (JSDOM does not support this)
if ('releasePointerCapture' in e.target) {
e.target.releasePointerCapture(e.pointerId);
if (eventTarget instanceof Element && 'releasePointerCapture' in eventTarget) {
eventTarget.releasePointerCapture(e.pointerId);
}
},
onContextMenu(e) {
Expand Down
10 changes: 5 additions & 5 deletions packages/@react-aria/calendar/src/useRangeCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {AriaRangeCalendarProps, DateValue} from '@react-types/calendar';
import {CalendarAria, useCalendarBase} from './useCalendarBase';
import {FocusableElement, RefObject} from '@react-types/shared';
import {RangeCalendarState} from '@react-stately/calendar';
import {useEvent} from '@react-aria/utils';
import {getActiveElement, useEvent, nodeContains, getEventTarget} from '@react-aria/utils';
import {useRef} from 'react';

/**
Expand Down Expand Up @@ -49,11 +49,11 @@ export function useRangeCalendar<T extends DateValue>(props: AriaRangeCalendarPr
return;
}

let target = e.target as Element;
let target = getEventTarget(e) as Element;
if (
ref.current &&
ref.current.contains(document.activeElement) &&
(!ref.current.contains(target) || !target.closest('button, [role="button"]'))
nodeContains(ref.current, getActiveElement(document)) &&
(!nodeContains(ref.current, target) || !target.closest('button, [role="button"]'))
) {
state.selectFocusedDate();
}
Expand All @@ -66,7 +66,7 @@ export function useRangeCalendar<T extends DateValue>(props: AriaRangeCalendarPr
if (!ref.current) {
return;
}
if ((!e.relatedTarget || !ref.current.contains(e.relatedTarget)) && state.anchorDate) {
if ((!e.relatedTarget || !nodeContains(ref.current, e.relatedTarget)) && state.anchorDate) {
state.selectFocusedDate();
}
};
Expand Down
6 changes: 3 additions & 3 deletions packages/@react-aria/combobox/src/useComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {AriaComboBoxProps} from '@react-types/combobox';
import {ariaHideOutside} from '@react-aria/overlays';
import {AriaListBoxOptions, getItemId, listData} from '@react-aria/listbox';
import {BaseEvent, DOMAttributes, KeyboardDelegate, LayoutDelegate, PressEvent, RefObject, RouterOptions, ValidationResult} from '@react-types/shared';
import {chain, getActiveElement, getOwnerDocument, isAppleDevice, mergeProps, useEvent, useLabels, useRouter, useUpdateEffect} from '@react-aria/utils';
import {chain, getActiveElement, getEventTarget, getOwnerDocument, isAppleDevice, mergeProps, nodeContains, useEvent, useLabels, useRouter, useUpdateEffect} from '@react-aria/utils';
import {ComboBoxState} from '@react-stately/combobox';
import {dispatchVirtualFocus} from '@react-aria/focus';
import {FocusEvent, InputHTMLAttributes, KeyboardEvent, TouchEvent, useEffect, useMemo, useRef} from 'react';
Expand Down Expand Up @@ -181,7 +181,7 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta

let onBlur = (e: FocusEvent<HTMLInputElement>) => {
let blurFromButton = buttonRef?.current && buttonRef.current === e.relatedTarget;
let blurIntoPopover = popoverRef.current?.contains(e.relatedTarget);
let blurIntoPopover = nodeContains(popoverRef.current, e.relatedTarget);
// Ignore blur if focused moved to the button(if exists) or into the popover.
if (blurFromButton || blurIntoPopover) {
return;
Expand Down Expand Up @@ -262,7 +262,7 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
return;
}

let rect = (e.target as Element).getBoundingClientRect();
let rect = (getEventTarget(e) as Element).getBoundingClientRect();
let touch = e.changedTouches[0];

let centerX = Math.ceil(rect.left + .5 * rect.width);
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/datepicker/src/useDatePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {CalendarProps} from '@react-types/calendar';
import {createFocusManager} from '@react-aria/focus';
import {DatePickerState} from '@react-stately/datepicker';
import {DOMAttributes, GroupDOMAttributes, KeyboardEvent, RefObject, ValidationResult} from '@react-types/shared';
import {filterDOMProps, mergeProps, useDescription, useId} from '@react-aria/utils';
import {filterDOMProps, mergeProps, nodeContains, useDescription, useId} from '@react-aria/utils';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {privateValidationStateProp} from '@react-stately/form';
Expand Down Expand Up @@ -84,7 +84,7 @@ export function useDatePicker<T extends DateValue>(props: AriaDatePickerProps<T>
onBlurWithin: e => {
// Ignore when focus moves into the popover.
let dialog = document.getElementById(dialogId);
if (!dialog?.contains(e.relatedTarget)) {
if (!nodeContains(dialog, e.relatedTarget)) {
isFocused.current = false;
props.onBlur?.(e);
props.onFocusChange?.(false);
Expand Down
10 changes: 5 additions & 5 deletions packages/@react-aria/datepicker/src/useDatePickerGroup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {createFocusManager, getFocusableTreeWalker} from '@react-aria/focus';
import {DateFieldState, DatePickerState, DateRangePickerState} from '@react-stately/datepicker';
import {DOMAttributes, FocusableElement, KeyboardEvent, RefObject} from '@react-types/shared';
import {mergeProps} from '@react-aria/utils';
import {getEventTarget, mergeProps, nodeContains} from '@react-aria/utils';
import {useLocale} from '@react-aria/i18n';
import {useMemo} from 'react';
import {usePress} from '@react-aria/interactions';
Expand All @@ -12,7 +12,7 @@ export function useDatePickerGroup(state: DatePickerState | DateRangePickerState

// Open the popover on alt + arrow down
let onKeyDown = (e: KeyboardEvent) => {
if (!e.currentTarget.contains(e.target)) {
if (!nodeContains(e.currentTarget, getEventTarget(e))) {
return;
}

Expand All @@ -32,7 +32,7 @@ export function useDatePickerGroup(state: DatePickerState | DateRangePickerState
e.stopPropagation();
if (direction === 'rtl') {
if (ref.current) {
let target = e.target as FocusableElement;
let target = getEventTarget(e) as FocusableElement;
let prev = findNextSegment(ref.current, target.getBoundingClientRect().left, -1);

if (prev) {
Expand All @@ -48,7 +48,7 @@ export function useDatePickerGroup(state: DatePickerState | DateRangePickerState
e.stopPropagation();
if (direction === 'rtl') {
if (ref.current) {
let target = e.target as FocusableElement;
let target = getEventTarget(e) as FocusableElement;
let next = findNextSegment(ref.current, target.getBoundingClientRect().left, 1);

if (next) {
Expand All @@ -68,7 +68,7 @@ export function useDatePickerGroup(state: DatePickerState | DateRangePickerState
return;
}
// Try to find the segment prior to the element that was clicked on.
let target = window.event?.target as FocusableElement;
let target = window.event ? getEventTarget(window.event) as FocusableElement : null;
let walker = getFocusableTreeWalker(ref.current, {tabbable: true});
if (target) {
walker.currentNode = target;
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/datepicker/src/useDateRangePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {DateRange, RangeCalendarProps} from '@react-types/calendar';
import {DateRangePickerState} from '@react-stately/datepicker';
import {DEFAULT_VALIDATION_RESULT, mergeValidation, privateValidationStateProp} from '@react-stately/form';
import {DOMAttributes, GroupDOMAttributes, KeyboardEvent, RefObject, ValidationResult} from '@react-types/shared';
import {filterDOMProps, mergeProps, useDescription, useId} from '@react-aria/utils';
import {filterDOMProps, mergeProps, nodeContains, useDescription, useId} from '@react-aria/utils';
import {focusManagerSymbol, roleSymbol} from './useDateField';
// @ts-ignore
import intlMessages from '../intl/*.json';
Expand Down Expand Up @@ -116,7 +116,7 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
onBlurWithin: e => {
// Ignore when focus moves into the popover.
let dialog = document.getElementById(dialogId);
if (!dialog?.contains(e.relatedTarget)) {
if (!nodeContains(dialog, e.relatedTarget)) {
isFocused.current = false;
props.onBlur?.(e);
props.onFocusChange?.(false);
Expand Down
7 changes: 4 additions & 3 deletions packages/@react-aria/datepicker/src/useDateSegment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@

import {CalendarDate, toCalendar} from '@internationalized/date';
import {DateFieldState, DateSegment} from '@react-stately/datepicker';
import {getScrollParent, isIOS, isMac, mergeProps, scrollIntoViewport, useEvent, useId, useLabels, useLayoutEffect} from '@react-aria/utils';
import {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';
import {RefObject} from '@react-types/shared';
import {useDateFormatter, useFilter, useLocale} from '@react-aria/i18n';
import {getActiveElement} from '@react-aria/utils';
import {useDisplayNames} from './useDisplayNames';
import {useSpinButton} from '@react-aria/spinbutton';

Expand Down Expand Up @@ -281,7 +282,7 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref:
// Otherwise, when tapping on a segment in Android Chrome and then entering text,
// composition events will be fired that break the DOM structure and crash the page.
let selection = window.getSelection();
if (selection?.anchorNode && ref.current?.contains(selection?.anchorNode)) {
if (selection?.anchorNode && nodeContains(ref.current, selection?.anchorNode)) {
selection.collapse(ref.current);
}
});
Expand Down Expand Up @@ -339,7 +340,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(document) === 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, 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 && !ref.current.contains(document.activeElement)) {
if (ref.current && !nodeContains(ref.current, getActiveElement(document))) {
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(document) === ref.current || getActiveElement(document) === document.body) {
isRefocusing.current = true;
if (ref.current) {
ref.current.blur();
Expand Down
Loading