From 2e3f908c96336e256a8b7dc9394e0a164182b472 Mon Sep 17 00:00:00 2001
From: AK <144495202+AKnassa@users.noreply.github.com>
Date: Fri, 15 May 2026 11:29:40 -0400
Subject: [PATCH 1/7] test: add failing regression tests for #9958, #9801,
#9624
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
These tests reproduce three bugs introduced by #9510 (IncompleteDate):
- #9958: DatePicker required validation skips on month clear
- #9801: TimeField hidden input retains stale value after partial clear
- #9624: DateField partial value not flagged as invalid on blur
Tests are intentionally failing — fix follows in a subsequent commit.
---
.../test/datepicker/DateField.test.js | 45 +++++++++++++++++++
.../test/datepicker/DatePicker.test.js | 45 +++++++++++++++++++
.../test/datepicker/TimeField.test.js | 32 +++++++++++++
3 files changed, 122 insertions(+)
diff --git a/packages/@adobe/react-spectrum/test/datepicker/DateField.test.js b/packages/@adobe/react-spectrum/test/datepicker/DateField.test.js
index 8949e1cb327..216244367e7 100644
--- a/packages/@adobe/react-spectrum/test/datepicker/DateField.test.js
+++ b/packages/@adobe/react-spectrum/test/datepicker/DateField.test.js
@@ -764,6 +764,51 @@ 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 via valueMissing on blur. onChange(null) is
+ // deliberately NOT fired for partial values; validation must compensate.
+ let {getByRole, getByTestId} = render(
+
+
+
+ );
+
+ 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('Constraints not satisfied');
+ });
});
describe('validationBehavior=aria', () => {
diff --git a/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js b/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js
index bfbbb5eba8e..913efcb7dc3 100644
--- a/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js
+++ b/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js
@@ -3149,6 +3149,51 @@ describe('DatePicker', function () {
expect(getDescription()).not.toContain('Constraints not satisfied');
});
+ it('should signal validation when a complete date is made partial by clearing the month segment (Bug #9958)', async () => {
+ // Regression: clearing the month segment of a previously-complete date does not
+ // change the committed state.value, so the blur-time guard in useDateField.ts
+ // (state.value !== valueOnFocus.current) fails and commitValidation() is never
+ // called. With isRequired + an incomplete displayValue, validation should fire.
+ let {getByRole, getByTestId} = render(
+
+
+
+ );
+
+ 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 segment and clear it (single Backspace for single-digit '4')
+ let segments = within(group).getAllByRole('spinbutton');
+ act(() => {
+ segments[0].focus();
+ });
+ await user.keyboard('{Backspace}');
+ expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty');
+
+ // Tab forward out of the group (month -> day -> year -> calendar trigger)
+ await user.tab();
+ await user.tab();
+ await user.tab();
+
+ // Field is now partial and required — expect validation error surfaced
+ let getDescription = () =>
+ (group.getAttribute('aria-describedby') || '')
+ .split(' ')
+ .map(d => document.getElementById(d)?.textContent || '')
+ .join(' ');
+ expect(getDescription()).toContain('Constraints not satisfied');
+ });
+
it('supports minValue and maxValue', async () => {
let {getByRole, getByTestId} = render(
diff --git a/packages/@adobe/react-spectrum/test/datepicker/TimeField.test.js b/packages/@adobe/react-spectrum/test/datepicker/TimeField.test.js
index 3bc3ff3fbbc..fd609231ed3 100644
--- a/packages/@adobe/react-spectrum/test/datepicker/TimeField.test.js
+++ b/packages/@adobe/react-spectrum/test/datepicker/TimeField.test.js
@@ -419,6 +419,38 @@ describe('TimeField', function () {
expect(input).toHaveValue('08:30:00');
});
+ it('should clear the hidden input value when a segment is cleared making the field partial (Bug #9801)', async () => {
+ // Regression: clearing one TimeField segment leaves the committed state.value
+ // unchanged, but the field is now visually incomplete. The hidden input — read by
+ // forms on submit — should reflect the missing value as '', not the stale committed
+ // time. Root cause: useDateField.ts uses state.timeValue?.toString() which is
+ // derived from the committed value, not from displayValue.
+ function Test() {
+ return (
+
+ );
+ }
+
+ let {getAllByRole} = render();
+ let input = document.querySelector('input[name=time]');
+ let segments = getAllByRole('spinbutton');
+
+ expect(input).toHaveValue('13:30:00');
+
+ // Clear the hour segment (display shows '1 PM' for 13 — single digit → Empty in one Backspace)
+ act(() => {
+ segments[0].focus();
+ });
+ fireEvent.keyDown(document.activeElement, {key: 'Backspace'});
+ fireEvent.keyUp(document.activeElement, {key: 'Backspace'});
+ expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty');
+
+ // Field is partial — hidden input should not carry the stale committed time
+ expect(input).toHaveValue('');
+ });
+
if (parseInt(React.version, 10) >= 19) {
it('resets to defaultValue when submitting form action', async () => {
function Test() {
From 7da2cffc1e78ca1f38e6cf8a6be81ecc20574538 Mon Sep 17 00:00:00 2001
From: AK <144495202+AKnassa@users.noreply.github.com>
Date: Sun, 17 May 2026 23:35:32 -0400
Subject: [PATCH 2/7] fix: surface validation error for incomplete date values
(#9958, #9801, #9624)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
When a user partially fills a date field (some segments typed, some still
placeholders) the value is an IncompleteDate and was silently swallowed —
no validation error was raised on form submit or blur.
- Add `isValuePartial` flag to `DateFieldState`, derived from
`IncompleteDate.isComplete / isCleared` on the display value.
- Pass `isValuePartial` into `getValidationResult` / `getRangeValidationResult`
so the built-in validation pipeline marks the field invalid and sets
`valueMissing: true` in the ValidityState object.
- Add "Please enter a value." i18n string (`incompleteValue`) used when
the partial-state error is the active error.
- Lift partial state up from the inner DateField(s) to DatePickerState and
DateRangePickerState via a new private symbol prop
(`privateSetIsValuePartialProp`) so those parents can include it in their
own `builtinValidation` memo and revalidate reactively.
- Wire the setter call in `useDateField` / `useDateRangePicker` so the
parent receives updates whenever the user edits a segment.
- Add regression tests covering: partial-fill validation, clear-after-partial
resets error, full-fill clears error, and form-submit behaviour for
DateField, DatePicker, and DateRangePicker.
---
.../test/datepicker/DateField.test.js | 131 +++++++++++++++++-
.../test/datepicker/DatePicker.test.js | 55 +++++++-
.../test/datepicker/DateRangePicker.test.js | 47 +++++++
.../react-aria/src/datepicker/useDateField.ts | 23 ++-
.../src/datepicker/useDatePicker.ts | 8 +-
.../src/datepicker/useDateRangePicker.ts | 9 +-
.../private/form/useFormValidationState.ts | 1 +
.../react-stately/intl/datepicker/en-US.json | 1 +
.../src/datepicker/useDateFieldState.ts | 23 ++-
.../src/datepicker/useDatePickerState.ts | 25 +++-
.../src/datepicker/useDateRangePickerState.ts | 29 +++-
.../react-stately/src/datepicker/utils.ts | 21 ++-
.../src/form/useFormValidationState.ts | 4 +
13 files changed, 347 insertions(+), 30 deletions(-)
diff --git a/packages/@adobe/react-spectrum/test/datepicker/DateField.test.js b/packages/@adobe/react-spectrum/test/datepicker/DateField.test.js
index 216244367e7..91f0ae4cb53 100644
--- a/packages/@adobe/react-spectrum/test/datepicker/DateField.test.js
+++ b/packages/@adobe/react-spectrum/test/datepicker/DateField.test.js
@@ -767,9 +767,10 @@ describe('DateField', function () {
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 via valueMissing on blur. onChange(null) is
- // deliberately NOT fired for partial values; validation must compensate.
- let {getByRole, getByTestId} = render(
+ // 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(