diff --git a/packages/react-aria-components/src/DateField.tsx b/packages/react-aria-components/src/DateField.tsx index c8ac4710ea4..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'; @@ -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,10 @@ export interface DateInputProps extends SlotProps, StyleRenderProps, + /** + * A ref for the first focusable date segment. + */ + focusableRef?: ForwardedRef, children: (segment: IDateSegment) => ReactElement } @@ -296,15 +301,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 +386,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(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 6f9830aba14..001a6262869 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -478,4 +478,42 @@ 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'); + expect(focusableRef.current).toBe(segments[0]); + + act(() => { + focusableRef.current.focus(); + }); + + 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]); + }); }); diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index 5298849b1a5..ab367eab685 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -334,4 +334,29 @@ 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'); + expect(focusableRef.current).toBe(segments[0]); + + act(() => { + focusableRef.current.focus(); + }); + + expect(document.activeElement).toBe(segments[0]); + }); }); diff --git a/packages/react-aria-components/test/DateRangePicker.test.js b/packages/react-aria-components/test/DateRangePicker.test.js index e6562776338..70b3a6edbf1 100644 --- a/packages/react-aria-components/test/DateRangePicker.test.js +++ b/packages/react-aria-components/test/DateRangePicker.test.js @@ -333,6 +333,69 @@ describe('DateRangePicker', () => { } }); + it('should support focusableRef', () => { + let focusableRef = React.createRef(); + let secondFocusableRef = React.createRef(); + let segmentRef = React.createRef(); + let {getAllByRole} = render( + + + + + {(segment) => { + if (segment.type === 'month') { + return ; + } + return ; + }} + + + + {(segment) => } + + + + Description + Error + + + + Yo + + test + +
+ + + +
+ + {(date) => } + +
+
+
+
+ ); + + expect(segmentRef.current).toBe(focusableRef.current); + + let segments = getAllByRole('spinbutton'); + expect(segmentRef.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(