Skip to content

Commit 720266f

Browse files
committed
Merge remote-tracking branch 'origin/main' into input-otp
2 parents 9e37220 + 1a30398 commit 720266f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1047
-239
lines changed

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/i18n/src/useFilter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import {useCollator} from './useCollator';
1515

1616
export interface Filter {
1717
/** Returns whether a string starts with a given substring. */
18-
startsWith(string: string, substring: string): boolean,
18+
startsWith: (string: string, substring: string) => boolean,
1919
/** Returns whether a string ends with a given substring. */
20-
endsWith(string: string, substring: string): boolean,
20+
endsWith: (string: string, substring: string) => boolean,
2121
/** Returns whether a string contains a given substring. */
22-
contains(string: string, substring: string): boolean
22+
contains: (string: string, substring: string) => boolean
2323
}
2424

2525
/**

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/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) {

packages/@react-aria/utils/test/useViewportSize.ssr.test.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {testSSR} from '@react-spectrum/test-utils-internal';
13+
import {screen, testSSR} from '@react-spectrum/test-utils-internal';
1414

1515
describe('useViewportSize SSR', () => {
1616
it('should render without errors', async () => {
@@ -25,4 +25,21 @@ describe('useViewportSize SSR', () => {
2525
<Viewport />
2626
`);
2727
});
28+
29+
it('should update dimensions after hydration', async () => {
30+
await testSSR(__filename, `
31+
import {useViewportSize} from '../src';
32+
33+
function Viewport() {
34+
let size = useViewportSize();
35+
return <div data-testid="viewport">{size.width}x{size.height}</div>;
36+
}
37+
38+
<Viewport />
39+
`, () => {
40+
expect(screen.getByTestId('viewport')).toHaveTextContent('0x0');
41+
});
42+
43+
expect(screen.getByTestId('viewport')).not.toHaveTextContent('0x0');
44+
});
2845
});

packages/@react-spectrum/s2/src/ComboBox.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export interface ComboboxStyleProps {
7979
size?: 'S' | 'M' | 'L' | 'XL'
8080
}
8181
export interface ComboBoxProps<T extends object> extends
82-
Omit<AriaComboBoxProps<T>, 'children' | 'style' | 'className' | 'defaultFilter' | 'allowsEmptyCollection' | keyof GlobalDOMAttributes>,
82+
Omit<AriaComboBoxProps<T>, 'children' | 'style' | 'className' | 'defaultFilter' | 'allowsEmptyCollection' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>,
8383
ComboboxStyleProps,
8484
StyleProps,
8585
SpectrumLabelableProps,
@@ -354,6 +354,7 @@ export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function Co
354354
return (
355355
<AriaComboBox
356356
{...comboBoxProps}
357+
isTriggerUpWhenOpen
357358
allowsEmptyCollection
358359
style={UNSAFE_style}
359360
className={UNSAFE_className + style(field(), getAllowedOverrides())({
@@ -643,9 +644,6 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps<any
643644
)}
644645
<Button
645646
ref={buttonRef}
646-
// Prevent press scale from sticking while ComboBox is open.
647-
// @ts-ignore
648-
isPressed={false}
649647
style={renderProps => pressScale(buttonRef)(renderProps)}
650648
className={renderProps => inputButton({
651649
...renderProps,

0 commit comments

Comments
 (0)