From bf308611cecb8ee340fb21e969b88d64c8fa6eae Mon Sep 17 00:00:00 2001 From: Dilip R Date: Sun, 25 Oct 2026 11:56:41 +0530 Subject: [PATCH 1/3] fix: move overflowing digit to the next DateField segment instead of replacing the value --- .../test/DateField.test.js | 84 +++++++++++++++++++ .../test/DateRangePicker.test.js | 15 ++++ .../test/TimeField.test.js | 18 ++++ .../src/datepicker/useDateSegment.ts | 23 +++++ 4 files changed, 140 insertions(+) diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index f1a16e5f30d..22ee7d87723 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -612,6 +612,90 @@ describe('DateField', () => { expect(segements[2]).toHaveTextContent('yyyy'); }); + it.each` + keys | expected + ${'532026'} | ${['5', '3', '2026']} + ${'13'} | ${['1', '3', 'yyyy']} + ${'1132'} | ${['11', '3', '2']} + `( + 'moves an overflowing digit to the next segment when typing $keys', + async ({keys, expected}) => { + let {getAllByRole} = render( + + + {segment => } + + ); + + let segments = getAllByRole('spinbutton'); + await user.click(segments[0]); + await user.keyboard(keys); + expect(segments.map(s => s.textContent)).toEqual(expected); + } + ); + + it('emits the full date after an overflowing digit moves to the next segment', async () => { + let onChange = jest.fn(); + let {getAllByRole} = render( + + + {segment => } + + ); + + await user.click(getAllByRole('spinbutton')[0]); + await user.keyboard('532026'); + expect(onChange).toHaveBeenLastCalledWith(new CalendarDate(2026, 5, 3)); + }); + + it('moves an overflowing digit to the next segment when editing an existing date', async () => { + let {getAllByRole} = render( + + + {segment => } + + ); + + let segments = getAllByRole('spinbutton'); + await user.click(segments[1]); + await user.keyboard('32024'); + expect(segments.map(s => s.textContent)).toEqual(['2', '3', '2024']); + expect(segments[2]).toHaveFocus(); + }); + + it('does not move to the next segment when the entered digits fit', async () => { + let {getAllByRole} = render( + + + {segment => } + + ); + + let segments = getAllByRole('spinbutton'); + await user.click(segments[1]); + await user.keyboard('31'); + expect(segments[1]).toHaveTextContent('31'); + expect(segments[2]).toHaveFocus(); + expect(segments[2]).toHaveTextContent('yyyy'); + }); + + it('replaces the value when the last segment overflows', async () => { + let {getAllByRole} = render( + + + + {segment => } + + + ); + + let segments = getAllByRole('spinbutton'); + await user.click(segments[0]); + await user.keyboard('2026532'); + expect(segments.map(s => s.textContent)).toEqual(['2026', '5', '2']); + expect(segments[2]).toHaveFocus(); + }); + // Regression test for #10259: in Firefox a stale selection anchor can remain inside a segment // after focus moves away, and the selectionchange handler would collapse onto it, stealing focus. it('does not collapse the selection onto a segment while another element is focused', () => { diff --git a/packages/react-aria-components/test/DateRangePicker.test.js b/packages/react-aria-components/test/DateRangePicker.test.js index bfaee202336..f0fdaec44e7 100644 --- a/packages/react-aria-components/test/DateRangePicker.test.js +++ b/packages/react-aria-components/test/DateRangePicker.test.js @@ -20,6 +20,7 @@ import {Dialog} from '../src/Dialog'; import {FieldError} from '../src/FieldError'; import {Group} from '../src/Group'; import {Heading} from '../src/Heading'; +import {I18nProvider} from 'react-aria/I18nProvider'; import {Label} from '../src/Label'; import {Popover} from '../src/Popover'; import React from 'react'; @@ -105,6 +106,20 @@ describe('DateRangePicker', () => { expect(getByRole('grid')).toHaveClass('react-aria-CalendarGrid'); }); + it('moves an overflowing digit from the start field into the end field', async () => { + let {getAllByRole} = render( + + + + ); + + let segments = getAllByRole('spinbutton'); + await user.click(segments[0]); + await user.keyboard('2026532'); + expect(segments.map(s => s.textContent)).toEqual(['2026', '5', '3', '2', '月', '日']); + expect(segments[3]).toHaveFocus(); + }); + it('should support the slot prop', () => { let {getByRole} = render( diff --git a/packages/react-aria-components/test/TimeField.test.js b/packages/react-aria-components/test/TimeField.test.js index f32948aec5a..acafee78261 100644 --- a/packages/react-aria-components/test/TimeField.test.js +++ b/packages/react-aria-components/test/TimeField.test.js @@ -210,6 +210,24 @@ describe('TimeField', () => { expect(group.closest('.react-aria-TimeField')).toHaveAttribute('data-required'); }); + it('moves an overflowing hour digit to the minute segment', async () => { + let onChange = jest.fn(); + let {getAllByRole} = render( + + + {segment => } + + ); + + let segments = getAllByRole('spinbutton'); + await user.click(segments[0]); + await user.keyboard('135'); + expect(segments[0]).toHaveTextContent('1'); + expect(segments[1]).toHaveTextContent('35'); + await user.keyboard('p'); + expect(onChange).toHaveBeenLastCalledWith(new Time(13, 35)); + }); + it('should support form value', () => { render( diff --git a/packages/react-aria/src/datepicker/useDateSegment.ts b/packages/react-aria/src/datepicker/useDateSegment.ts index 09f75359211..8a02511e474 100644 --- a/packages/react-aria/src/datepicker/useDateSegment.ts +++ b/packages/react-aria/src/datepicker/useDateSegment.ts @@ -12,6 +12,7 @@ import {CalendarDate, toCalendar} from '@internationalized/date'; import {DateFieldState, DateSegment} from 'react-stately/useDateFieldState'; +import {FocusManager} from '../focus/FocusScope'; import {getActiveElement, nodeContains} from '../utils/shadowdom/DOMFunctions'; import {getScrollParent} from '../utils/getScrollParent'; import {hookData} from './useDateField'; @@ -37,6 +38,11 @@ export interface DateSegmentAria { segmentProps: React.HTMLAttributes; } +// A digit that overflowed the previous segment (e.g. the "2" in "32" for a day), to be entered +// into the next segment when it receives focus. Set and consumed synchronously within focusNext(). +// Keyed by focus manager so it also crosses between the start and end fields of a date range picker. +const pendingKeys = new WeakMap(); + /** * Provides the behavior and accessibility implementation for a segment in a date field. * A date segment displays an individual unit of a date and time, and allows users to edit @@ -226,6 +232,17 @@ export function useDateSegment( let numberValue = parser.parse(newValue); let segmentValue = numberValue; if (segment.maxValue !== undefined && numberValue > segment.maxValue) { + // The previously entered digits are a complete value on their own, so keep them + // and enter the new digit into the next segment instead (e.g. typing "32" in a day). + if (enteredKeys.current !== '') { + enteredKeys.current = ''; + pendingKeys.set(focusManager, key); + let next = focusManager.focusNext(); + pendingKeys.delete(focusManager); + if (next) { + break; + } + } segmentValue = parser.parse(key); } @@ -259,6 +276,12 @@ export function useDateSegment( // Collapse selection to start or Chrome won't fire input events. let selection = window.getSelection(); selection?.collapse(ref.current); + + let pendingKey = pendingKeys.get(focusManager); + if (pendingKey != null) { + pendingKeys.delete(focusManager); + onInput(pendingKey); + } }; let documentRef = useRef(typeof document !== 'undefined' ? document : null); From 25b0c2fbf3499348f05bb52e4a129a2b98cba76f Mon Sep 17 00:00:00 2001 From: Dilip R Date: Sun, 25 Oct 2026 13:10:19 +0530 Subject: [PATCH 2/3] chore: retrigger CLA check From 8f2c36c28e362df9dc13df20fae0856433fc901c Mon Sep 17 00:00:00 2001 From: Dilip R Date: Tue, 27 Oct 2026 22:01:58 +0530 Subject: [PATCH 3/3] test(DateField): pre-filled segment is replaced by first typed digit --- .../react-aria-components/test/DateField.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index 22ee7d87723..9b1453b80cc 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -663,6 +663,21 @@ describe('DateField', () => { expect(segments[2]).toHaveFocus(); }); + it('replaces a pre-filled segment value with the first typed digit', async () => { + let {getAllByRole} = render( + + + {segment => } + + ); + + let segments = getAllByRole('spinbutton'); + await user.click(segments[1]); + await user.keyboard('2'); + expect(segments.map(s => s.textContent)).toEqual(['5', '2', '2026']); + expect(segments[1]).toHaveFocus(); + }); + it('does not move to the next segment when the entered digits fit', async () => { let {getAllByRole} = render(