From 4e8d6fad79d8f8b2ba704449cd5bf67c13373ccc Mon Sep 17 00:00:00 2001 From: hasegawa-101 Date: Fri, 14 Nov 2025 21:57:09 +0900 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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