Skip to content

Commit dfef0f5

Browse files
committed
Merge branch 'main' of github.com:adobe/react-spectrum into docs_fixes
2 parents 9214ec1 + f36100b commit dfef0f5

File tree

27 files changed

+375
-107
lines changed

27 files changed

+375
-107
lines changed

packages/@internationalized/date/src/queries.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,9 @@ function getWeekStart(locale: string): number {
268268
}
269269
let region = getRegion(locale);
270270
if (locale.includes('-fw-')) {
271-
let day = locale.split('-fw-')[1];
271+
// pull the value for the attribute fw from strings such as en-US-u-ca-iso8601-fw-tue or en-US-u-ca-iso8601-fw-mon-nu-thai
272+
// where the fw attribute could be followed by another unicode locale extension or not
273+
let day = locale.split('-fw-')[1].split('-')[0];
272274
if (day === 'mon') {
273275
weekInfo = {firstDay: 1};
274276
} else if (day === 'tue') {
@@ -284,7 +286,7 @@ function getWeekStart(locale: string): number {
284286
} else {
285287
weekInfo = {firstDay: 0};
286288
}
287-
} else if (locale.includes('u-ca-iso8601')) {
289+
} else if (locale.includes('-ca-iso8601')) {
288290
weekInfo = {firstDay: 1};
289291
} else {
290292
weekInfo = {firstDay: region ? weekStartData[region] || 0 : 0};

packages/@internationalized/date/tests/queries.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,11 @@ describe('queries', function () {
281281

282282
// override first day of week
283283
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US-u-ca-iso8601-fw-tue')).toEqual(new CalendarDate(2021, 8, 3));
284+
285+
// override applied if extension appears in the middle of other extensions
286+
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US-u-nu-thai-ca-iso8601')).toEqual(new CalendarDate(2021, 8, 2));
287+
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US-u-nu-thai-ca-iso8601-fw-tue')).toEqual(new CalendarDate(2021, 8, 3));
288+
expect(startOfWeek(new CalendarDate(2021, 8, 4), 'en-US-u-ca-iso8601-fw-tue-nu-thai')).toEqual(new CalendarDate(2021, 8, 3));
284289
});
285290
});
286291

packages/@react-aria/autocomplete/src/useAutocomplete.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {AriaLabelingProps, BaseEvent, DOMProps, FocusableElement, FocusEvents, KeyboardEvents, Node, RefObject, ValueBase} from '@react-types/shared';
1414
import {AriaTextFieldProps} from '@react-aria/textfield';
1515
import {AutocompleteProps, AutocompleteState} from '@react-stately/autocomplete';
16-
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useLabels, useObjectRef, useSlotId} from '@react-aria/utils';
16+
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, getOwnerWindow, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useLabels, useObjectRef, useSlotId} from '@react-aria/utils';
1717
import {dispatchVirtualBlur, dispatchVirtualFocus, getVirtuallyFocusedElement, moveVirtualFocus} from '@react-aria/focus';
1818
import {getInteractionModality} from '@react-aria/interactions';
1919
// @ts-ignore
@@ -106,6 +106,9 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
106106
// Ensure input is focused if the user clicks on the collection directly.
107107
if (!e.isTrusted && shouldUseVirtualFocus && inputRef.current && getActiveElement(getOwnerDocument(inputRef.current)) !== inputRef.current) {
108108
inputRef.current.focus();
109+
if (inputRef.current instanceof getOwnerWindow(inputRef.current).HTMLInputElement) {
110+
inputRef.current.select();
111+
}
109112
}
110113

111114
let target = e.target as Element | null;

