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
168 changes: 168 additions & 0 deletions packages/@adobe/react-spectrum/test/datepicker/DateField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,142 @@ describe('DateField', function () {
await user.tab();
expect(getDescription()).not.toContain('Constraints not satisfied');
});

it('should signal valueMissing when a complete date is made partial by clearing a segment (Bug #9624)', async () => {
// Regression (per devongovett's direction in #9624): a partially-filled required
// DateField should be marked invalid on blur. onChange(null) is deliberately NOT
// fired for partial values; getValidationResult compensates by surfacing the
// localized "Please enter a value." message via builtinValidation.
let {getByRole} = render(
<Provider theme={theme}>
<Form data-testid="form">
<DateField label="Date" name="date" isRequired validationBehavior="native" />
</Form>
</Provider>
);

let group = getByRole('group');
let input = document.querySelector('input[name=date]');

// Fill a complete valid date: 4/28/2026
await user.tab();
await user.keyboard('4');
await user.keyboard('28');
await user.keyboard('2026');
expect(input.validity.valid).toBe(true);

// Refocus the month and clear it
let segments = within(group).getAllByRole('spinbutton');
act(() => {
segments[0].focus();
});
await user.keyboard('{Backspace}');
expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty');

// Tab forward past day and year to exit the group
await user.tab();
await user.tab();
await user.tab();

// The hidden input should reflect missing value and validation should surface
expect(input.validity.valid).toBe(false);
let getDescriptionAfter = () =>
(group.getAttribute('aria-describedby') || '')
.split(' ')
.map(d => document.getElementById(d)?.textContent || '')
.join(' ');
expect(getDescriptionAfter()).toContain('Please enter a value.');
});

it('should clear the validation error after a partial field is completed (Bug #9624 round-trip)', async () => {
// Locks the lifecycle: complete -> clear segment (error) -> re-complete with a new
// valid value (error clears). Verifies the fix does not leave the field stuck invalid.
let {getByRole} = render(
<Provider theme={theme}>
<Form data-testid="form">
<DateField label="Date" name="date" isRequired validationBehavior="native" />
</Form>
</Provider>
);

let group = getByRole('group');
let input = document.querySelector('input[name=date]');

await user.tab();
await user.keyboard('4');
await user.keyboard('28');
await user.keyboard('2026');

let segments = within(group).getAllByRole('spinbutton');
act(() => {
segments[0].focus();
});
await user.keyboard('{Backspace}');
await user.tab();
await user.tab();
await user.tab();

let getDescription = () =>
(group.getAttribute('aria-describedby') || '')
.split(' ')
.map(d => document.getElementById(d)?.textContent || '')
.join(' ');
expect(getDescription()).toContain('Please enter a value.');

// Re-focus month and complete the date with a different valid value
act(() => {
segments[0].focus();
});
await user.keyboard('5');
await user.tab();
await user.tab();
await user.tab();

expect(input.validity.valid).toBe(true);
expect(getDescription()).not.toContain('Please enter a value.');
});

it('should signal validation for partial values regardless of isRequired (Bug #9958)', async () => {
// Per LFDanLu: "invalidate the field if the date is incomplete in general."
// The earlier scope-guard test asserted the opposite; updated to lock the new
// behavior — partial values are flagged invalid even without isRequired so that
// min/max/unavailable/validate configurations also block submission.
let {getByRole} = render(
<Provider theme={theme}>
<Form>
<DateField
label="Date"
name="date"
minValue={new CalendarDate(2020, 1, 1)}
defaultValue={new CalendarDate(2026, 4, 28)}
validationBehavior="native"
/>
</Form>
</Provider>
);

let group = getByRole('group');
let input = document.querySelector('input[name=date]');
let segments = within(group).getAllByRole('spinbutton');

act(() => {
segments[0].focus();
});
await user.keyboard('{Backspace}');
await user.keyboard('{Backspace}');
expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty');

await user.tab();
await user.tab();
await user.tab();

expect(input.validity.valid).toBe(false);
let description = (group.getAttribute('aria-describedby') || '')
.split(' ')
.map(d => document.getElementById(d)?.textContent || '')
.join(' ');
expect(description).toContain('Please enter a value.');
});
});

describe('validationBehavior=aria', () => {
Expand Down Expand Up @@ -850,6 +986,38 @@ describe('DateField', function () {
await user.keyboard('[Tab][ArrowRight][ArrowRight]2024[Tab]');
expect(getDescription()).not.toContain('Invalid value');
});

it('should clear the hidden input value when partial in aria mode (Bug #9624)', async () => {
// Aria-mode counterpart to the native-mode #9624 regression: the hidden input must
// reflect the partial display state regardless of validationBehavior so any consumer
// (e.g. FormData) sees the missing value.
let {getByRole} = render(
<Provider theme={theme}>
<Form>
<DateField
label="Date"
name="date"
isRequired
validationBehavior="aria"
defaultValue={new CalendarDate(2026, 4, 28)}
/>
</Form>
</Provider>
);

let group = getByRole('group');
let input = document.querySelector('input[name=date]');
expect(input).toHaveValue('2026-04-28');

let segments = within(group).getAllByRole('spinbutton');
act(() => {
segments[0].focus();
});
await user.keyboard('{Backspace}');
expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty');

expect(input).toHaveValue('');
});
});
});
});
Expand Down
Loading