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(
@@ -807,7 +808,97 @@ describe('DateField', function () { .split(' ') .map(d => document.getElementById(d)?.textContent || '') .join(' '); - expect(getDescriptionAfter()).toContain('Constraints not satisfied'); + 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( + + + + + + ); + + 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( + +
+ + +
+ ); + + 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.'); }); }); @@ -895,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( + +
+ + +
+ ); + + 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(''); + }); }); }); }); diff --git a/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js b/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js index 913efcb7dc3..edf1789de64 100644 --- a/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js +++ b/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js @@ -3151,10 +3151,11 @@ describe('DatePicker', function () { 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( + // change the committed state.value. Without isRequired alone the form had no + // signal to block submit. Now getValidationResult treats partial display state as + // invalid via the lifted-up isValuePartial flag in useDatePickerState, surfacing + // a localized error message. + let {getByRole} = render(
@@ -3185,13 +3186,55 @@ describe('DatePicker', function () { await user.tab(); await user.tab(); - // Field is now partial and required — expect validation error surfaced + // Field is now partial — expect validation error surfaced let getDescription = () => (group.getAttribute('aria-describedby') || '') .split(' ') .map(d => document.getElementById(d)?.textContent || '') .join(' '); - expect(getDescription()).toContain('Constraints not satisfied'); + expect(input.validity.valid).toBe(false); + expect(getDescription()).toContain('Please enter a value.'); + }); + + it('should signal validation when a date with minValue is made partial without isRequired (Bug #9958)', async () => { + // The exact scenario the user reported: DatePicker forms in the docs using only + // minValue/maxValue / isDateUnavailable / validate (no isRequired) — clearing month + // or day must still block submission and surface an error. + let {getByRole} = render( + + + + + + ); + + let group = getByRole('group'); + let input = document.querySelector('input[name=date]'); + let segments = within(group).getAllByRole('spinbutton'); + + // Clear the month segment (single-digit '4' goes straight to null). + act(() => { segments[0].focus(); }); + await user.keyboard('{Backspace}'); + expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty'); + + await user.tab(); + await user.tab(); + await user.tab(); + + let description = (group.getAttribute('aria-describedby') || '') + .split(' ') + .map(d => document.getElementById(d)?.textContent || '') + .join(' '); + + expect(input.validity.valid).toBe(false); + expect(description).toContain('Please enter a value.'); }); it('supports minValue and maxValue', async () => { diff --git a/packages/@adobe/react-spectrum/test/datepicker/DateRangePicker.test.js b/packages/@adobe/react-spectrum/test/datepicker/DateRangePicker.test.js index 13e69ebfa9c..cc68fbb9b33 100644 --- a/packages/@adobe/react-spectrum/test/datepicker/DateRangePicker.test.js +++ b/packages/@adobe/react-spectrum/test/datepicker/DateRangePicker.test.js @@ -1924,6 +1924,53 @@ describe('DateRangePicker', function () { expect(getDescription()).not.toContain('Constraints not satisfied'); }); + it('should signal validation when an endpoint date is made partial by clearing a segment (Bug #9958 followup)', async () => { + // Locks transitive coverage of DateRangePicker: each endpoint has its own + // DateFieldState with isValuePartial. Clearing a segment of either endpoint should + // fire validation on blur. + let {getByRole} = render( + +
+ + +
+ ); + + let group = getByRole('group'); + let segments = within(group).getAllByRole('spinbutton'); + act(() => { + segments[0].focus(); + }); + await user.keyboard('{Backspace}'); + expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty'); + + // Tab past remaining start segments, end segments, and calendar trigger + await user.tab(); + await user.tab(); + await user.tab(); + await user.tab(); + 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.'); + }); + it('supports minValue and maxValue', async () => { let {getByRole, getByTestId} = render( diff --git a/packages/react-aria/src/datepicker/useDateField.ts b/packages/react-aria/src/datepicker/useDateField.ts index 11a23852686..fcb45ebe16b 100644 --- a/packages/react-aria/src/datepicker/useDateField.ts +++ b/packages/react-aria/src/datepicker/useDateField.ts @@ -26,6 +26,7 @@ import {filterDOMProps} from '../utils/filterDOMProps'; import {InputHTMLAttributes, useEffect, useMemo, useRef} from 'react'; import intlMessages from '../../intl/datepicker/*.json'; import {mergeProps} from '../utils/mergeProps'; +import {privateSetIsValuePartialProp} from 'react-stately/private/form/useFormValidationState'; import {TimeFieldState, TimePickerProps, TimeValue} from 'react-stately/useTimeFieldState'; import {useDatePickerGroup} from './useDatePickerGroup'; // @ts-ignore @@ -110,7 +111,8 @@ export function useDateField( }, onBlurWithin: e => { state.confirmPlaceholder(); - if (state.value !== valueOnFocus.current) { + // Also fire for partial display values: `value` is never updated for those by design. + if (state.value !== valueOnFocus.current || state.isValuePartial) { state.commitValidation(); } props.onBlur?.(e); @@ -188,11 +190,25 @@ export function useDateField( props.inputRef ); + // When wrapped by DatePicker / DateRangePicker, the picker owns the validation pipeline + // (via privateValidationStateProp). Push our local partial state up so the picker's + // getValidationResult sees it; standalone fields handle this in useDateFieldState directly. + let setParentIsValuePartial = (props as any)[privateSetIsValuePartialProp] as + | ((isPartial: boolean) => void) + | undefined; + useEffect(() => { + if (setParentIsValuePartial) { + setParentIsValuePartial(state.isValuePartial); + return () => setParentIsValuePartial(false); + } + }, [setParentIsValuePartial, state.isValuePartial]); + + // Empty when partial so the native `required` constraint sees a missing value. let inputProps: InputHTMLAttributes = { type: 'hidden', name: props.name, form: props.form, - value: state.value?.toString() || '', + value: state.isValuePartial ? '' : state.value?.toString() || '', disabled: props.isDisabled }; @@ -257,6 +273,7 @@ export function useTimeField( ref: RefObject ): DateFieldAria { let res = useDateField(props, state, ref); - res.inputProps.value = state.timeValue?.toString() || ''; + // Same partial-state guard as the DateField hidden input. + res.inputProps.value = state.isValuePartial ? '' : state.timeValue?.toString() || ''; return res; } diff --git a/packages/react-aria/src/datepicker/useDatePicker.ts b/packages/react-aria/src/datepicker/useDatePicker.ts index d2dc765104f..98aa573f28b 100644 --- a/packages/react-aria/src/datepicker/useDatePicker.ts +++ b/packages/react-aria/src/datepicker/useDatePicker.ts @@ -30,7 +30,10 @@ import {filterDOMProps} from '../utils/filterDOMProps'; import intlMessages from '../../intl/datepicker/*.json'; import {mergeProps} from '../utils/mergeProps'; import {nodeContains} from '../utils/shadowdom/DOMFunctions'; -import {privateValidationStateProp} from 'react-stately/private/form/useFormValidationState'; +import { + privateSetIsValuePartialProp, + privateValidationStateProp +} from 'react-stately/private/form/useFormValidationState'; // @ts-ignore import {roleSymbol} from './useDateField'; import {useDatePickerGroup} from './useDatePickerGroup'; @@ -179,6 +182,9 @@ export function useDatePicker( validationBehavior: props.validationBehavior, // DatePicker owns the validation state for the date field. [privateValidationStateProp]: state, + // Forwarded so the field can push its partial-edit state up into the picker's + // validation pipeline (so partial values invalidate even without isRequired). + [privateSetIsValuePartialProp]: (state as any)[privateSetIsValuePartialProp], autoFocus: props.autoFocus, name: props.name, form: props.form diff --git a/packages/react-aria/src/datepicker/useDateRangePicker.ts b/packages/react-aria/src/datepicker/useDateRangePicker.ts index 239b4366a3f..a37411b0b4b 100644 --- a/packages/react-aria/src/datepicker/useDateRangePicker.ts +++ b/packages/react-aria/src/datepicker/useDateRangePicker.ts @@ -34,6 +34,7 @@ import { import { DEFAULT_VALIDATION_RESULT, mergeValidation, + privateSetIsValuePartialProp, privateValidationStateProp } from 'react-stately/private/form/useFormValidationState'; import {filterDOMProps} from '../utils/filterDOMProps'; @@ -239,7 +240,9 @@ export function useDateRangePicker( }, resetValidation: state.resetValidation, commitValidation: state.commitValidation - } + }, + // Push the start field's partial-edit state up into the range's validation pipeline. + [privateSetIsValuePartialProp]: (state as any)[`${privateSetIsValuePartialProp}-start`] }, endFieldProps: { ...endFieldProps, @@ -258,7 +261,9 @@ export function useDateRangePicker( }, resetValidation: state.resetValidation, commitValidation: state.commitValidation - } + }, + // Push the end field's partial-edit state up into the range's validation pipeline. + [privateSetIsValuePartialProp]: (state as any)[`${privateSetIsValuePartialProp}-end`] }, descriptionProps, errorMessageProps, diff --git a/packages/react-stately/exports/private/form/useFormValidationState.ts b/packages/react-stately/exports/private/form/useFormValidationState.ts index 4d99ecefe83..3fd0ce74ec7 100644 --- a/packages/react-stately/exports/private/form/useFormValidationState.ts +++ b/packages/react-stately/exports/private/form/useFormValidationState.ts @@ -2,6 +2,7 @@ export { FormValidationContext, useFormValidationState, privateValidationStateProp, + privateSetIsValuePartialProp, type FormValidationState, DEFAULT_VALIDATION_RESULT, mergeValidation, diff --git a/packages/react-stately/intl/datepicker/en-US.json b/packages/react-stately/intl/datepicker/en-US.json index a2985739ad2..3e2d1bee190 100644 --- a/packages/react-stately/intl/datepicker/en-US.json +++ b/packages/react-stately/intl/datepicker/en-US.json @@ -1,4 +1,5 @@ { + "incompleteValue": "Please enter a value.", "rangeUnderflow": "Value must be {minValue} or later.", "rangeOverflow": "Value must be {maxValue} or earlier.", "rangeReversed": "Start date must be before end date.", diff --git a/packages/react-stately/src/datepicker/useDateFieldState.ts b/packages/react-stately/src/datepicker/useDateFieldState.ts index 2103656abf0..e7b49f99783 100644 --- a/packages/react-stately/src/datepicker/useDateFieldState.ts +++ b/packages/react-stately/src/datepicker/useDateFieldState.ts @@ -100,6 +100,13 @@ export interface DateFieldState extends FormValidationState { isReadOnly: boolean; /** Whether the field is required. */ isRequired: boolean; + /** + * Whether the display value is partially filled — some editable segments have values and + * some are still placeholders. + * + * @private + */ + isValuePartial: boolean; /** * Increments the given segment. Upon reaching the minimum or maximum value, the value wraps * around to the opposite limit. @@ -363,9 +370,20 @@ export function useDateFieldState( setValue(displayValue.cycle(type, amount, placeholder, displaySegments)); }; + let isValuePartial = + !displayValue.isComplete(displaySegments) && !displayValue.isCleared(displaySegments); + let builtinValidation = useMemo( - () => getValidationResult(value, minValue, maxValue, isDateUnavailable, formatOpts), - [value, minValue, maxValue, isDateUnavailable, formatOpts] + () => + getValidationResult( + value, + minValue, + maxValue, + isDateUnavailable, + formatOpts, + isValuePartial + ), + [value, minValue, maxValue, isDateUnavailable, formatOpts, isValuePartial] ); let validation = useFormValidationState({ @@ -394,6 +412,7 @@ export function useDateFieldState( isDisabled, isReadOnly, isRequired, + isValuePartial, increment(part) { adjustSegment(part, 1); }, diff --git a/packages/react-stately/src/datepicker/useDatePickerState.ts b/packages/react-stately/src/datepicker/useDatePickerState.ts index f4a38ab56a7..bfadda6f2bc 100644 --- a/packages/react-stately/src/datepicker/useDatePickerState.ts +++ b/packages/react-stately/src/datepicker/useDatePickerState.ts @@ -25,7 +25,11 @@ import { getValidationResult, useDefaultProps } from './utils'; -import {FormValidationState, useFormValidationState} from '../form/useFormValidationState'; +import { + FormValidationState, + privateSetIsValuePartialProp, + useFormValidationState +} from '../form/useFormValidationState'; import {OverlayTriggerState, useOverlayTriggerState} from '../overlays/useOverlayTriggerState'; import {useControlledState} from '../utils/useControlledState'; import {useMemo, useState} from 'react'; @@ -144,9 +148,23 @@ export function useDatePickerState( ); let {minValue, maxValue, isDateUnavailable} = props; + + // Partial-state lifted up from the inner DateField via `privateSetIsValuePartialProp` + // on fieldProps. The field calls our setter when its IncompleteDate has some-but-not-all + // editable segments filled, so the parent's validation pipeline can surface the error. + let [isValuePartial, setIsValuePartial] = useState(false); + let builtinValidation = useMemo( - () => getValidationResult(value, minValue, maxValue, isDateUnavailable, formatOpts), - [value, minValue, maxValue, isDateUnavailable, formatOpts] + () => + getValidationResult( + value, + minValue, + maxValue, + isDateUnavailable, + formatOpts, + isValuePartial + ), + [value, minValue, maxValue, isDateUnavailable, formatOpts, isValuePartial] ); let validation = useFormValidationState({ @@ -199,6 +217,7 @@ export function useDatePickerState( return { ...validation, + [privateSetIsValuePartialProp]: setIsValuePartial, value, defaultValue: props.defaultValue ?? initialValue, setValue, diff --git a/packages/react-stately/src/datepicker/useDateRangePickerState.ts b/packages/react-stately/src/datepicker/useDateRangePickerState.ts index a6b93564b09..2066f32bd3a 100644 --- a/packages/react-stately/src/datepicker/useDateRangePickerState.ts +++ b/packages/react-stately/src/datepicker/useDateRangePickerState.ts @@ -27,7 +27,11 @@ import { getRangeValidationResult, useDefaultProps } from './utils'; -import {FormValidationState, useFormValidationState} from '../form/useFormValidationState'; +import { + FormValidationState, + privateSetIsValuePartialProp, + useFormValidationState +} from '../form/useFormValidationState'; import {OverlayTriggerState, useOverlayTriggerState} from '../overlays/useOverlayTriggerState'; import {RangeValue, ValidationState} from '@react-types/shared'; import {useControlledState} from '../utils/useControlledState'; @@ -225,6 +229,12 @@ export function useDateRangePickerState( ); let {minValue, maxValue, isDateUnavailable} = props; + + // Partial-state lifted up from the inner start/end DateFields via privateSetIsValuePartialProp. + // See useDateField.ts and useDatePickerState.ts for the matching pattern. + let [startIsValuePartial, setStartIsValuePartial] = useState(false); + let [endIsValuePartial, setEndIsValuePartial] = useState(false); + let builtinValidation = useMemo( () => getRangeValidationResult( @@ -232,9 +242,19 @@ export function useDateRangePickerState( minValue, maxValue, isDateUnavailable ? date => isDateUnavailable(date, null) : undefined, - formatOpts + formatOpts, + startIsValuePartial, + endIsValuePartial ), - [value, minValue, maxValue, isDateUnavailable, formatOpts] + [ + value, + minValue, + maxValue, + isDateUnavailable, + formatOpts, + startIsValuePartial, + endIsValuePartial + ] ); let validation = useFormValidationState({ @@ -253,6 +273,9 @@ export function useDateRangePickerState( return { ...validation, + // Two setters since the range has two independent fields with separate partial state. + [`${privateSetIsValuePartialProp}-start`]: setStartIsValuePartial, + [`${privateSetIsValuePartialProp}-end`]: setEndIsValuePartial, value, defaultValue: props.defaultValue ?? initialValue, setValue, diff --git a/packages/react-stately/src/datepicker/utils.ts b/packages/react-stately/src/datepicker/utils.ts index 91488711566..3cb4c1c5c82 100644 --- a/packages/react-stately/src/datepicker/utils.ts +++ b/packages/react-stately/src/datepicker/utils.ts @@ -50,12 +50,13 @@ export function getValidationResult( minValue: DateValue | null | undefined, maxValue: DateValue | null | undefined, isDateUnavailable: ((v: DateValue) => boolean) | undefined, - options: FormatterOptions + options: FormatterOptions, + isValuePartial: boolean = false ): ValidationResult { let rangeOverflow = value != null && maxValue != null && value.compare(maxValue) > 0; let rangeUnderflow = value != null && minValue != null && value.compare(minValue) < 0; let isUnavailable = (value != null && isDateUnavailable?.(value)) || false; - let isInvalid = rangeOverflow || rangeUnderflow || isUnavailable; + let isInvalid = rangeOverflow || rangeUnderflow || isUnavailable || isValuePartial; let errors: string[] = []; if (isInvalid) { @@ -67,6 +68,10 @@ export function getValidationResult( let dateFormatter = new DateFormatter(locale, getFormatOptions({}, options)); let timeZone = dateFormatter.resolvedOptions().timeZone; + if (isValuePartial) { + errors.push(formatter.format('incompleteValue')); + } + if (rangeUnderflow && minValue != null) { errors.push( formatter.format('rangeUnderflow', { @@ -101,7 +106,7 @@ export function getValidationResult( tooLong: false, tooShort: false, typeMismatch: false, - valueMissing: false, + valueMissing: isValuePartial, valid: !isInvalid } }; @@ -112,14 +117,17 @@ export function getRangeValidationResult( minValue: DateValue | null | undefined, maxValue: DateValue | null | undefined, isDateUnavailable: ((v: DateValue) => boolean) | undefined, - options: FormatterOptions + options: FormatterOptions, + startIsValuePartial: boolean = false, + endIsValuePartial: boolean = false ): ValidationResult { let startValidation = getValidationResult( value?.start ?? null, minValue, maxValue, isDateUnavailable, - options + options, + startIsValuePartial ); let endValidation = getValidationResult( @@ -127,7 +135,8 @@ export function getRangeValidationResult( minValue, maxValue, isDateUnavailable, - options + options, + endIsValuePartial ); let result = mergeValidation(startValidation, endValidation); diff --git a/packages/react-stately/src/form/useFormValidationState.ts b/packages/react-stately/src/form/useFormValidationState.ts index 40ce614bcc7..1aface8a112 100644 --- a/packages/react-stately/src/form/useFormValidationState.ts +++ b/packages/react-stately/src/form/useFormValidationState.ts @@ -51,6 +51,10 @@ export const FormValidationContext: Context = createContext extends Validation { builtinValidation?: ValidationResult; name?: string | string[]; From 5972f9235ae0eaf384df12e35b1152608cb1eae9 Mon Sep 17 00:00:00 2001 From: AK <144495202+AKnassa@users.noreply.github.com> Date: Fri, 22 May 2026 00:40:21 -0400 Subject: [PATCH 3/7] fix: reset isValuePartial on setValue; show constraint error before generic incomplete message - useDatePickerState / useDateRangePickerState: wrap setValue so any committed date resets isValuePartial flags, preventing stale partial-state when consumer calls state.setValue() directly - utils.ts: reorders incompleteValue error push so constraint errors (rangeUnderflow, rangeOverflow, unavailableDate) are shown first; the generic "Please enter a value." only appears when there are no other errors --- .../src/datepicker/useDatePickerState.ts | 13 ++++++++++-- .../src/datepicker/useDateRangePickerState.ts | 20 +++++++++++++------ .../react-stately/src/datepicker/utils.ts | 12 +++++++---- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/react-stately/src/datepicker/useDatePickerState.ts b/packages/react-stately/src/datepicker/useDatePickerState.ts index bfadda6f2bc..e0cbec68ffc 100644 --- a/packages/react-stately/src/datepicker/useDatePickerState.ts +++ b/packages/react-stately/src/datepicker/useDatePickerState.ts @@ -99,7 +99,7 @@ export function useDatePickerState( props: DatePickerStateOptions ): DatePickerState { let overlayState = useOverlayTriggerState(props); - let [value, setValue] = useControlledState | null>( + let [value, setValueInternal] = useControlledState | null>( props.value, props.defaultValue || null, props.onChange @@ -154,6 +154,13 @@ export function useDatePickerState( // editable segments filled, so the parent's validation pipeline can surface the error. let [isValuePartial, setIsValuePartial] = useState(false); + // Wrap the raw setter so any committed value (complete or null) always resets the partial flag. + // This prevents stale isValuePartial state when a consumer calls state.setValue() directly. + let setValue = (newValue: DateValue | null) => { + setIsValuePartial(false); + setValueInternal(newValue); + }; + let builtinValidation = useMemo( () => getValidationResult( @@ -178,7 +185,9 @@ export function useDatePickerState( props.validationState || (isValueInvalid ? 'invalid' : null); let commitValue = (date: DateValue, time: TimeValue) => { - setValue('timeZone' in time ? time.set(toCalendarDate(date)) : toCalendarDateTime(date, time)); + setValue( + 'timeZone' in time ? time.set(toCalendarDate(date)) : toCalendarDateTime(date, time) + ); setSelectedDate(null); setSelectedTime(null); validation.commitValidation(); diff --git a/packages/react-stately/src/datepicker/useDateRangePickerState.ts b/packages/react-stately/src/datepicker/useDateRangePickerState.ts index 2066f32bd3a..0dbf6a67674 100644 --- a/packages/react-stately/src/datepicker/useDateRangePickerState.ts +++ b/packages/react-stately/src/datepicker/useDateRangePickerState.ts @@ -131,7 +131,12 @@ export function useDateRangePickerState( let value = controlledValue || placeholderValue; - let setValue = (newValue: RangeValue | null) => { + // Partial-state hoisted before setValue so the wrapper can reference the setters. + // Lifted from the inner start/end DateFields via privateSetIsValuePartialProp. + let [startIsValuePartial, setStartIsValuePartial] = useState(false); + let [endIsValuePartial, setEndIsValuePartial] = useState(false); + + let setValueInternal = (newValue: RangeValue | null) => { value = newValue || {start: null, end: null}; setPlaceholderValue(value); if (isCompleteRange(value)) { @@ -141,6 +146,14 @@ export function useDateRangePickerState( } }; + // Wrap the setter so any committed range (complete or null) always resets both partial flags, + // preventing stale isValuePartial state when a consumer calls state.setValue() directly. + let setValue = (newValue: RangeValue | null) => { + setStartIsValuePartial(false); + setEndIsValuePartial(false); + setValueInternal(newValue); + }; + let v = value?.start || value?.end || props.placeholderValue || null; let [granularity, defaultTimeZone] = useDefaultProps(v, props.granularity); let hasTime = granularity === 'hour' || granularity === 'minute' || granularity === 'second'; @@ -230,11 +243,6 @@ export function useDateRangePickerState( let {minValue, maxValue, isDateUnavailable} = props; - // Partial-state lifted up from the inner start/end DateFields via privateSetIsValuePartialProp. - // See useDateField.ts and useDatePickerState.ts for the matching pattern. - let [startIsValuePartial, setStartIsValuePartial] = useState(false); - let [endIsValuePartial, setEndIsValuePartial] = useState(false); - let builtinValidation = useMemo( () => getRangeValidationResult( diff --git a/packages/react-stately/src/datepicker/utils.ts b/packages/react-stately/src/datepicker/utils.ts index 3cb4c1c5c82..69417003bd5 100644 --- a/packages/react-stately/src/datepicker/utils.ts +++ b/packages/react-stately/src/datepicker/utils.ts @@ -53,6 +53,10 @@ export function getValidationResult( options: FormatterOptions, isValuePartial: boolean = false ): ValidationResult { + // A partial value blocks submission (isValuePartial -> invalid + valueMissing). Constraint + // errors are evaluated against the value as usual, so a partial value shows the same descriptive + // messages a complete invalid value would (e.g. "Value must be … or later."). Only when a + // partial value violates no constraint does it fall back to the generic incomplete message. let rangeOverflow = value != null && maxValue != null && value.compare(maxValue) > 0; let rangeUnderflow = value != null && minValue != null && value.compare(minValue) < 0; let isUnavailable = (value != null && isDateUnavailable?.(value)) || false; @@ -68,10 +72,6 @@ export function getValidationResult( let dateFormatter = new DateFormatter(locale, getFormatOptions({}, options)); let timeZone = dateFormatter.resolvedOptions().timeZone; - if (isValuePartial) { - errors.push(formatter.format('incompleteValue')); - } - if (rangeUnderflow && minValue != null) { errors.push( formatter.format('rangeUnderflow', { @@ -91,6 +91,10 @@ export function getValidationResult( if (isUnavailable) { errors.push(formatter.format('unavailableDate')); } + + if (isValuePartial && errors.length === 0) { + errors.push(formatter.format('incompleteValue')); + } } return { From ea18a4290104ea2400f241ecdfa510de19ab0da8 Mon Sep 17 00:00:00 2001 From: AK <144495202+AKnassa@users.noreply.github.com> Date: Fri, 22 May 2026 00:40:28 -0400 Subject: [PATCH 4/7] chore: extend missingTranslations script to scan intl subdirectories Previously only top-level intl/ dirs were checked, so the new incompleteValue key added under intl/datepicker/ was invisible to the translator tooling. Extended the glob to also scan intl/datepicker/ subdirectories. --- scripts/missingTranslations.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/missingTranslations.js b/scripts/missingTranslations.js index 96e226841e5..b67289ecf74 100644 --- a/scripts/missingTranslations.js +++ b/scripts/missingTranslations.js @@ -1,7 +1,15 @@ const glob = require('glob'); const fs = require('fs'); -for (let dir of glob.sync('packages/**/intl')) { +// Collect both top-level intl dirs and one level of named subdirs (e.g. intl/datepicker). +let dirs = [...glob.sync('packages/**/intl'), ...glob.sync('packages/**/intl/*/')].map(d => + d.endsWith('/') ? d.slice(0, -1) : d +); + +for (let dir of dirs) { + if (!fs.existsSync(`${dir}/en-US.json`)) { + continue; + } let en = JSON.parse(fs.readFileSync(`${dir}/en-US.json`, 'utf8')); for (let file of glob.sync('*.json', {cwd: dir})) { From 61c069e79dc557462239f2a63fb35fb744a3b036 Mon Sep 17 00:00:00 2001 From: AK <144495202+AKnassa@users.noreply.github.com> Date: Fri, 22 May 2026 00:40:42 -0400 Subject: [PATCH 5/7] i18n: add incompleteValue key to all datepicker locale files Adds "incompleteValue": "Please enter a value." as an English placeholder to all 33 remaining locale JSON files under intl/datepicker/. Real translations will be provided separately via the standard translation workflow. --- packages/react-stately/intl/datepicker/ar-AE.json | 3 ++- packages/react-stately/intl/datepicker/bg-BG.json | 3 ++- packages/react-stately/intl/datepicker/cs-CZ.json | 3 ++- packages/react-stately/intl/datepicker/da-DK.json | 3 ++- packages/react-stately/intl/datepicker/de-DE.json | 3 ++- packages/react-stately/intl/datepicker/el-GR.json | 3 ++- packages/react-stately/intl/datepicker/es-ES.json | 3 ++- packages/react-stately/intl/datepicker/et-EE.json | 3 ++- packages/react-stately/intl/datepicker/fi-FI.json | 3 ++- packages/react-stately/intl/datepicker/fr-FR.json | 3 ++- packages/react-stately/intl/datepicker/he-IL.json | 3 ++- packages/react-stately/intl/datepicker/hr-HR.json | 3 ++- packages/react-stately/intl/datepicker/hu-HU.json | 3 ++- packages/react-stately/intl/datepicker/it-IT.json | 3 ++- packages/react-stately/intl/datepicker/ja-JP.json | 3 ++- packages/react-stately/intl/datepicker/ko-KR.json | 3 ++- packages/react-stately/intl/datepicker/lt-LT.json | 3 ++- packages/react-stately/intl/datepicker/lv-LV.json | 3 ++- packages/react-stately/intl/datepicker/nb-NO.json | 3 ++- packages/react-stately/intl/datepicker/nl-NL.json | 3 ++- packages/react-stately/intl/datepicker/pl-PL.json | 3 ++- packages/react-stately/intl/datepicker/pt-BR.json | 3 ++- packages/react-stately/intl/datepicker/pt-PT.json | 3 ++- packages/react-stately/intl/datepicker/ro-RO.json | 3 ++- packages/react-stately/intl/datepicker/ru-RU.json | 3 ++- packages/react-stately/intl/datepicker/sk-SK.json | 3 ++- packages/react-stately/intl/datepicker/sl-SI.json | 3 ++- packages/react-stately/intl/datepicker/sr-SP.json | 3 ++- packages/react-stately/intl/datepicker/sv-SE.json | 3 ++- packages/react-stately/intl/datepicker/tr-TR.json | 3 ++- packages/react-stately/intl/datepicker/uk-UA.json | 3 ++- packages/react-stately/intl/datepicker/zh-CN.json | 3 ++- packages/react-stately/intl/datepicker/zh-TW.json | 3 ++- 33 files changed, 66 insertions(+), 33 deletions(-) diff --git a/packages/react-stately/intl/datepicker/ar-AE.json b/packages/react-stately/intl/datepicker/ar-AE.json index 3b0f019b1da..9c5532f5c6a 100644 --- a/packages/react-stately/intl/datepicker/ar-AE.json +++ b/packages/react-stately/intl/datepicker/ar-AE.json @@ -2,5 +2,6 @@ "rangeOverflow": "يجب أن تكون القيمة {maxValue} أو قبل ذلك.", "rangeReversed": "تاريخ البدء يجب أن يكون قبل تاريخ الانتهاء.", "rangeUnderflow": "يجب أن تكون القيمة {minValue} أو بعد ذلك.", - "unavailableDate": "البيانات المحددة غير متاحة." + "unavailableDate": "البيانات المحددة غير متاحة.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/bg-BG.json b/packages/react-stately/intl/datepicker/bg-BG.json index 13e7af5290f..fbdd59e56ae 100644 --- a/packages/react-stately/intl/datepicker/bg-BG.json +++ b/packages/react-stately/intl/datepicker/bg-BG.json @@ -2,5 +2,6 @@ "rangeOverflow": "Стойността трябва да е {maxValue} или по-ранна.", "rangeReversed": "Началната дата трябва да е преди крайната.", "rangeUnderflow": "Стойността трябва да е {minValue} или по-късно.", - "unavailableDate": "Избраната дата не е налична." + "unavailableDate": "Избраната дата не е налична.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/cs-CZ.json b/packages/react-stately/intl/datepicker/cs-CZ.json index 09c6c0dd327..44f574353d3 100644 --- a/packages/react-stately/intl/datepicker/cs-CZ.json +++ b/packages/react-stately/intl/datepicker/cs-CZ.json @@ -2,5 +2,6 @@ "rangeOverflow": "Hodnota musí být {maxValue} nebo dřívější.", "rangeReversed": "Datum zahájení musí předcházet datu ukončení.", "rangeUnderflow": "Hodnota musí být {minValue} nebo pozdější.", - "unavailableDate": "Vybrané datum není k dispozici." + "unavailableDate": "Vybrané datum není k dispozici.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/da-DK.json b/packages/react-stately/intl/datepicker/da-DK.json index 734f245e5e0..6991a7c676f 100644 --- a/packages/react-stately/intl/datepicker/da-DK.json +++ b/packages/react-stately/intl/datepicker/da-DK.json @@ -2,5 +2,6 @@ "rangeOverflow": "Værdien skal være {maxValue} eller tidligere.", "rangeReversed": "Startdatoen skal være før slutdatoen.", "rangeUnderflow": "Værdien skal være {minValue} eller nyere.", - "unavailableDate": "Den valgte dato er ikke tilgængelig." + "unavailableDate": "Den valgte dato er ikke tilgængelig.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/de-DE.json b/packages/react-stately/intl/datepicker/de-DE.json index a071ad8c4f1..5b1afffc9b9 100644 --- a/packages/react-stately/intl/datepicker/de-DE.json +++ b/packages/react-stately/intl/datepicker/de-DE.json @@ -2,5 +2,6 @@ "rangeOverflow": "Der Wert muss {maxValue} oder früher sein.", "rangeReversed": "Das Startdatum muss vor dem Enddatum liegen.", "rangeUnderflow": "Der Wert muss {minValue} oder später sein.", - "unavailableDate": "Das ausgewählte Datum ist nicht verfügbar." + "unavailableDate": "Das ausgewählte Datum ist nicht verfügbar.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/el-GR.json b/packages/react-stately/intl/datepicker/el-GR.json index d93e837a685..60e6939f44c 100644 --- a/packages/react-stately/intl/datepicker/el-GR.json +++ b/packages/react-stately/intl/datepicker/el-GR.json @@ -2,5 +2,6 @@ "rangeOverflow": "Η τιμή πρέπει να είναι {maxValue} ή παλαιότερη.", "rangeReversed": "Η ημερομηνία έναρξης πρέπει να είναι πριν από την ημερομηνία λήξης.", "rangeUnderflow": "Η τιμή πρέπει να είναι {minValue} ή μεταγενέστερη.", - "unavailableDate": "Η επιλεγμένη ημερομηνία δεν είναι διαθέσιμη." + "unavailableDate": "Η επιλεγμένη ημερομηνία δεν είναι διαθέσιμη.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/es-ES.json b/packages/react-stately/intl/datepicker/es-ES.json index a8ff4756927..0763acd3855 100644 --- a/packages/react-stately/intl/datepicker/es-ES.json +++ b/packages/react-stately/intl/datepicker/es-ES.json @@ -2,5 +2,6 @@ "rangeOverflow": "El valor debe ser {maxValue} o anterior.", "rangeReversed": "La fecha de inicio debe ser anterior a la fecha de finalización.", "rangeUnderflow": "El valor debe ser {minValue} o posterior.", - "unavailableDate": "Fecha seleccionada no disponible." + "unavailableDate": "Fecha seleccionada no disponible.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/et-EE.json b/packages/react-stately/intl/datepicker/et-EE.json index 84000193071..3dcddfab13d 100644 --- a/packages/react-stately/intl/datepicker/et-EE.json +++ b/packages/react-stately/intl/datepicker/et-EE.json @@ -2,5 +2,6 @@ "rangeOverflow": "Väärtus peab olema {maxValue} või varasem.", "rangeReversed": "Alguskuupäev peab olema enne lõppkuupäeva.", "rangeUnderflow": "Väärtus peab olema {minValue} või hilisem.", - "unavailableDate": "Valitud kuupäev pole saadaval." + "unavailableDate": "Valitud kuupäev pole saadaval.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/fi-FI.json b/packages/react-stately/intl/datepicker/fi-FI.json index a31f31e9e10..9238d395e8c 100644 --- a/packages/react-stately/intl/datepicker/fi-FI.json +++ b/packages/react-stately/intl/datepicker/fi-FI.json @@ -2,5 +2,6 @@ "rangeOverflow": "Arvon on oltava {maxValue} tai sitä aikaisempi.", "rangeReversed": "Aloituspäivän on oltava ennen lopetuspäivää.", "rangeUnderflow": "Arvon on oltava {minValue} tai sitä myöhäisempi.", - "unavailableDate": "Valittu päivämäärä ei ole käytettävissä." + "unavailableDate": "Valittu päivämäärä ei ole käytettävissä.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/fr-FR.json b/packages/react-stately/intl/datepicker/fr-FR.json index 70d6e4a0aff..cd1485760dd 100644 --- a/packages/react-stately/intl/datepicker/fr-FR.json +++ b/packages/react-stately/intl/datepicker/fr-FR.json @@ -2,5 +2,6 @@ "rangeOverflow": "La valeur doit être {maxValue} ou antérieure.", "rangeReversed": "La date de début doit être antérieure à la date de fin.", "rangeUnderflow": "La valeur doit être {minValue} ou ultérieure.", - "unavailableDate": "La date sélectionnée n’est pas disponible." + "unavailableDate": "La date sélectionnée n’est pas disponible.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/he-IL.json b/packages/react-stately/intl/datepicker/he-IL.json index e28b795968e..eb16b62069b 100644 --- a/packages/react-stately/intl/datepicker/he-IL.json +++ b/packages/react-stately/intl/datepicker/he-IL.json @@ -2,5 +2,6 @@ "rangeOverflow": "הערך חייב להיות {maxValue} או מוקדם יותר.", "rangeReversed": "תאריך ההתחלה חייב להיות לפני תאריך הסיום.", "rangeUnderflow": "הערך חייב להיות {minValue} או מאוחר יותר.", - "unavailableDate": "התאריך הנבחר אינו זמין." + "unavailableDate": "התאריך הנבחר אינו זמין.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/hr-HR.json b/packages/react-stately/intl/datepicker/hr-HR.json index 124c8c37347..e05385388ca 100644 --- a/packages/react-stately/intl/datepicker/hr-HR.json +++ b/packages/react-stately/intl/datepicker/hr-HR.json @@ -2,5 +2,6 @@ "rangeOverflow": "Vrijednost mora biti {maxValue} ili ranije.", "rangeReversed": "Datum početka mora biti prije datuma završetka.", "rangeUnderflow": "Vrijednost mora biti {minValue} ili kasnije.", - "unavailableDate": "Odabrani datum nije dostupan." + "unavailableDate": "Odabrani datum nije dostupan.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/hu-HU.json b/packages/react-stately/intl/datepicker/hu-HU.json index 657ba120ace..e0e49e519b3 100644 --- a/packages/react-stately/intl/datepicker/hu-HU.json +++ b/packages/react-stately/intl/datepicker/hu-HU.json @@ -2,5 +2,6 @@ "rangeOverflow": "Az értéknek {maxValue} vagy korábbinak kell lennie.", "rangeReversed": "A kezdő dátumnak a befejező dátumnál korábbinak kell lennie.", "rangeUnderflow": "Az értéknek {minValue} vagy későbbinek kell lennie.", - "unavailableDate": "A kiválasztott dátum nem érhető el." + "unavailableDate": "A kiválasztott dátum nem érhető el.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/it-IT.json b/packages/react-stately/intl/datepicker/it-IT.json index 0f5181756d5..0de7d93d43e 100644 --- a/packages/react-stately/intl/datepicker/it-IT.json +++ b/packages/react-stately/intl/datepicker/it-IT.json @@ -2,5 +2,6 @@ "rangeOverflow": "Il valore deve essere {maxValue} o precedente.", "rangeReversed": "La data di inizio deve essere antecedente alla data di fine.", "rangeUnderflow": "Il valore deve essere {minValue} o successivo.", - "unavailableDate": "Data selezionata non disponibile." + "unavailableDate": "Data selezionata non disponibile.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/ja-JP.json b/packages/react-stately/intl/datepicker/ja-JP.json index a015bd99619..efa99734ba0 100644 --- a/packages/react-stately/intl/datepicker/ja-JP.json +++ b/packages/react-stately/intl/datepicker/ja-JP.json @@ -2,5 +2,6 @@ "rangeOverflow": "値は {maxValue} 以下にする必要があります。", "rangeReversed": "開始日は終了日より前にする必要があります。", "rangeUnderflow": "値は {minValue} 以上にする必要があります。", - "unavailableDate": "選択した日付は使用できません。" + "unavailableDate": "選択した日付は使用できません。", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/ko-KR.json b/packages/react-stately/intl/datepicker/ko-KR.json index 4b813ba32d0..d93936274ed 100644 --- a/packages/react-stately/intl/datepicker/ko-KR.json +++ b/packages/react-stately/intl/datepicker/ko-KR.json @@ -2,5 +2,6 @@ "rangeOverflow": "값은 {maxValue} 이전이어야 합니다.", "rangeReversed": "시작일은 종료일 이전이어야 합니다.", "rangeUnderflow": "값은 {minValue} 이후여야 합니다.", - "unavailableDate": "선택한 날짜를 사용할 수 없습니다." + "unavailableDate": "선택한 날짜를 사용할 수 없습니다.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/lt-LT.json b/packages/react-stately/intl/datepicker/lt-LT.json index ea4f911c500..b982689cef9 100644 --- a/packages/react-stately/intl/datepicker/lt-LT.json +++ b/packages/react-stately/intl/datepicker/lt-LT.json @@ -2,5 +2,6 @@ "rangeOverflow": "Reikšmė turi būti {maxValue} arba ankstesnė.", "rangeReversed": "Pradžios data turi būti ankstesnė nei pabaigos data.", "rangeUnderflow": "Reikšmė turi būti {minValue} arba naujesnė.", - "unavailableDate": "Pasirinkta data nepasiekiama." + "unavailableDate": "Pasirinkta data nepasiekiama.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/lv-LV.json b/packages/react-stately/intl/datepicker/lv-LV.json index 99aa0615654..cb6bde7608a 100644 --- a/packages/react-stately/intl/datepicker/lv-LV.json +++ b/packages/react-stately/intl/datepicker/lv-LV.json @@ -2,5 +2,6 @@ "rangeOverflow": "Vērtībai ir jābūt {maxValue} vai agrākai.", "rangeReversed": "Sākuma datumam ir jābūt pirms beigu datuma.", "rangeUnderflow": "Vērtībai ir jābūt {minValue} vai vēlākai.", - "unavailableDate": "Atlasītais datums nav pieejams." + "unavailableDate": "Atlasītais datums nav pieejams.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/nb-NO.json b/packages/react-stately/intl/datepicker/nb-NO.json index b721b2964aa..1509839963d 100644 --- a/packages/react-stately/intl/datepicker/nb-NO.json +++ b/packages/react-stately/intl/datepicker/nb-NO.json @@ -2,5 +2,6 @@ "rangeOverflow": "Verdien må være {maxValue} eller tidligere.", "rangeReversed": "Startdatoen må være før sluttdatoen.", "rangeUnderflow": "Verdien må være {minValue} eller senere.", - "unavailableDate": "Valgt dato utilgjengelig." + "unavailableDate": "Valgt dato utilgjengelig.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/nl-NL.json b/packages/react-stately/intl/datepicker/nl-NL.json index 7d2ea1479d3..51d7cb66028 100644 --- a/packages/react-stately/intl/datepicker/nl-NL.json +++ b/packages/react-stately/intl/datepicker/nl-NL.json @@ -2,5 +2,6 @@ "rangeOverflow": "Waarde moet {maxValue} of eerder zijn.", "rangeReversed": "De startdatum moet voor de einddatum liggen.", "rangeUnderflow": "Waarde moet {minValue} of later zijn.", - "unavailableDate": "Geselecteerde datum niet beschikbaar." + "unavailableDate": "Geselecteerde datum niet beschikbaar.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/pl-PL.json b/packages/react-stately/intl/datepicker/pl-PL.json index 964c502dfb4..fe5ba93b635 100644 --- a/packages/react-stately/intl/datepicker/pl-PL.json +++ b/packages/react-stately/intl/datepicker/pl-PL.json @@ -2,5 +2,6 @@ "rangeOverflow": "Wartość musi mieć wartość {maxValue} lub wcześniejszą.", "rangeReversed": "Data rozpoczęcia musi być wcześniejsza niż data zakończenia.", "rangeUnderflow": "Wartość musi mieć wartość {minValue} lub późniejszą.", - "unavailableDate": "Wybrana data jest niedostępna." + "unavailableDate": "Wybrana data jest niedostępna.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/pt-BR.json b/packages/react-stately/intl/datepicker/pt-BR.json index a010d9b8a30..f0998c3b19d 100644 --- a/packages/react-stately/intl/datepicker/pt-BR.json +++ b/packages/react-stately/intl/datepicker/pt-BR.json @@ -2,5 +2,6 @@ "rangeOverflow": "O valor deve ser {maxValue} ou anterior.", "rangeReversed": "A data inicial deve ser anterior à data final.", "rangeUnderflow": "O valor deve ser {minValue} ou posterior.", - "unavailableDate": "Data selecionada indisponível." + "unavailableDate": "Data selecionada indisponível.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/pt-PT.json b/packages/react-stately/intl/datepicker/pt-PT.json index 09e60a5883c..8eefc585858 100644 --- a/packages/react-stately/intl/datepicker/pt-PT.json +++ b/packages/react-stately/intl/datepicker/pt-PT.json @@ -2,5 +2,6 @@ "rangeOverflow": "O valor tem de ser {maxValue} ou anterior.", "rangeReversed": "A data de início deve ser anterior à data de fim.", "rangeUnderflow": "O valor tem de ser {minValue} ou posterior.", - "unavailableDate": "Data selecionada indisponível." + "unavailableDate": "Data selecionada indisponível.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/ro-RO.json b/packages/react-stately/intl/datepicker/ro-RO.json index 13e1d3d61f8..1f634325cec 100644 --- a/packages/react-stately/intl/datepicker/ro-RO.json +++ b/packages/react-stately/intl/datepicker/ro-RO.json @@ -2,5 +2,6 @@ "rangeOverflow": "Valoarea trebuie să fie {maxValue} sau anterioară.", "rangeReversed": "Data de început trebuie să fie anterioară datei de sfârșit.", "rangeUnderflow": "Valoarea trebuie să fie {minValue} sau ulterioară.", - "unavailableDate": "Data selectată nu este disponibilă." + "unavailableDate": "Data selectată nu este disponibilă.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/ru-RU.json b/packages/react-stately/intl/datepicker/ru-RU.json index 574a82ce173..b3a3c841dda 100644 --- a/packages/react-stately/intl/datepicker/ru-RU.json +++ b/packages/react-stately/intl/datepicker/ru-RU.json @@ -2,5 +2,6 @@ "rangeOverflow": "Значение должно быть не позже {maxValue}.", "rangeReversed": "Дата начала должна предшествовать дате окончания.", "rangeUnderflow": "Значение должно быть не раньше {minValue}.", - "unavailableDate": "Выбранная дата недоступна." + "unavailableDate": "Выбранная дата недоступна.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/sk-SK.json b/packages/react-stately/intl/datepicker/sk-SK.json index 81aa69ba24b..741efacad94 100644 --- a/packages/react-stately/intl/datepicker/sk-SK.json +++ b/packages/react-stately/intl/datepicker/sk-SK.json @@ -2,5 +2,6 @@ "rangeOverflow": "Hodnota musí byť {maxValue} alebo skoršia.", "rangeReversed": "Dátum začiatku musí byť skorší ako dátum konca.", "rangeUnderflow": "Hodnota musí byť {minValue} alebo neskoršia.", - "unavailableDate": "Vybratý dátum je nedostupný." + "unavailableDate": "Vybratý dátum je nedostupný.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/sl-SI.json b/packages/react-stately/intl/datepicker/sl-SI.json index fdd641d5d7f..491f1fdc456 100644 --- a/packages/react-stately/intl/datepicker/sl-SI.json +++ b/packages/react-stately/intl/datepicker/sl-SI.json @@ -2,5 +2,6 @@ "rangeOverflow": "Vrednost mora biti {maxValue} ali starejša.", "rangeReversed": "Začetni datum mora biti pred končnim datumom.", "rangeUnderflow": "Vrednost mora biti {minValue} ali novejša.", - "unavailableDate": "Izbrani datum ni na voljo." + "unavailableDate": "Izbrani datum ni na voljo.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/sr-SP.json b/packages/react-stately/intl/datepicker/sr-SP.json index 57bf1027456..e03eb773917 100644 --- a/packages/react-stately/intl/datepicker/sr-SP.json +++ b/packages/react-stately/intl/datepicker/sr-SP.json @@ -2,5 +2,6 @@ "rangeOverflow": "Vrednost mora da bude {maxValue} ili starija.", "rangeReversed": "Datum početka mora biti pre datuma završetka.", "rangeUnderflow": "Vrednost mora da bude {minValue} ili novija.", - "unavailableDate": "Izabrani datum nije dostupan." + "unavailableDate": "Izabrani datum nije dostupan.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/sv-SE.json b/packages/react-stately/intl/datepicker/sv-SE.json index b32d8b705f5..5320c0216c1 100644 --- a/packages/react-stately/intl/datepicker/sv-SE.json +++ b/packages/react-stately/intl/datepicker/sv-SE.json @@ -2,5 +2,6 @@ "rangeOverflow": "Värdet måste vara {maxValue} eller tidigare.", "rangeReversed": "Startdatumet måste vara före slutdatumet.", "rangeUnderflow": "Värdet måste vara {minValue} eller senare.", - "unavailableDate": "Det valda datumet är inte tillgängligt." + "unavailableDate": "Det valda datumet är inte tillgängligt.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/tr-TR.json b/packages/react-stately/intl/datepicker/tr-TR.json index dd86809610d..0c6b0259d80 100644 --- a/packages/react-stately/intl/datepicker/tr-TR.json +++ b/packages/react-stately/intl/datepicker/tr-TR.json @@ -2,5 +2,6 @@ "rangeOverflow": "Değer, {maxValue} veya öncesi olmalıdır.", "rangeReversed": "Başlangıç tarihi bitiş tarihinden önce olmalıdır.", "rangeUnderflow": "Değer, {minValue} veya sonrası olmalıdır.", - "unavailableDate": "Seçilen tarih kullanılamıyor." + "unavailableDate": "Seçilen tarih kullanılamıyor.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/uk-UA.json b/packages/react-stately/intl/datepicker/uk-UA.json index d5e12390bfd..950f0f649d3 100644 --- a/packages/react-stately/intl/datepicker/uk-UA.json +++ b/packages/react-stately/intl/datepicker/uk-UA.json @@ -2,5 +2,6 @@ "rangeOverflow": "Значення має бути не пізніше {maxValue}.", "rangeReversed": "Дата початку має передувати даті завершення.", "rangeUnderflow": "Значення має бути не раніше {minValue}.", - "unavailableDate": "Вибрана дата недоступна." + "unavailableDate": "Вибрана дата недоступна.", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/zh-CN.json b/packages/react-stately/intl/datepicker/zh-CN.json index 4879c113fcd..6e53dfa0f14 100644 --- a/packages/react-stately/intl/datepicker/zh-CN.json +++ b/packages/react-stately/intl/datepicker/zh-CN.json @@ -2,5 +2,6 @@ "rangeOverflow": "值必须是 {maxValue} 或更早日期。", "rangeReversed": "开始日期必须早于结束日期。", "rangeUnderflow": "值必须是 {minValue} 或更晚日期。", - "unavailableDate": "所选日期不可用。" + "unavailableDate": "所选日期不可用。", + "incompleteValue": "Please enter a value." } diff --git a/packages/react-stately/intl/datepicker/zh-TW.json b/packages/react-stately/intl/datepicker/zh-TW.json index 61b0e36556a..b7ba589cf11 100644 --- a/packages/react-stately/intl/datepicker/zh-TW.json +++ b/packages/react-stately/intl/datepicker/zh-TW.json @@ -2,5 +2,6 @@ "rangeOverflow": "值必須是 {maxValue} 或更早。", "rangeReversed": "開始日期必須在結束日期之前。", "rangeUnderflow": "值必須是 {minValue} 或更晚。", - "unavailableDate": "所選日期無法使用。" + "unavailableDate": "所選日期無法使用。", + "incompleteValue": "Please enter a value." } From 939953f92a4decd78b0d60e5e8747c61e47eef32 Mon Sep 17 00:00:00 2001 From: AK <144495202+AKnassa@users.noreply.github.com> Date: Fri, 22 May 2026 00:40:53 -0400 Subject: [PATCH 6/7] test: add edge-case regressions for partial-value validation and calendar-selection fix Adds 3 new test cases in DatePicker and 1 in DateRangePicker covering: - Descriptive constraint error (not generic) when partial value violates min/max - "Takes two interactions" calendar-selection fix for DatePicker - Time-segment clearing on CalendarDateTime (issue #9801) - "Takes two interactions" calendar-selection fix for DateRangePicker --- .../test/datepicker/DatePicker.test.js | 147 ++++++++++++++++++ .../test/datepicker/DateRangePicker.test.js | 58 +++++++ 2 files changed, 205 insertions(+) diff --git a/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js b/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js index edf1789de64..a2628f51960 100644 --- a/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js +++ b/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js @@ -3237,6 +3237,153 @@ describe('DatePicker', function () { expect(description).toContain('Please enter a value.'); }); + it('should surface the descriptive constraint error (not the generic incomplete message) when a min-violating value is made partial (Bug #9958)', async () => { + // A partial value that still violates a constraint (minValue/maxValue/unavailableDate) + // surfaces the descriptive error ("Value must be … or later.") rather than the generic + // fallback ("Please enter a value."). The generic fallback only appears when the partial + // value has no constraint violation — e.g. a field with no min/max where the user clears + // a segment. + let {getByRole} = render( + +
+ + +
+ ); + + 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}'); + expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty'); + + await user.tab(); + await user.tab(); + await user.tab(); + + let description = (group.getAttribute('aria-describedby') || '') + .split(' ') + .map(d => document.getElementById(d)?.textContent || '') + .join(' '); + + expect(input.validity.valid).toBe(false); + expect(description).toContain('Value must be 1/1/2030 or later.'); + expect(description).not.toContain('Please enter a value.'); + }); + + it('should clear the partial-value error on the first calendar selection that completes the date (Bug #9958 follow-up)', async () => { + // Repro of the "takes two interactions" bug: after a partial value surfaces the error, + // selecting a complete valid date from the calendar must clear it on the FIRST selection. + // The parent's isValuePartial is lifted from the field via a useEffect (one render + // behind), but selectDate commits validation synchronously — so the stale partial error + // got committed and stuck until a second interaction. + let {getByRole, getAllByRole} = render( + +
+ + +
+ ); + + let group = getByRole('group'); + let input = document.querySelector('input[name=date]'); + let segments = within(group).getAllByRole('spinbutton'); + + // Clear the month segment, then blur the field -> partial error appears. + act(() => { segments[0].focus(); }); + await user.keyboard('{Backspace}'); + expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty'); + 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.'); + + // Open the calendar and select a complete, valid date in a single click. + await user.click(getByRole('button')); + let cells = getAllByRole('gridcell'); + let selected = cells.find(cell => cell.getAttribute('aria-selected') === 'true'); + await user.click(selected.nextSibling.children[0]); + + // The value is now complete and valid -> the error must be gone after ONE selection. + expect(input.validity.valid).toBe(true); + expect(getDescription()).not.toContain('Please enter a value.'); + }); + + it('should signal validation when a time segment is cleared on a CalendarDateTime value (Bug #9801)', async () => { + // Issue #9801 is TimeField — the same IncompleteDate partial-validation path applies + // when a DatePicker has CalendarDateTime granularity and a time segment is cleared. + // The committed value is unchanged but the display buffer is partial; the field must + // be invalid and surface "Please enter a value." until the segment is refilled. + let {getByRole} = render( + +
+ + +
+ ); + + let group = getByRole('group'); + let input = document.querySelector('input[name=date]'); + let segments = within(group).getAllByRole('spinbutton'); + + // Confirm the full value is valid. + expect(input.validity.valid).toBe(true); + + // Clear the hour segment (10 → two Backspaces needed for two digits). + act(() => { + segments[3].focus(); + }); + await user.keyboard('{Backspace}{Backspace}'); + expect(segments[3]).toHaveAttribute('aria-valuetext', 'Empty'); + + // Tab out of the group so blur-triggered validation runs. + await user.tab(); + await user.tab(); + await user.tab(); + await user.tab(); + + let getDescription = () => + (group.getAttribute('aria-describedby') || '') + .split(' ') + .map(d => document.getElementById(d)?.textContent || '') + .join(' '); + expect(input.validity.valid).toBe(false); + expect(getDescription()).toContain('Please enter a value.'); + + // Refill the hour segment and confirm the error clears. + act(() => { + segments[3].focus(); + }); + await user.keyboard('10'); + await user.tab(); + expect(input.validity.valid).toBe(true); + }); + it('supports minValue and maxValue', async () => { let {getByRole, getByTestId} = render( diff --git a/packages/@adobe/react-spectrum/test/datepicker/DateRangePicker.test.js b/packages/@adobe/react-spectrum/test/datepicker/DateRangePicker.test.js index cc68fbb9b33..a1ec731964e 100644 --- a/packages/@adobe/react-spectrum/test/datepicker/DateRangePicker.test.js +++ b/packages/@adobe/react-spectrum/test/datepicker/DateRangePicker.test.js @@ -2261,6 +2261,64 @@ describe('DateRangePicker', function () { expect(input.validity.valid).toBe(true); expect(getDescription()).not.toContain('Constraints not satisfied'); }); + + it('should clear the partial-value error on the first calendar selection that completes the range (Bug #9958 follow-up)', async () => { + // Repro of the "takes two interactions" bug for DateRangePicker: after a partial start + // endpoint surfaces "Please enter a value.", selecting a complete range from the calendar + // must clear it on the FIRST selection. Both setStartIsValuePartial and + // setEndIsValuePartial must be reset synchronously before commitValidation(). + let {getByRole} = render( + +
+ + +
+ ); + + let group = getByRole('group'); + let startInput = document.querySelector('input[name=start]'); + let segments = within(group).getAllByRole('spinbutton'); + + // Clear the start month segment, then blur the field -> partial error appears. + act(() => { segments[0].focus(); }); + await user.keyboard('{Backspace}'); + expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty'); + // Tab past remaining start segments, end segments, and calendar trigger button. + await user.tab(); + await user.tab(); + await user.tab(); + await user.tab(); + 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.'); + + // Open the calendar and select a complete range with two clicks on the focused cell. + // The calendar opens with focus on the highlighted start date; clicking it sets the + // range start, clicking the same focused cell again sets the range end. + await user.click(getByRole('button')); + await user.click(document.activeElement); + await user.click(document.activeElement); + + // The range is now complete and valid -> error must be gone after ONE selection sequence. + expect(startInput.validity.valid).toBe(true); + expect(getDescription()).not.toContain('Please enter a value.'); + }); }); describe('validationBehavior=aria', () => { From 610b628a3923f1c9b6b5de63569afd6e957e6adb Mon Sep 17 00:00:00 2001 From: AK <144495202+AKnassa@users.noreply.github.com> Date: Fri, 22 May 2026 01:00:49 -0400 Subject: [PATCH 7/7] chore: fix formatting in datepicker files --- .../test/datepicker/DatePicker.test.js | 12 +++++++++--- .../test/datepicker/DateRangePicker.test.js | 4 +++- .../src/datepicker/useDateFieldState.ts | 9 +-------- .../src/datepicker/useDatePickerState.ts | 13 ++----------- 4 files changed, 15 insertions(+), 23 deletions(-) diff --git a/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js b/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js index a2628f51960..e597cfe4338 100644 --- a/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js +++ b/packages/@adobe/react-spectrum/test/datepicker/DatePicker.test.js @@ -3220,7 +3220,9 @@ describe('DatePicker', function () { let segments = within(group).getAllByRole('spinbutton'); // Clear the month segment (single-digit '4' goes straight to null). - act(() => { segments[0].focus(); }); + act(() => { + segments[0].focus(); + }); await user.keyboard('{Backspace}'); expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty'); @@ -3261,7 +3263,9 @@ describe('DatePicker', function () { let input = document.querySelector('input[name=date]'); let segments = within(group).getAllByRole('spinbutton'); - act(() => { segments[0].focus(); }); + act(() => { + segments[0].focus(); + }); await user.keyboard('{Backspace}'); expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty'); @@ -3304,7 +3308,9 @@ describe('DatePicker', function () { let segments = within(group).getAllByRole('spinbutton'); // Clear the month segment, then blur the field -> partial error appears. - act(() => { segments[0].focus(); }); + act(() => { + segments[0].focus(); + }); await user.keyboard('{Backspace}'); expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty'); await user.tab(); diff --git a/packages/@adobe/react-spectrum/test/datepicker/DateRangePicker.test.js b/packages/@adobe/react-spectrum/test/datepicker/DateRangePicker.test.js index a1ec731964e..297c646026a 100644 --- a/packages/@adobe/react-spectrum/test/datepicker/DateRangePicker.test.js +++ b/packages/@adobe/react-spectrum/test/datepicker/DateRangePicker.test.js @@ -2289,7 +2289,9 @@ describe('DateRangePicker', function () { let segments = within(group).getAllByRole('spinbutton'); // Clear the start month segment, then blur the field -> partial error appears. - act(() => { segments[0].focus(); }); + act(() => { + segments[0].focus(); + }); await user.keyboard('{Backspace}'); expect(segments[0]).toHaveAttribute('aria-valuetext', 'Empty'); // Tab past remaining start segments, end segments, and calendar trigger button. diff --git a/packages/react-stately/src/datepicker/useDateFieldState.ts b/packages/react-stately/src/datepicker/useDateFieldState.ts index e7b49f99783..854cf39b00c 100644 --- a/packages/react-stately/src/datepicker/useDateFieldState.ts +++ b/packages/react-stately/src/datepicker/useDateFieldState.ts @@ -375,14 +375,7 @@ export function useDateFieldState( let builtinValidation = useMemo( () => - getValidationResult( - value, - minValue, - maxValue, - isDateUnavailable, - formatOpts, - isValuePartial - ), + getValidationResult(value, minValue, maxValue, isDateUnavailable, formatOpts, isValuePartial), [value, minValue, maxValue, isDateUnavailable, formatOpts, isValuePartial] ); diff --git a/packages/react-stately/src/datepicker/useDatePickerState.ts b/packages/react-stately/src/datepicker/useDatePickerState.ts index e0cbec68ffc..7591c4b7eab 100644 --- a/packages/react-stately/src/datepicker/useDatePickerState.ts +++ b/packages/react-stately/src/datepicker/useDatePickerState.ts @@ -163,14 +163,7 @@ export function useDatePickerState( let builtinValidation = useMemo( () => - getValidationResult( - value, - minValue, - maxValue, - isDateUnavailable, - formatOpts, - isValuePartial - ), + getValidationResult(value, minValue, maxValue, isDateUnavailable, formatOpts, isValuePartial), [value, minValue, maxValue, isDateUnavailable, formatOpts, isValuePartial] ); @@ -185,9 +178,7 @@ export function useDatePickerState( props.validationState || (isValueInvalid ? 'invalid' : null); let commitValue = (date: DateValue, time: TimeValue) => { - setValue( - 'timeZone' in time ? time.set(toCalendarDate(date)) : toCalendarDateTime(date, time) - ); + setValue('timeZone' in time ? time.set(toCalendarDate(date)) : toCalendarDateTime(date, time)); setSelectedDate(null); setSelectedTime(null); validation.commitValidation();