packages/@react-aria/checkbox/docs/useCheckbox.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ function Checkbox(props) {
8585
let {children} = props;
8686
let state = useToggleState(props);
8787
let ref = React.useRef(null);
88-
let {inputProps} = useCheckbox(props, state, ref);
88+
let {inputProps, labelProps} = useCheckbox(props, state, ref);
8989

9090
return (
91-
<label style={{display: 'block'}}>
91+
<label {...labelProps} style={{display: 'block'}}>
9292
<input {...inputProps} ref={ref} />
9393
{children}
9494
</label>
@@ -120,12 +120,12 @@ import {mergeProps} from '@react-aria/utils';
120120
function Checkbox(props) {
121121
let state = useToggleState(props);
122122
let ref = React.useRef(null);
123-
let {inputProps} = useCheckbox(props, state, ref);
123+
let {inputProps, labelProps} = useCheckbox(props, state, ref);
124124
let {isFocusVisible, focusProps} = useFocusRing();
125125
let isSelected = state.isSelected && !props.isIndeterminate;
126126

127127
return (
128-
<label style={{display: 'flex', alignItems: 'center', opacity: props.isDisabled ? 0.4 : 1}}>
128+
<label {...labelProps} style={{display: 'flex', alignItems: 'center', opacity: props.isDisabled ? 0.4 : 1}}>
129129
<VisuallyHidden>
130130
<input {...mergeProps(inputProps, focusProps)} ref={ref} />
131131
</VisuallyHidden>

packages/@react-aria/checkbox/docs/useCheckboxGroup.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,14 @@ function Checkbox(props) {
127127
let {children} = props;
128128
let state = React.useContext(CheckboxGroupContext);
129129
let ref = React.useRef(null);
130-
let {inputProps} = useCheckboxGroupItem(props, state, ref);
130+
let {inputProps, labelProps} = useCheckboxGroupItem(props, state, ref);
131131

132132
let isDisabled = state.isDisabled || props.isDisabled;
133133
let isSelected = state.isSelected(props.value);
134134

135135
return (
136136
<label
137+
{...labelProps}
137138
style={{
138139
display: 'block',
139140
color: (isDisabled && 'var(--gray)') || (isSelected && 'var(--blue)'),

packages/@react-aria/checkbox/src/useCheckbox.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {AriaCheckboxProps} from '@react-types/checkbox';
14-
import {InputHTMLAttributes, LabelHTMLAttributes, useEffect} from 'react';
14+
import {InputHTMLAttributes, LabelHTMLAttributes, useEffect, useMemo} from 'react';
1515
import {mergeProps} from '@react-aria/utils';
1616
import {privateValidationStateProp, useFormValidationState} from '@react-stately/form';
1717
import {RefObject, ValidationResult} from '@react-types/shared';
@@ -69,17 +69,24 @@ export function useCheckbox(props: AriaCheckboxProps, state: ToggleState, inputR
6969
onPress() {
7070
// @ts-expect-error
7171
let {[privateValidationStateProp]: groupValidationState} = props;
72-
72+
7373
let {commitValidation} = groupValidationState
7474
? groupValidationState
7575
: validationState;
76-
76+
7777
commitValidation();
7878
}
7979
});
8080

8181
return {
82-
labelProps: mergeProps(labelProps, pressProps),
82+
labelProps: mergeProps(
83+
labelProps,
84+
pressProps,
85+
useMemo(() => ({
86+
// Prevent label from being focused when mouse down on it.
87+
// Note, this does not prevent the input from being focused in the `click` event.
88+
onMouseDown: e => e.preventDefault()
89+
}), [])),
8390
inputProps: {
8491
...inputProps,
8592
checked: isSelected,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2025 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {action} from '@storybook/addon-actions';
14+
import {AriaCheckboxProps, useCheckbox} from '../';
15+
import React from 'react';
16+
import {StoryObj} from '@storybook/react';
17+
import {useToggleState} from '@react-stately/toggle';
18+
19+
export default {
20+
title: 'useCheckbox'
21+
};
22+
23+
export type CheckboxStory = StoryObj<typeof Checkbox>;
24+
25+
function Checkbox(props: AriaCheckboxProps) {
26+
let {children} = props;
27+
let state = useToggleState(props);
28+
let ref = React.useRef(null);
29+
let {inputProps, labelProps} = useCheckbox(props, state, ref);
30+
31+
return (
32+
<>
33+
<label {...labelProps} style={{display: 'block'}}>
34+
{children}
35+
</label>
36+
<input {...inputProps} ref={ref} />
37+
</>
38+
);
39+
}
40+
41+
export const Example: CheckboxStory = {
42+
render: (args) => <Checkbox {...args}>Unsubscribe</Checkbox>,
43+
args: {
44+
onFocus: action('onFocus'),
45+
onBlur: action('onBlur')
46+
}
47+
};

packages/@react-aria/disclosure/src/useDisclosure.ts

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,13 @@ export interface DisclosureAria {
4141
* @param state - State for the disclosure, as returned by `useDisclosureState`.
4242
* @param ref - A ref for the disclosure panel.
4343
*/
44-
export function useDisclosure(props: AriaDisclosureProps, state: DisclosureState, ref: RefObject<Element | null>): DisclosureAria {
44+
export function useDisclosure(props: AriaDisclosureProps, state: DisclosureState, ref: RefObject<HTMLElement | null>): DisclosureAria {
4545
let {
4646
isDisabled
4747
} = props;
4848
let triggerId = useId();
4949
let panelId = useId();
5050
let isSSR = useIsSSR();
51-
let supportsBeforeMatch = !isSSR && 'onbeforematch' in document.body;
5251

5352
let raf = useRef<number | null>(null);
5453

@@ -66,22 +65,64 @@ export function useDisclosure(props: AriaDisclosureProps, state: DisclosureState
6665
}, [ref, state]);
6766

6867
// @ts-ignore https://github.com/facebook/react/pull/24741
69-
useEvent(ref, 'beforematch', supportsBeforeMatch ? handleBeforeMatch : null);
68+
useEvent(ref, 'beforematch', handleBeforeMatch);
7069

70+
let isExpandedRef = useRef<boolean | null>(null);
7171
useLayoutEffect(() => {
7272
// Cancel any pending RAF to prevent stale updates
7373
if (raf.current) {
7474
cancelAnimationFrame(raf.current);
7575
}
76-
// Until React supports hidden="until-found": https://github.com/facebook/react/pull/24741
77-
if (supportsBeforeMatch && ref.current && !isDisabled) {
78-
if (state.isExpanded) {
79-
ref.current.removeAttribute('hidden');
80-
} else {
81-
ref.current.setAttribute('hidden', 'until-found');
76+
if (ref.current && !isDisabled && !isSSR) {
77+
let panel = ref.current;
78+
79+
if (isExpandedRef.current == null || typeof panel.getAnimations !== 'function') {
80+
// On initial render (and in tests), set attributes without animation.
81+
if (state.isExpanded) {
82+
panel.removeAttribute('hidden');
83+
panel.style.setProperty('--disclosure-panel-width', 'auto');
84+
panel.style.setProperty('--disclosure-panel-height', 'auto');
85+
} else {
86+
panel.setAttribute('hidden', 'until-found');
87+
panel.style.setProperty('--disclosure-panel-width', '0px');
88+
panel.style.setProperty('--disclosure-panel-height', '0px');
89+
}
90+
} else if (state.isExpanded !== isExpandedRef.current) {
91+
if (state.isExpanded) {
92+
panel.removeAttribute('hidden');
93+
94+
// Set the width and height as pixels so they can be animated.
95+
panel.style.setProperty('--disclosure-panel-width', panel.scrollWidth + 'px');
96+
panel.style.setProperty('--disclosure-panel-height', panel.scrollHeight + 'px');
97+
98+
Promise.all(panel.getAnimations().map(a => a.finished))
99+
.then(() => {
100+
// After the animations complete, switch back to auto so the content can resize.
101+
panel.style.setProperty('--disclosure-panel-width', 'auto');
102+
panel.style.setProperty('--disclosure-panel-height', 'auto');
103+
})
104+
.catch(() => {});
105+
} else {
106+
panel.style.setProperty('--disclosure-panel-width', panel.scrollWidth + 'px');
107+
panel.style.setProperty('--disclosure-panel-height', panel.scrollHeight + 'px');
108+
109+
// Force style re-calculation to trigger animations.
110+
window.getComputedStyle(panel).height;
111+
112+
// Animate to zero size.
113+
panel.style.setProperty('--disclosure-panel-width', '0px');
114+
panel.style.setProperty('--disclosure-panel-height', '0px');
115+
116+
// Wait for animations to apply the hidden attribute.
117+
Promise.all(panel.getAnimations().map(a => a.finished))
118+
.then(() => panel.setAttribute('hidden', 'until-found'))
119+
.catch(() => {});
120+
}
82121
}
122+
123+
isExpandedRef.current = state.isExpanded;
83124
}
84-
}, [isDisabled, ref, state.isExpanded, supportsBeforeMatch]);
125+
}, [isDisabled, ref, state.isExpanded, isSSR]);
85126

86127
useEffect(() => {
87128
return () => {
@@ -114,7 +155,7 @@ export function useDisclosure(props: AriaDisclosureProps, state: DisclosureState
114155
role: 'group',
115156
'aria-labelledby': triggerId,
116157
'aria-hidden': !state.isExpanded,
117-
hidden: supportsBeforeMatch ? true : !state.isExpanded
158+
hidden: isSSR ? !state.isExpanded : undefined
118159
}
119160
};
120161
}

packages/@react-aria/disclosure/test/useDisclosure.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ describe('useDisclosure', () => {
3131
let {buttonProps, panelProps} = result.current;
3232

3333
expect(buttonProps['aria-expanded']).toBe(false);
34-
expect(panelProps.hidden).toBe(true);
3534
expect(panelProps['aria-hidden']).toBe(true);
3635
});
3736

@@ -44,7 +43,7 @@ describe('useDisclosure', () => {
4443
let {buttonProps, panelProps} = result.current;
4544

4645
expect(buttonProps['aria-expanded']).toBe(true);
47-
expect(panelProps.hidden).toBe(false);
46+
expect(panelProps['aria-hidden']).toBe(false);
4847
});
4948

5049
it('should handle expanding on press event (with mouse)', () => {

packages/@react-aria/focus/src/FocusScope.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
getActiveElement,
1616
getEventTarget,
1717
getOwnerDocument,
18+
getOwnerWindow,
1819
isAndroid,
1920
isChrome,
2021
isFocusable,
@@ -371,6 +372,10 @@ function useFocusContainment(scopeRef: RefObject<Element[] | null>, contain?: bo
371372
// restore focus to the previously focused node or the first tabbable element in the active scope.
372373
if (focusedNode.current) {
373374
focusedNode.current.focus();
375+
376+
if (focusedNode.current instanceof getOwnerWindow(focusedNode.current).HTMLInputElement) {
377+
focusedNode.current.select();
378+
}
374379
} else if (activeScope && activeScope.current) {
375380
focusFirstInScope(activeScope.current);
376381
}
@@ -399,6 +404,9 @@ function useFocusContainment(scopeRef: RefObject<Element[] | null>, contain?: bo
399404
if (target && target.isConnected) {
400405
focusedNode.current = target;
401406
focusedNode.current?.focus();
407+
if (focusedNode.current instanceof getOwnerWindow(focusedNode.current).HTMLInputElement) {
408+
focusedNode.current.select();
409+
}
402410
} else if (activeScope.current) {
403411
focusFirstInScope(activeScope.current);
404412
}
@@ -486,6 +494,9 @@ function focusElement(element: FocusableElement | null, scroll = false) {
486494
} else if (element != null) {
487495
try {
488496
element.focus();
497+
if (element instanceof getOwnerWindow(element).HTMLInputElement) {
498+
element.select();
499+
}
489500
} catch {
490501
// ignore
491502
}

0 commit comments

Comments
 (0)