From e1058df6825c2f866af959e25707202a99cf6710 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 21:57:09 +0900 Subject: [PATCH 01/11] feat: Add `focusableRef` support to `DateInput` --- .../react-aria-components/src/DateField.tsx | 25 ++++++-- .../test/DateField.test.js | 57 +++++++++++++++++++ .../test/DatePicker.test.js | 46 +++++++++++++++ 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/packages/react-aria-components/src/DateField.tsx b/packages/react-aria-components/src/DateField.tsx index f08a593ce85..a000b8e2a0b 100644 --- a/packages/react-aria-components/src/DateField.tsx +++ b/packages/react-aria-components/src/DateField.tsx @@ -77,6 +77,7 @@ export const DateFieldContext = createContext, export const TimeFieldContext = createContext, HTMLDivElement>>(null); export const DateFieldStateContext = createContext(null); export const TimeFieldStateContext = createContext(null); +const DateInputFocusableRefContext = createContext | null>(null); /** * A date field allows users to enter and edit date and time values using a keyboard. @@ -255,6 +256,11 @@ export interface DateInputProps extends SlotProps, StyleRenderProps, + /** + * A ref for the first focusable date segment. Useful for programmatically focusing the input, + * for example when using with react-hook-form. + */ + focusableRef?: ForwardedRef, children: (segment: IDateSegment) => ReactElement } @@ -296,15 +302,18 @@ const DateInputStandalone = forwardRef((props: DateInputProps, ref: ForwardedRef }); const DateInputInner = forwardRef((props: DateInputProps, ref: ForwardedRef) => { - let {className, children} = props; + let {className, children, focusableRef, ...otherProps} = props; let dateFieldState = useContext(DateFieldStateContext); let timeFieldState = useContext(TimeFieldStateContext); let state = dateFieldState ?? timeFieldState!; return ( - <> + cloneElement(children(segment), {key: i}))} - + ); }); @@ -378,7 +387,13 @@ export const DateSegment = /*#__PURE__*/ (forwardRef as forwardRefType)(function let dateFieldState = useContext(DateFieldStateContext); let timeFieldState = useContext(TimeFieldStateContext); let state = dateFieldState ?? timeFieldState!; - let domRef = useObjectRef(ref); + let focusableRef = useContext(DateInputFocusableRefContext); + + // If this is the first editable segment and focusableRef is provided, use it + let isFirstEditableSegment = segment.isEditable && + segment.type === state.segments.find(s => s.isEditable)?.type; + + let domRef = useObjectRef((isFirstEditableSegment && focusableRef) ? focusableRef : ref); let {segmentProps} = useDateSegment(segment, state, domRef); let {focusProps, isFocused, isFocusVisible} = useFocusRing(); let {hoverProps, isHovered} = useHover({...otherProps, isDisabled: state.isDisabled || segment.type === 'literal'}); diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index 6f9830aba14..e2bca35252c 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -478,4 +478,61 @@ describe('DateField', () => { expect(segements[1]).toHaveTextContent('dd'); expect(segements[2]).toHaveTextContent('yyyy'); }); + + it('should support focusableRef', () => { + let focusableRef = React.createRef(); + let {getAllByRole} = render( + + + + {segment => } + + + ); + + let segments = getAllByRole('spinbutton'); + // focusableRef should point to the first editable segment (month in en-US) + expect(focusableRef.current).toBe(segments[0]); + }); + + it('should focus first segment when calling focus() on focusableRef', () => { + let focusableRef = React.createRef(); + let {getAllByRole} = render( + + + + {segment => } + + + ); + + let segments = getAllByRole('spinbutton'); + expect(document.activeElement).not.toBe(segments[0]); + + // Programmatically focus the first segment + act(() => { + focusableRef.current.focus(); + }); + + expect(document.activeElement).toBe(segments[0]); + }); + + it('should support focusableRef with different locales', () => { + let focusableRef = React.createRef(); + let {getAllByRole} = render( + + + + + {segment => } + + + + ); + + let segments = getAllByRole('spinbutton'); + // In zh-CN, year comes first + expect(focusableRef.current).toBe(segments[0]); + expect(segments[0]).toHaveAttribute('data-type', 'year'); + }); }); diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index 5298849b1a5..d86624257cb 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -334,4 +334,50 @@ describe('DatePicker', () => { let input = group.querySelector('.react-aria-DateInput'); expect(input).toHaveTextContent('5/30/2000'); }); + + it('should support focusableRef on DateInput', () => { + let focusableRef = React.createRef(); + let {getByRole} = render( + + + + + {(segment) => } + + + + + ); + + let group = getByRole('group'); + let segments = within(group).getAllByRole('spinbutton'); + // focusableRef should point to the first editable segment + expect(focusableRef.current).toBe(segments[0]); + }); + + it('should focus first segment when calling focus() on focusableRef', () => { + let focusableRef = React.createRef(); + let {getByRole} = render( + + + + + {(segment) => } + + + + + ); + + let group = getByRole('group'); + let segments = within(group).getAllByRole('spinbutton'); + expect(document.activeElement).not.toBe(segments[0]); + + // Programmatically focus the first segment + act(() => { + focusableRef.current.focus(); + }); + + expect(document.activeElement).toBe(segments[0]); + }); }); From 18aaa6fa0f95baa6eee52cee26c84b3a34efa5b0 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Sat, 15 Nov 2025 00:41:40 +0900 Subject: [PATCH 02/11] test: remove redundant focusableRef tests in DatePicker and DateField --- .../test/DateField.test.js | 35 ------------------- .../test/DatePicker.test.js | 20 ----------- 2 files changed, 55 deletions(-) diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index e2bca35252c..87150ddd188 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -491,23 +491,7 @@ describe('DateField', () => { ); let segments = getAllByRole('spinbutton'); - // focusableRef should point to the first editable segment (month in en-US) expect(focusableRef.current).toBe(segments[0]); - }); - - it('should focus first segment when calling focus() on focusableRef', () => { - let focusableRef = React.createRef(); - let {getAllByRole} = render( - - - - {segment => } - - - ); - - let segments = getAllByRole('spinbutton'); - expect(document.activeElement).not.toBe(segments[0]); // Programmatically focus the first segment act(() => { @@ -516,23 +500,4 @@ describe('DateField', () => { expect(document.activeElement).toBe(segments[0]); }); - - it('should support focusableRef with different locales', () => { - let focusableRef = React.createRef(); - let {getAllByRole} = render( - - - - - {segment => } - - - - ); - - let segments = getAllByRole('spinbutton'); - // In zh-CN, year comes first - expect(focusableRef.current).toBe(segments[0]); - expect(segments[0]).toHaveAttribute('data-type', 'year'); - }); }); diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index d86624257cb..664c5e3803f 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -351,27 +351,7 @@ describe('DatePicker', () => { let group = getByRole('group'); let segments = within(group).getAllByRole('spinbutton'); - // focusableRef should point to the first editable segment expect(focusableRef.current).toBe(segments[0]); - }); - - it('should focus first segment when calling focus() on focusableRef', () => { - let focusableRef = React.createRef(); - let {getByRole} = render( - - - - - {(segment) => } - - - - - ); - - let group = getByRole('group'); - let segments = within(group).getAllByRole('spinbutton'); - expect(document.activeElement).not.toBe(segments[0]); // Programmatically focus the first segment act(() => { From 3c622a6b8416d139a30ef89ac7cedb0bde96ce6e Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Sat, 15 Nov 2025 01:37:14 +0900 Subject: [PATCH 03/11] test: clean up redundant comments in DatePicker and DateField tests --- packages/react-aria-components/test/DateField.test.js | 1 - packages/react-aria-components/test/DatePicker.test.js | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index 87150ddd188..667a7da2f4a 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -493,7 +493,6 @@ describe('DateField', () => { let segments = getAllByRole('spinbutton'); expect(focusableRef.current).toBe(segments[0]); - // Programmatically focus the first segment act(() => { focusableRef.current.focus(); }); diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index 664c5e3803f..ab367eab685 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -353,7 +353,6 @@ describe('DatePicker', () => { let segments = within(group).getAllByRole('spinbutton'); expect(focusableRef.current).toBe(segments[0]); - // Programmatically focus the first segment act(() => { focusableRef.current.focus(); }); From 0e2e8b6c7911fed8cec15a6dfda63a282050d07f Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Sat, 15 Nov 2025 01:41:49 +0900 Subject: [PATCH 04/11] refactor: simplify focusableRef comment in DateField component --- packages/react-aria-components/src/DateField.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react-aria-components/src/DateField.tsx b/packages/react-aria-components/src/DateField.tsx index a000b8e2a0b..29234fd8cb4 100644 --- a/packages/react-aria-components/src/DateField.tsx +++ b/packages/react-aria-components/src/DateField.tsx @@ -257,8 +257,7 @@ export interface DateInputProps extends SlotProps, StyleRenderProps, /** - * A ref for the first focusable date segment. Useful for programmatically focusing the input, - * for example when using with react-hook-form. + * A ref for the first focusable date segment. */ focusableRef?: ForwardedRef, children: (segment: IDateSegment) => ReactElement From 4e8d6fad79d8f8b2ba704449cd5bf67c13373ccc Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 21:57:09 +0900 Subject: [PATCH 05/11] feat: Add `focusableRef` support to `DateInput` --- .../react-aria-components/src/DateField.tsx | 25 ++++++-- .../test/DateField.test.js | 57 +++++++++++++++++++ .../test/DatePicker.test.js | 46 +++++++++++++++ 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/packages/react-aria-components/src/DateField.tsx b/packages/react-aria-components/src/DateField.tsx index f08a593ce85..a000b8e2a0b 100644 --- a/packages/react-aria-components/src/DateField.tsx +++ b/packages/react-aria-components/src/DateField.tsx @@ -77,6 +77,7 @@ export const DateFieldContext = createContext, export const TimeFieldContext = createContext, HTMLDivElement>>(null); export const DateFieldStateContext = createContext(null); export const TimeFieldStateContext = createContext(null); +const DateInputFocusableRefContext = createContext | null>(null); /** * A date field allows users to enter and edit date and time values using a keyboard. @@ -255,6 +256,11 @@ export interface DateInputProps extends SlotProps, StyleRenderProps, + /** + * A ref for the first focusable date segment. Useful for programmatically focusing the input, + * for example when using with react-hook-form. + */ + focusableRef?: ForwardedRef, children: (segment: IDateSegment) => ReactElement } @@ -296,15 +302,18 @@ const DateInputStandalone = forwardRef((props: DateInputProps, ref: ForwardedRef }); const DateInputInner = forwardRef((props: DateInputProps, ref: ForwardedRef) => { - let {className, children} = props; + let {className, children, focusableRef, ...otherProps} = props; let dateFieldState = useContext(DateFieldStateContext); let timeFieldState = useContext(TimeFieldStateContext); let state = dateFieldState ?? timeFieldState!; return ( - <> + cloneElement(children(segment), {key: i}))} - + ); }); @@ -378,7 +387,13 @@ export const DateSegment = /*#__PURE__*/ (forwardRef as forwardRefType)(function let dateFieldState = useContext(DateFieldStateContext); let timeFieldState = useContext(TimeFieldStateContext); let state = dateFieldState ?? timeFieldState!; - let domRef = useObjectRef(ref); + let focusableRef = useContext(DateInputFocusableRefContext); + + // If this is the first editable segment and focusableRef is provided, use it + let isFirstEditableSegment = segment.isEditable && + segment.type === state.segments.find(s => s.isEditable)?.type; + + let domRef = useObjectRef((isFirstEditableSegment && focusableRef) ? focusableRef : ref); let {segmentProps} = useDateSegment(segment, state, domRef); let {focusProps, isFocused, isFocusVisible} = useFocusRing(); let {hoverProps, isHovered} = useHover({...otherProps, isDisabled: state.isDisabled || segment.type === 'literal'}); diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index 6f9830aba14..e2bca35252c 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -478,4 +478,61 @@ describe('DateField', () => { expect(segements[1]).toHaveTextContent('dd'); expect(segements[2]).toHaveTextContent('yyyy'); }); + + it('should support focusableRef', () => { + let focusableRef = React.createRef(); + let {getAllByRole} = render( + + + + {segment => } + + + ); + + let segments = getAllByRole('spinbutton'); + // focusableRef should point to the first editable segment (month in en-US) + expect(focusableRef.current).toBe(segments[0]); + }); + + it('should focus first segment when calling focus() on focusableRef', () => { + let focusableRef = React.createRef(); + let {getAllByRole} = render( + + + + {segment => } + + + ); + + let segments = getAllByRole('spinbutton'); + expect(document.activeElement).not.toBe(segments[0]); + + // Programmatically focus the first segment + act(() => { + focusableRef.current.focus(); + }); + + expect(document.activeElement).toBe(segments[0]); + }); + + it('should support focusableRef with different locales', () => { + let focusableRef = React.createRef(); + let {getAllByRole} = render( + + + + + {segment => } + + + + ); + + let segments = getAllByRole('spinbutton'); + // In zh-CN, year comes first + expect(focusableRef.current).toBe(segments[0]); + expect(segments[0]).toHaveAttribute('data-type', 'year'); + }); }); diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index 5298849b1a5..d86624257cb 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -334,4 +334,50 @@ describe('DatePicker', () => { let input = group.querySelector('.react-aria-DateInput'); expect(input).toHaveTextContent('5/30/2000'); }); + + it('should support focusableRef on DateInput', () => { + let focusableRef = React.createRef(); + let {getByRole} = render( + + + + + {(segment) => } + + + + + ); + + let group = getByRole('group'); + let segments = within(group).getAllByRole('spinbutton'); + // focusableRef should point to the first editable segment + expect(focusableRef.current).toBe(segments[0]); + }); + + it('should focus first segment when calling focus() on focusableRef', () => { + let focusableRef = React.createRef(); + let {getByRole} = render( + + + + + {(segment) => } + + + + + ); + + let group = getByRole('group'); + let segments = within(group).getAllByRole('spinbutton'); + expect(document.activeElement).not.toBe(segments[0]); + + // Programmatically focus the first segment + act(() => { + focusableRef.current.focus(); + }); + + expect(document.activeElement).toBe(segments[0]); + }); }); From 90fbb26646e1a2f9c31f4f1bf5e2839f263be2ae Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Sat, 15 Nov 2025 00:41:40 +0900 Subject: [PATCH 06/11] test: remove redundant focusableRef tests in DatePicker and DateField --- .../test/DateField.test.js | 35 ------------------- .../test/DatePicker.test.js | 20 ----------- 2 files changed, 55 deletions(-) diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index e2bca35252c..87150ddd188 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -491,23 +491,7 @@ describe('DateField', () => { ); let segments = getAllByRole('spinbutton'); - // focusableRef should point to the first editable segment (month in en-US) expect(focusableRef.current).toBe(segments[0]); - }); - - it('should focus first segment when calling focus() on focusableRef', () => { - let focusableRef = React.createRef(); - let {getAllByRole} = render( - - - - {segment => } - - - ); - - let segments = getAllByRole('spinbutton'); - expect(document.activeElement).not.toBe(segments[0]); // Programmatically focus the first segment act(() => { @@ -516,23 +500,4 @@ describe('DateField', () => { expect(document.activeElement).toBe(segments[0]); }); - - it('should support focusableRef with different locales', () => { - let focusableRef = React.createRef(); - let {getAllByRole} = render( - - - - - {segment => } - - - - ); - - let segments = getAllByRole('spinbutton'); - // In zh-CN, year comes first - expect(focusableRef.current).toBe(segments[0]); - expect(segments[0]).toHaveAttribute('data-type', 'year'); - }); }); diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index d86624257cb..664c5e3803f 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -351,27 +351,7 @@ describe('DatePicker', () => { let group = getByRole('group'); let segments = within(group).getAllByRole('spinbutton'); - // focusableRef should point to the first editable segment expect(focusableRef.current).toBe(segments[0]); - }); - - it('should focus first segment when calling focus() on focusableRef', () => { - let focusableRef = React.createRef(); - let {getByRole} = render( - - - - - {(segment) => } - - - - - ); - - let group = getByRole('group'); - let segments = within(group).getAllByRole('spinbutton'); - expect(document.activeElement).not.toBe(segments[0]); // Programmatically focus the first segment act(() => { From fee41cf5090f5a7ababd275bb13650cef8dccc95 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Sat, 15 Nov 2025 01:37:14 +0900 Subject: [PATCH 07/11] test: clean up redundant comments in DatePicker and DateField tests --- packages/react-aria-components/test/DateField.test.js | 1 - packages/react-aria-components/test/DatePicker.test.js | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index 87150ddd188..667a7da2f4a 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -493,7 +493,6 @@ describe('DateField', () => { let segments = getAllByRole('spinbutton'); expect(focusableRef.current).toBe(segments[0]); - // Programmatically focus the first segment act(() => { focusableRef.current.focus(); }); diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index 664c5e3803f..ab367eab685 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -353,7 +353,6 @@ describe('DatePicker', () => { let segments = within(group).getAllByRole('spinbutton'); expect(focusableRef.current).toBe(segments[0]); - // Programmatically focus the first segment act(() => { focusableRef.current.focus(); }); From 9cddf2dc9fd1af9cf130f39377fef3ac5657b022 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Sat, 15 Nov 2025 01:41:49 +0900 Subject: [PATCH 08/11] refactor: simplify focusableRef comment in DateField component --- packages/react-aria-components/src/DateField.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react-aria-components/src/DateField.tsx b/packages/react-aria-components/src/DateField.tsx index a000b8e2a0b..29234fd8cb4 100644 --- a/packages/react-aria-components/src/DateField.tsx +++ b/packages/react-aria-components/src/DateField.tsx @@ -257,8 +257,7 @@ export interface DateInputProps extends SlotProps, StyleRenderProps, /** - * A ref for the first focusable date segment. Useful for programmatically focusing the input, - * for example when using with react-hook-form. + * A ref for the first focusable date segment. */ focusableRef?: ForwardedRef, children: (segment: IDateSegment) => ReactElement From 60f715f4162c80a986c274e7bf6cccced425926f Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Wed, 17 Dec 2025 17:29:01 +1100 Subject: [PATCH 09/11] Add a DateRangePicker test --- .../test/DateRangePicker.test.js | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/react-aria-components/test/DateRangePicker.test.js b/packages/react-aria-components/test/DateRangePicker.test.js index e6562776338..c0d31565dca 100644 --- a/packages/react-aria-components/test/DateRangePicker.test.js +++ b/packages/react-aria-components/test/DateRangePicker.test.js @@ -333,6 +333,61 @@ describe('DateRangePicker', () => { } }); + it('should support focusableRef', () => { + let focusableRef = React.createRef(); + let secondFocusableRef = React.createRef(); + let {getAllByRole} = render( + + + + + {(segment) => } + + + + {(segment) => } + + + + Description + Error + + + + Yo + + test + +
+ + + +
+ + {(date) => } + +
+
+
+
+ ); + + let segments = getAllByRole('spinbutton'); + expect(focusableRef.current).toBe(segments[0]); + + act(() => { + focusableRef.current.focus(); + }); + + expect(document.activeElement).toBe(segments[0]); + + act(() => { + secondFocusableRef.current.focus(); + }); + + expect(document.activeElement).toBe(segments[3]); + }); + it('should clear contexts inside popover', async () => { let {getByRole, getByTestId} = render( From 4f9720a32b1bedae83c45edfbf1f15e1d2e6cd45 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Wed, 17 Dec 2025 17:46:30 +1100 Subject: [PATCH 10/11] expand test for ref merging --- .../test/DateRangePicker.test.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/react-aria-components/test/DateRangePicker.test.js b/packages/react-aria-components/test/DateRangePicker.test.js index c0d31565dca..70b3a6edbf1 100644 --- a/packages/react-aria-components/test/DateRangePicker.test.js +++ b/packages/react-aria-components/test/DateRangePicker.test.js @@ -336,12 +336,18 @@ describe('DateRangePicker', () => { it('should support focusableRef', () => { let focusableRef = React.createRef(); let secondFocusableRef = React.createRef(); + let segmentRef = React.createRef(); let {getAllByRole} = render( - {(segment) => } + {(segment) => { + if (segment.type === 'month') { + return ; + } + return ; + }} @@ -372,8 +378,10 @@ describe('DateRangePicker', () => { ); + expect(segmentRef.current).toBe(focusableRef.current); + let segments = getAllByRole('spinbutton'); - expect(focusableRef.current).toBe(segments[0]); + expect(segmentRef.current).toBe(segments[0]); act(() => { focusableRef.current.focus(); From 9287d6b298067f022ae6442299650194a3663dc3 Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Thu, 18 Dec 2025 02:07:46 +0900 Subject: [PATCH 11/11] fix: support concurrent `focusableRef` and `ref` in the `DateSegment` component --- .../react-aria-components/src/DateField.tsx | 4 ++-- .../test/DateField.test.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/react-aria-components/src/DateField.tsx b/packages/react-aria-components/src/DateField.tsx index 15cdfd21dce..6f36d3c38bf 100644 --- a/packages/react-aria-components/src/DateField.tsx +++ b/packages/react-aria-components/src/DateField.tsx @@ -27,7 +27,7 @@ import { import {createCalendar} from '@internationalized/date'; import {DateFieldState, DateSegmentType, DateSegment as IDateSegment, TimeFieldState, useDateFieldState, useTimeFieldState} from 'react-stately'; import {FieldErrorContext} from './FieldError'; -import {filterDOMProps, useObjectRef} from '@react-aria/utils'; +import {filterDOMProps, mergeRefs, useObjectRef} from '@react-aria/utils'; import {FormContext} from './Form'; import {forwardRefType, GlobalDOMAttributes} from '@react-types/shared'; import {Group, GroupContext} from './Group'; @@ -392,7 +392,7 @@ export const DateSegment = /*#__PURE__*/ (forwardRef as forwardRefType)(function let isFirstEditableSegment = segment.isEditable && segment.type === state.segments.find(s => s.isEditable)?.type; - let domRef = useObjectRef((isFirstEditableSegment && focusableRef) ? focusableRef : ref); + let domRef = useObjectRef(mergeRefs((isFirstEditableSegment && focusableRef) ? focusableRef : null, ref)); let {segmentProps} = useDateSegment(segment, state, domRef); let {focusProps, isFocused, isFocusVisible} = useFocusRing(); let {hoverProps, isHovered} = useHover({...otherProps, isDisabled: state.isDisabled || segment.type === 'literal'}); diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index 667a7da2f4a..001a6262869 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -499,4 +499,21 @@ describe('DateField', () => { expect(document.activeElement).toBe(segments[0]); }); + + it('should support focusableRef and ref on DateSegment concurrently', () => { + let focusableRef = React.createRef(); + let segmentRef = React.createRef(); + let {getAllByRole} = render( + + + + {segment => } + + + ); + + let segments = getAllByRole('spinbutton'); + expect(focusableRef.current).toBe(segments[0]); + expect(segmentRef.current).toBe(segments[0]); + }); });