Skip to content

Commit 650ddf9

Browse files
authored
Merge branch 'main' into gridlist-section-docs
2 parents 77fb608 + a0cc05b commit 650ddf9

File tree

22 files changed

+576
-203
lines changed

22 files changed

+576
-203
lines changed

eslint.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,10 @@ export default [{
361361
},
362362

363363
languageOptions: {
364+
globals: {
365+
globalThis: "readonly",
366+
},
367+
364368
parser: tseslint.parser,
365369
ecmaVersion: 6,
366370
sourceType: "module",

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
},
1010
"packageManager": "[email protected]",
1111
"scripts": {
12-
"check-types": "tsc",
12+
"tsgo": "tsgo",
13+
"check-types": "tsgo --noEmit",
14+
"check-types:tsc": "tsc",
1315
"clean": "make clean",
1416
"clean:all": "make clean_all",
1517
"install-16": "node scripts/react-16-install-prep.mjs && yarn add react@^16.8.0 react-dom@^16.8.0 @testing-library/react@^12 @testing-library/react-hooks@^8 @testing-library/dom@^8 react-test-renderer@^16.9.0 && node scripts/oldReactSupport.mjs",
@@ -134,6 +136,7 @@
134136
"@testing-library/user-event": "patch:@testing-library/user-event@npm%3A14.6.1#~/.yarn/patches/@testing-library-user-event-npm-14.6.1-5da7e1d4e2.patch",
135137
"@types/react": "^19.0.0",
136138
"@types/react-dom": "^19.0.0",
139+
"@typescript/native-preview": "^7.0.0-dev.20251223.1",
137140
"@yarnpkg/types": "^4.0.0",
138141
"autoprefixer": "^9.6.0",
139142
"axe-playwright": "^1.1.11",

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,13 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
338338
// outside the original pressed element.
339339
// (JSDOM does not support this)
340340
if ('releasePointerCapture' in e.target) {
341-
e.target.releasePointerCapture(e.pointerId);
341+
if ('hasPointerCapture' in e.target) {
342+
if (e.target.hasPointerCapture(e.pointerId)) {
343+
e.target.releasePointerCapture(e.pointerId);
344+
}
345+
} else {
346+
e.target.releasePointerCapture(e.pointerId);
347+
}
342348
}
343349
},
344350
onContextMenu(e) {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,13 @@ export function usePress(props: PressHookProps): PressResult {
596596
// This enables onPointerLeave and onPointerEnter to fire.
597597
let target = getEventTarget(e.nativeEvent);
598598
if ('releasePointerCapture' in target) {
599-
target.releasePointerCapture(e.pointerId);
599+
if ('hasPointerCapture' in target) {
600+
if (target.hasPointerCapture(e.pointerId)) {
601+
target.releasePointerCapture(e.pointerId);
602+
}
603+
} else {
604+
(target as Element).releasePointerCapture(e.pointerId);
605+
}
600606
}
601607
}
602608

packages/@react-aria/interactions/test/usePress.test.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,8 +420,10 @@ describe('usePress', function () {
420420

421421
let el = res.getByText('test');
422422
el.releasePointerCapture = jest.fn();
423+
el.hasPointerCapture = jest.fn().mockReturnValue(true);
423424
fireEvent(el, pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
424-
expect(el.releasePointerCapture).toHaveBeenCalled();
425+
expect(el.hasPointerCapture).toHaveBeenCalledWith(1);
426+
expect(el.releasePointerCapture).toHaveBeenCalledWith(1);
425427
// react listens for pointerout and pointerover instead of pointerleave and pointerenter...
426428
fireEvent(el, pointerEvent('pointerout', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
427429
fireEvent(document, pointerEvent('pointerup', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
@@ -560,6 +562,16 @@ describe('usePress', function () {
560562
]);
561563
});
562564

565+
it('should not call releasePointerCapture when hasPointerCapture returns false', function () {
566+
let res = render(<Example />);
567+
let el = res.getByText('test');
568+
el.releasePointerCapture = jest.fn();
569+
el.hasPointerCapture = jest.fn().mockReturnValue(false);
570+
fireEvent(el, pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
571+
expect(el.hasPointerCapture).toHaveBeenCalledWith(1);
572+
expect(el.releasePointerCapture).not.toHaveBeenCalled();
573+
});
574+
563575
it('should handle pointer cancel events', function () {
564576
let events = [];
565577
let addEvent = (e) => events.push(e);
@@ -4011,8 +4023,10 @@ describe('usePress', function () {
40114023

40124024
const el = shadowRoot.getElementById('testElement');
40134025
el.releasePointerCapture = jest.fn();
4026+
el.hasPointerCapture = jest.fn().mockReturnValue(true);
40144027
fireEvent(el, pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
4015-
expect(el.releasePointerCapture).toHaveBeenCalled();
4028+
expect(el.hasPointerCapture).toHaveBeenCalledWith(1);
4029+
expect(el.releasePointerCapture).toHaveBeenCalledWith(1);
40164030
// react listens for pointerout and pointerover instead of pointerleave and pointerenter...
40174031
fireEvent(el, pointerEvent('pointerout', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
40184032
fireEvent(document, pointerEvent('pointerup', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));

packages/@react-aria/overlays/src/ariaHideOutside.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,12 @@ export function ariaHideOutside(targets: Element[], options?: AriaHideOutsideOpt
147147

148148
// If the parent element of the added nodes is not within one of the targets,
149149
// and not already inside a hidden node, hide all of the new children.
150-
if (![...visibleNodes, ...hiddenNodes].some(node => node.contains(change.target))) {
150+
if (
151+
change.target.isConnected &&
152+
![...visibleNodes, ...hiddenNodes].some((node) =>
153+
node.contains(change.target)
154+
)
155+
) {
151156
for (let node of change.addedNodes) {
152157
if (
153158
(node instanceof HTMLElement || node instanceof SVGElement) &&

packages/@react-aria/overlays/test/ariaHideOutside.test.js

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {act, render, waitFor} from '@react-spectrum/test-utils-internal';
1414
import {ariaHideOutside} from '../src';
15-
import React, {useState} from 'react';
15+
import React, {useRef, useState} from 'react';
1616

1717
describe('ariaHideOutside', function () {
1818
it('should hide everything except the provided element [button]', function () {
@@ -275,6 +275,76 @@ describe('ariaHideOutside', function () {
275275
expect(() => getByTestId('test')).not.toThrow();
276276
});
277277

278+
it('should handle when a new element is added and then reparented', async function () {
279+
280+
let Test = () => {
281+
const ref = useRef(null);
282+
const mutate = () => {
283+
let parent = document.createElement('ul');
284+
let child = document.createElement('li');
285+
ref.current.append(parent);
286+
parent.appendChild(child);
287+
parent.remove(); // this results in a mutation record for a disconnected ul with a connected li (through the new ul parent) in `addedNodes`
288+
let newParent = document.createElement('ul');
289+
newParent.appendChild(child);
290+
ref.current.append(newParent);
291+
};
292+
293+
return (
294+
<>
295+
<div data-testid="test" ref={ref}>
296+
<button onClick={mutate}>Mutate</button>
297+
</div>
298+
</>
299+
);
300+
};
301+
302+
let {queryByRole, getAllByRole, getByTestId} = render(<Test />);
303+
304+
ariaHideOutside([getByTestId('test')]);
305+
306+
queryByRole('button').click();
307+
await Promise.resolve(); // Wait for mutation observer tick
308+
309+
expect(getAllByRole('listitem')).toHaveLength(1);
310+
});
311+
312+
it('should handle when a new element is added and then reparented to a hidden container', async function () {
313+
314+
let Test = () => {
315+
const ref = useRef(null);
316+
const mutate = () => {
317+
let parent = document.createElement('ul');
318+
let child = document.createElement('li');
319+
ref.current.append(parent);
320+
parent.appendChild(child);
321+
parent.remove(); // this results in a mutation record for a disconnected ul with a connected li (through the new ul parent) in `addedNodes`
322+
let newParent = document.createElement('ul');
323+
newParent.appendChild(child);
324+
ref.current.append(newParent);
325+
};
326+
327+
return (
328+
<>
329+
<div data-testid="test">
330+
<button onClick={mutate}>Mutate</button>
331+
</div>
332+
<div data-testid="sibling" ref={ref} />
333+
</>
334+
);
335+
};
336+
337+
let {queryByRole, queryAllByRole, getByTestId} = render(<Test />);
338+
339+
ariaHideOutside([getByTestId('test')]);
340+
341+
queryByRole('button').click();
342+
await Promise.resolve(); // Wait for mutation observer tick
343+
344+
expect(queryAllByRole('listitem')).toHaveLength(0);
345+
});
346+
347+
278348
it('work when called multiple times', function () {
279349
let {getByRole, getAllByRole} = render(
280350
<>

packages/@react-aria/selection/src/useSelectableCollection.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
118118
disallowTypeAhead = false,
119119
shouldUseVirtualFocus,
120120
allowsTabNavigation = false,
121-
isVirtualized,
122121
// If no scrollRef is provided, assume the collection ref is the scrollable region
123122
scrollRef = ref,
124123
linkBehavior = 'action'
@@ -328,7 +327,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
328327
// Store the scroll position so we can restore it later.
329328
/// TODO: should this happen all the time??
330329
let scrollPos = useRef({top: 0, left: 0});
331-
useEvent(scrollRef, 'scroll', isVirtualized ? undefined : () => {
330+
useEvent(scrollRef, 'scroll', () => {
332331
scrollPos.current = {
333332
top: scrollRef.current?.scrollTop ?? 0,
334333
left: scrollRef.current?.scrollLeft ?? 0
@@ -369,7 +368,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
369368
} else {
370369
navigateToKey(manager.firstSelectedKey ?? delegate.getFirstKey?.());
371370
}
372-
} else if (!isVirtualized && scrollRef.current) {
371+
} else if (scrollRef.current) {
373372
// Restore the scroll position to what it was before.
374373
scrollRef.current.scrollTop = scrollPos.current.top;
375374
scrollRef.current.scrollLeft = scrollPos.current.left;
@@ -581,7 +580,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
581580
// This will be marshalled to either the first or last item depending on where focus came from.
582581
let tabIndex: number | undefined = undefined;
583582
if (!shouldUseVirtualFocus) {
584-
tabIndex = manager.focusedKey == null ? 0 : -1;
583+
tabIndex = manager.isFocused ? -1 : 0;
585584
}
586585

587586
let collectionId = useCollectionId(manager.collection);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ export const getOwnerDocument = (el: Element | null | undefined): Document => {
33
};
44

55
export const getOwnerWindow = (
6-
el: (Window & typeof global) | Element | null | undefined
7-
): Window & typeof global => {
6+
el: (Window & typeof globalThis) | Element | null | undefined
7+
): Window & typeof globalThis => {
88
if (el && 'window' in el && el.window === el) {
99
return el;
1010
}

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

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,23 @@ export function useViewportSize(): ViewportSize {
2626
let [size, setSize] = useState(() => isSSR ? {width: 0, height: 0} : getViewportSize());
2727

2828
useEffect(() => {
29+
let updateSize = (newSize: ViewportSize) => {
30+
setSize(size => {
31+
if (newSize.width === size.width && newSize.height === size.height) {
32+
return size;
33+
}
34+
return newSize;
35+
});
36+
};
37+
2938
// Use visualViewport api to track available height even on iOS virtual keyboard opening
3039
let onResize = () => {
3140
// Ignore updates when zoomed.
3241
if (visualViewport && visualViewport.scale > 1) {
3342
return;
3443
}
3544

36-
setSize(size => {
37-
let newSize = getViewportSize();
38-
if (newSize.width === size.width && newSize.height === size.height) {
39-
return size;
40-
}
41-
return newSize;
42-
});
45+
updateSize(getViewportSize());
4346
};
4447

4548
// When closing the keyboard, iOS does not fire the visual viewport resize event until the animation is complete.
@@ -54,18 +57,14 @@ export function useViewportSize(): ViewportSize {
5457
// Wait one frame to see if a new element gets focused.
5558
frame = requestAnimationFrame(() => {
5659
if (!document.activeElement || !willOpenKeyboard(document.activeElement)) {
57-
setSize(size => {
58-
let newSize = {width: window.innerWidth, height: window.innerHeight};
59-
if (newSize.width === size.width && newSize.height === size.height) {
60-
return size;
61-
}
62-
return newSize;
63-
});
60+
updateSize({width: window.innerWidth, height: window.innerHeight});
6461
}
6562
});
6663
}
6764
};
6865

66+
updateSize(getViewportSize());
67+
6968
window.addEventListener('blur', onBlur, true);
7069

7170
if (!visualViewport) {

0 commit comments

Comments
 (0)