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
24 changes: 24 additions & 0 deletions .yarn/patches/nwsapi-npm-2.2.23-aa3710d724.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
diff --git a/src/nwsapi.js b/src/nwsapi.js
index 872026b4ab3462f3c2411bc564dd428bd3165323..748f32f16595e62ddf40446e361df6555cfb5495 100644
--- a/src/nwsapi.js
+++ b/src/nwsapi.js
@@ -1241,9 +1241,16 @@
'if((e===n||e.autofocus)){' + source + '}';
break;
case 'focus-within':
- source = 'if(n=s.isFocusable(e)){' +
- 'if(n!==e){while(n){n=n.parentElement;if(n===e)break;}}}' +
- 'if((n===e||n.autofocus)){' + source + '}';
+ // Shadow DOM safe: drill down through shadowRoot.activeElement, then walk up including shadow boundaries
+ source = 'if(s.doc.hasFocus()){' +
+ 'n=s.doc.activeElement;' +
+ // Drill down through shadow roots to find the actual focused element
+ 'while(n&&n.shadowRoot&&n.shadowRoot.activeElement){n=n.shadowRoot.activeElement;}' +
+ // Walk up the tree, crossing shadow boundaries via getRootNode().host
+ 'while(n){' +
+ 'if(n===e){' + source + 'break;}' +
+ 'n=n.parentElement||(n.getRootNode&&n.getRootNode().host);' +
+ '}}';
break;
default:
emit('\'' + expression + '\'' + qsInvalid);
1 change: 1 addition & 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/faster-node-contains": [ERROR],
"rulesdir/imports": [ERROR],
"rulesdir/useLayoutEffectRule": [ERROR],
"rulesdir/pure-render": [ERROR],
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@
"lightningcss": "1.30.1",
"react-server-dom-parcel": "canary",
"react-test-renderer": "19.1.0",
"@parcel/packager-react-static": "^2.16.3"
"@parcel/packager-react-static": "^2.16.3",
"nwsapi@npm:^2.2.2": "patch:nwsapi@npm%3A2.2.23#~/.yarn/patches/nwsapi-npm-2.2.23-aa3710d724.patch"
},
"@parcel/transformer-css": {
"cssModules": {
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/calendar/src/useRangeCalendar.ts
Original file line number Diff line number Diff line change
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) &&
ref.current.matches(':focus-within') &&
(!nodeContains(ref.current, target) || !target.closest('button, [role="button"]'))
) {
state.selectFocusedDate();
Expand Down
4 changes: 2 additions & 2 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, 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,7 +40,7 @@ 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 && !ref.current.matches(':focus-within')) {
focusSafely(ref.current);

// Safari on iOS does not move the VoiceOver cursor to the dialog
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/grid/src/useGridCell.ts
Original file line number Diff line number Diff line change
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 (ref.current.matches(':focus-within') && ref.current !== document.activeElement) {
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)
!ref.current.matches(':focus-within')
) {
focusSafely(ref.current);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/gridlist/src/useGridListItem.ts
Original file line number Diff line number Diff line change
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))
!ref.current.matches(':focus-within'))
) {
focusSafely(ref.current);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/landmark/src/useLandmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ class LandmarkManager implements LandmarkManagerApi {

private focusMain() {
let main = this.getLandmarkByRole('main');
if (main && main.ref.current && nodeContains(document, main.ref.current)) {
if (main && main.ref.current && main.ref.current.isConnected) {
this.focusLandmark(main.ref.current, 'forward');
return true;
}
Expand All @@ -352,7 +352,7 @@ class LandmarkManager implements LandmarkManagerApi {
}

// Otherwise, focus the landmark itself
if (nextLandmark.ref.current && nodeContains(document, nextLandmark.ref.current)) {
if (nextLandmark.ref.current && nextLandmark.ref.current.isConnected) {
this.focusLandmark(nextLandmark.ref.current, backward ? 'backward' : 'forward');
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/menu/src/useSubmenuTrigger.ts
Original file line number Diff line number Diff line change
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 (!e.currentTarget.matches(':focus-within')) {
return;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/overlays/src/useOverlayPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@

import {calculatePosition, getRect, PositionResult} from './calculatePosition';
import {DOMAttributes, RefObject} from '@react-types/shared';
import {nodeContains, useLayoutEffect, useResizeObserver} from '@react-aria/utils';
import {Placement, PlacementAxis, PositionProps} from '@react-types/overlays';
import {useCallback, useEffect, useRef, useState} from 'react';
import {useCloseOnScroll} from './useCloseOnScroll';
import {useLayoutEffect, useResizeObserver} from '@react-aria/utils';
import {useLocale} from '@react-aria/i18n';

export interface AriaPositionProps extends PositionProps {
Expand Down Expand Up @@ -154,7 +154,7 @@ export function useOverlayPosition(props: AriaPositionProps): PositionAria {
// so it can be restored after repositioning. This way if the overlay height
// changes, the focused element appears to stay in the same position.
let anchor: ScrollAnchor | null = null;
if (scrollRef.current && nodeContains(scrollRef.current, document.activeElement)) {
if (scrollRef.current && scrollRef.current.matches(':focus-within')) {
let anchorRect = document.activeElement?.getBoundingClientRect();
let scrollRect = scrollRef.current.getBoundingClientRect();
// Anchor from the top if the offset is in the top half of the scrollable element,
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/selection/src/useSelectableCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
// If the active element is NOT tabbable but is contained by an element that IS tabbable (aka the cell), the browser will actually move focus to
// the containing element. We need to special case this so that tab will move focus out of the grid instead of looping between
// focusing the containing cell and back to the non-tabbable child element
if (next && (!nodeContains(next, document.activeElement) || (document.activeElement && !isTabbable(document.activeElement)))) {
if (next && (!next.matches(':focus-within') || (document.activeElement && !isTabbable(document.activeElement)))) {
focusWithoutScrolling(next);
}
}
Expand Down Expand Up @@ -379,7 +379,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
let element = getItemElement(ref, manager.focusedKey);
if (element instanceof HTMLElement) {
// This prevents a flash of focus on the first/last element in the collection, or the collection itself.
if (!nodeContains(element, document.activeElement) && !shouldUseVirtualFocus) {
if (!element.matches(':focus-within') && !shouldUseVirtualFocus) {
focusWithoutScrolling(element);
}

Expand Down
3 changes: 1 addition & 2 deletions packages/@react-aria/test-utils/src/checkboxgroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import {act, within} from '@testing-library/react';
import {CheckboxGroupTesterOpts, UserOpts} from './types';
import {nodeContains} from '@react-aria/utils';
import {pressElement} from './events';

interface TriggerCheckboxOptions {
Expand Down Expand Up @@ -95,7 +94,7 @@ export class CheckboxGroupTester {
throw new Error('Checkbox provided is not in the checkbox group.');
}

if (!nodeContains(this.checkboxgroup, document.activeElement)) {
if (!this.checkboxgroup.matches(':focus-within')) {
act(() => checkboxes[0].focus());
}

Expand Down
5 changes: 2 additions & 3 deletions packages/@react-aria/test-utils/src/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import {act, waitFor, within} from '@testing-library/react';
import {ComboBoxTesterOpts, UserOpts} from './types';
import {nodeContains} from '@react-aria/utils';

interface ComboBoxOpenOpts {
/**
Expand Down Expand Up @@ -177,7 +176,7 @@ export class ComboBoxTester {

if (option.getAttribute('href') == null) {
await waitFor(() => {
if (nodeContains(document, listbox)) {
if (listbox.isConnected) {
throw new Error('Expected listbox element to not be in the document after selecting an option');
} else {
return true;
Expand All @@ -199,7 +198,7 @@ export class ComboBoxTester {
await this.user.keyboard('[Escape]');

await waitFor(() => {
if (nodeContains(document, listbox)) {
if (listbox.isConnected) {
throw new Error('Expected listbox element to not be in the document after selecting an option');
} else {
return true;
Expand Down
7 changes: 3 additions & 4 deletions packages/@react-aria/test-utils/src/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import {act, waitFor, within} from '@testing-library/react';
import {DialogTesterOpts, UserOpts} from './types';
import {nodeContains} from '@react-aria/utils';

interface DialogOpenOpts {
/**
Expand Down Expand Up @@ -97,7 +96,7 @@ export class DialogTester {
}
});

if (dialog && document.activeElement !== this._trigger && nodeContains(dialog, document.activeElement)) {
if (dialog && document.activeElement !== this._trigger && dialog.matches(':focus-within')) {
this._dialog = dialog;
} else {
throw new Error('New modal dialog doesnt contain the active element OR the active element is still the trigger. Uncertain if the proper modal dialog was found');
Expand All @@ -114,7 +113,7 @@ export class DialogTester {
if (dialog) {
await this.user.keyboard('[Escape]');
await waitFor(() => {
if (nodeContains(document, dialog)) {
if (dialog.isConnected) {
throw new Error('Expected the dialog to not be in the document after closing it.');
} else {
this._dialog = undefined;
Expand All @@ -139,6 +138,6 @@ export class DialogTester {
* Returns the dialog if present.
*/
get dialog(): HTMLElement | null {
return this._dialog && nodeContains(document, this._dialog) ? this._dialog : null;
return this._dialog && this._dialog.isConnected ? this._dialog : null;
}
}
5 changes: 2 additions & 3 deletions packages/@react-aria/test-utils/src/gridlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import {act, within} from '@testing-library/react';
import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
import {GridListTesterOpts, GridRowActionOpts, ToggleGridRowOpts, UserOpts} from './types';
import {nodeContains} from '@react-aria/utils';

interface GridListToggleRowOpts extends ToggleGridRowOpts {}
interface GridListRowActionOpts extends GridRowActionOpts {}
Expand Down Expand Up @@ -67,13 +66,13 @@ export class GridListTester {
throw new Error('Option provided is not in the gridlist');
}

if (document.activeElement !== this._gridlist && !nodeContains(this._gridlist, document.activeElement)) {
if (document.activeElement !== this._gridlist && !this._gridlist.matches(':focus-within')) {
act(() => this._gridlist.focus());
}

if (document.activeElement === this._gridlist) {
await this.user.keyboard(`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`);
} else if (nodeContains(this._gridlist, document.activeElement) && document.activeElement!.getAttribute('role') !== 'row') {
} else if (this._gridlist.matches(':focus-within') && document.activeElement!.getAttribute('role') !== 'row') {
do {
await this.user.keyboard('[ArrowLeft]');
} while (document.activeElement!.getAttribute('role') !== 'row');
Expand Down
3 changes: 1 addition & 2 deletions packages/@react-aria/test-utils/src/listbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import {act, within} from '@testing-library/react';
import {getAltKey, getMetaKey, pressElement, triggerLongPress} from './events';
import {ListBoxTesterOpts, UserOpts} from './types';
import {nodeContains} from '@react-aria/utils';

interface ListBoxToggleOptionOpts {
/**
Expand Down Expand Up @@ -104,7 +103,7 @@ export class ListBoxTester {
throw new Error('Option provided is not in the listbox');
}

if (document.activeElement !== this._listbox && !nodeContains(this._listbox, document.activeElement)) {
if (document.activeElement !== this._listbox && !this._listbox.matches(':focus-within')) {
act(() => this._listbox.focus());
await this.user.keyboard(`${selectionOnNav === 'none' ? `[${altKey}>]` : ''}[ArrowDown]${selectionOnNav === 'none' ? `[/${altKey}]` : ''}`);
}
Expand Down
6 changes: 3 additions & 3 deletions packages/@react-aria/test-utils/src/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export class MenuTester {
return;
}

if (document.activeElement !== menu && !nodeContains(menu, document.activeElement)) {
if (document.activeElement !== menu && !menu.matches(':focus-within')) {
act(() => menu.focus());
}

Expand Down Expand Up @@ -263,7 +263,7 @@ export class MenuTester {
// close. In React 16, focus actually makes it all the way to the root menu's submenu trigger so we need check the root menu
if (this._isSubmenu) {
await waitFor(() => {
if (document.activeElement === this.trigger || nodeContains(this._rootMenu, document.activeElement)) {
if (document.activeElement === this.trigger || (this._rootMenu && this._rootMenu.matches(':focus-within'))) {
throw new Error('Expected focus after selecting an submenu option to move away from the original submenu trigger.');
} else {
return true;
Expand Down Expand Up @@ -379,7 +379,7 @@ export class MenuTester {
}
});

if (nodeContains(document, menu)) {
if (menu.isConnected) {
throw new Error('Expected the menu to not be in the document after closing it.');
}
}
Expand Down
3 changes: 1 addition & 2 deletions packages/@react-aria/test-utils/src/radiogroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import {act, within} from '@testing-library/react';
import {Direction, Orientation, RadioGroupTesterOpts, UserOpts} from './types';
import {nodeContains} from '@react-aria/utils';
import {pressElement} from './events';

interface TriggerRadioOptions {
Expand Down Expand Up @@ -95,7 +94,7 @@ export class RadioGroupTester {
throw new Error('Radio provided is not in the radio group.');
}

if (!nodeContains(this.radiogroup, document.activeElement)) {
if (!this.radiogroup.matches(':focus-within')) {
let selectedRadio = this.selectedRadio;
if (selectedRadio != null) {
act(() => selectedRadio.focus());
Expand Down
7 changes: 3 additions & 4 deletions packages/@react-aria/test-utils/src/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
*/

import {act, waitFor, within} from '@testing-library/react';
import {nodeContains} from '@react-aria/utils';
import {SelectTesterOpts, UserOpts} from './types';

interface SelectOpenOpts {
Expand Down Expand Up @@ -111,7 +110,7 @@ export class SelectTester {
}
});

if (listbox && nodeContains(document, listbox)) {
if (listbox && listbox.isConnected) {
throw new Error('Expected the select element listbox to not be in the document after closing the dropdown.');
}
}
Expand Down Expand Up @@ -192,7 +191,7 @@ export class SelectTester {
return;
}

if (document.activeElement !== listbox && !nodeContains(listbox, document.activeElement)) {
if (document.activeElement !== listbox && !listbox.matches(':focus-within')) {
act(() => listbox.focus());
}
await this.keyboardNavigateToOption({option});
Expand All @@ -215,7 +214,7 @@ export class SelectTester {
}
});

if (nodeContains(document, listbox)) {
if (listbox.isConnected) {
throw new Error('Expected select element listbox to not be in the document after selecting an option');
}
}
Expand Down
Loading