Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions packages/react-aria-components/test/DateField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,105 @@ 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(
<DateField>
<Label>Date</Label>
<DateInput>{segment => <DateSegment segment={segment} />}</DateInput>
</DateField>
);

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(
<DateField onChange={onChange}>
<Label>Date</Label>
<DateInput>{segment => <DateSegment segment={segment} />}</DateInput>
</DateField>
);

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(
<DateField defaultValue={new CalendarDate(2020, 2, 15)}>
<Label>Date</Label>
<DateInput>{segment => <DateSegment segment={segment} />}</DateInput>
</DateField>
);

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('replaces a pre-filled segment value with the first typed digit', async () => {
let {getAllByRole} = render(
<DateField defaultValue={new CalendarDate(2026, 5, 3)}>
<Label>Date</Label>
<DateInput>{segment => <DateSegment segment={segment} />}</DateInput>
</DateField>
);

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(
<DateField>
<Label>Date</Label>
<DateInput>{segment => <DateSegment segment={segment} />}</DateInput>
</DateField>
);

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(
<I18nProvider locale="ja-JP">
<DateField>
<Label>Date</Label>
<DateInput>{segment => <DateSegment segment={segment} />}</DateInput>
</DateField>
</I18nProvider>
);

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', () => {
Expand Down
15 changes: 15 additions & 0 deletions packages/react-aria-components/test/DateRangePicker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(
<I18nProvider locale="ja-JP">
<TestDateRangePicker />
</I18nProvider>
);

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(
<DateRangePickerContext.Provider value={{slots: {test: {'aria-label': 'test'}}}}>
Expand Down
18 changes: 18 additions & 0 deletions packages/react-aria-components/test/TimeField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<TimeField onChange={onChange}>
<Label>Time</Label>
<DateInput>{segment => <DateSegment segment={segment} />}</DateInput>
</TimeField>
);

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(
<TimeField name="time" form="test" value={new Time(8, 30)}>
Expand Down
23 changes: 23 additions & 0 deletions packages/react-aria/src/datepicker/useDateSegment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -37,6 +38,11 @@ export interface DateSegmentAria {
segmentProps: React.HTMLAttributes<HTMLDivElement>;
}

// 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<FocusManager, string>();

/**
* 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
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down
Loading