-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[pickers] Keep invalid date state consistent #20040
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
michelengelen
wants to merge
13
commits into
mui:master
Choose a base branch
from
michelengelen:bugfix/17967
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+201
−18
Open
Changes from 12 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
1404482
Keep invalid date state consistent to avoid transient clearing during…
michelengelen 0449d5c
Merge branch 'master' into bugfix/17967
michelengelen e043f59
Merge branch 'master' into bugfix/17967
michelengelen f727d2a
Merge branch 'master' into bugfix/17967
michelengelen 140b3c4
Merge branch 'master' into bugfix/17967
michelengelen c904747
fixed tests
michelengelen ff5cd46
removed some verbose comments
michelengelen 9f7f7c3
Merge branch 'master' into bugfix/17967
michelengelen 58cc0b4
removed unused imports
michelengelen dc91eef
Merge remote-tracking branch 'origin/bugfix/17967' into bugfix/17967
michelengelen 8f0b0dc
make prettier CI happy
michelengelen 97c9771
Merge branch 'master' into bugfix/17967
michelengelen 637b65c
review fixes
michelengelen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
78 changes: 78 additions & 0 deletions
78
packages/x-date-pickers/src/DateField/tests/blurPartialFilling.DateField.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { DateField } from '@mui/x-date-pickers/DateField'; | ||
| import { describeAdapters, getTextbox, getFieldInputRoot } from 'test/utils/pickers'; | ||
| import { fireEvent } from '@mui/internal-test-utils'; | ||
|
|
||
| // Tests that on blur, partially filled fields are considered invalid | ||
| // while completely empty or fully valid fields remain not invalid. | ||
|
|
||
| describeAdapters( | ||
| 'DateField - partial filling on blur', | ||
| DateField, | ||
| ({ adapter, renderWithProps }) => { | ||
| it('marks field invalid on blur when only some sections are filled (accessible DOM)', async () => { | ||
| const view = renderWithProps({ enableAccessibleFieldDOMStructure: true }); | ||
|
|
||
| await view.selectSectionAsync('month'); | ||
| await view.user.keyboard('0'); | ||
| await view.user.keyboard('1'); | ||
|
|
||
| const fieldRoot = getFieldInputRoot(); | ||
|
|
||
| // While focused and partially filled, it should not be invalid yet | ||
| expect(fieldRoot).to.have.attribute('aria-invalid', 'false'); | ||
|
|
||
| // Blur the sections container to trigger validation in accessible DOM | ||
| await view.user.tab(); | ||
|
|
||
| expect(fieldRoot).to.have.attribute('aria-invalid', 'true'); | ||
|
|
||
| view.unmount(); | ||
| }); | ||
|
|
||
| it('does not mark invalid on blur when all sections are empty (accessible DOM)', async () => { | ||
| const view = renderWithProps({ enableAccessibleFieldDOMStructure: true }); | ||
|
|
||
| // Focus a section then blur without typing | ||
| await view.selectSectionAsync('month'); | ||
| await view.user.tab(); | ||
|
|
||
| expect(getFieldInputRoot()).to.have.attribute('aria-invalid', 'false'); | ||
|
|
||
| view.unmount(); | ||
| }); | ||
|
|
||
| it('does not mark invalid on blur when value is fully valid (accessible DOM)', async () => { | ||
| const view = renderWithProps({ | ||
| enableAccessibleFieldDOMStructure: true, | ||
| defaultValue: adapter.date('2025-01-15'), | ||
| }); | ||
|
|
||
| // Focus and blur | ||
| await view.selectSectionAsync('month'); | ||
| await view.user.tab(); | ||
|
|
||
| expect(getFieldInputRoot()).to.have.attribute('aria-invalid', 'false'); | ||
|
|
||
| view.unmount(); | ||
| }); | ||
|
|
||
| it('marks field invalid on blur when only some sections are filled (non-accessible DOM)', async () => { | ||
| const view = renderWithProps({ enableAccessibleFieldDOMStructure: false }); | ||
|
|
||
| await view.selectSectionAsync('month'); | ||
| const input = getTextbox(); | ||
|
|
||
| // Partially fill the month: "01/DD/YYYY" | ||
| fireEvent.change(input, { target: { value: '01/DD/YYYY' } }); | ||
|
|
||
| expect(input).to.have.attribute('aria-invalid', 'false'); | ||
|
|
||
| // Blur the input in non-accessible DOM | ||
| fireEvent.blur(input); | ||
|
|
||
| expect(input).to.have.attribute('aria-invalid', 'true'); | ||
|
|
||
| view.unmount(); | ||
| }); | ||
| }, | ||
| ); |
64 changes: 64 additions & 0 deletions
64
packages/x-date-pickers/src/DateField/tests/invalidStateKeyboard.DateField.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { DateField } from '@mui/x-date-pickers/DateField'; | ||
| import { describeAdapters, getTextbox, getFieldInputRoot } from 'test/utils/pickers'; | ||
|
|
||
| // Regression: invalid state should not temporarily clear during keyboard spin when sections are still invalid | ||
| // Reproduction steps covered: | ||
| // 1. Type an invalid month ("00") | ||
| // 2. Move focus to the year section | ||
| // 3. Press ArrowUp/ArrowDown to spin year | ||
| // Expectation: aria-invalid remains true while the overall value is still invalid | ||
|
|
||
| describeAdapters( | ||
| 'DateField - sticky invalid state during keyboard spin', | ||
| DateField, | ||
| ({ renderWithProps, adapter }) => { | ||
| it('keeps aria-invalid=true while spinning year when month is invalid (accessible DOM)', async () => { | ||
| const view = renderWithProps({ enableAccessibleFieldDOMStructure: true }); // default format is numeric, e.g. MM/DD/YYYY | ||
|
|
||
| // Make month invalid by typing "00" | ||
| await view.selectSectionAsync('month'); | ||
| await view.user.keyboard('00'); | ||
| await view.user.tab(); | ||
|
|
||
| // Should be invalid now | ||
| expect(getFieldInputRoot()).to.have.attribute('aria-invalid', 'true'); | ||
|
|
||
| // Move to year and spin | ||
| await view.selectSectionAsync('year'); | ||
| await view.user.keyboard('[ArrowUp][ArrowUp][ArrowDown]'); | ||
| await view.user.tab(); | ||
|
|
||
| // Still invalid, must not flash to valid between spins | ||
| expect(getFieldInputRoot()).to.have.attribute('aria-invalid', 'true'); | ||
|
|
||
| view.unmount(); | ||
| }); | ||
|
|
||
| it('keeps aria-invalid=true while spinning year when month is invalid (non-accessible DOM)', async () => { | ||
| // moment and luxon validation seem to not work | ||
| if (['luxon', 'moment'].includes(adapter.lib)) { | ||
| return; | ||
| } | ||
|
|
||
| const view = renderWithProps({ enableAccessibleFieldDOMStructure: false }); // default format is numeric, e.g. MM/DD/YYYY | ||
|
|
||
| await view.selectSectionAsync('month'); | ||
| const input = getTextbox(); | ||
|
|
||
| await view.selectSectionAsync('month'); | ||
| await view.user.keyboard('0'); | ||
| await view.user.tab(); | ||
|
|
||
| expect(input).to.have.attribute('aria-invalid', 'true'); | ||
|
|
||
| // Move to year and spin using keypress | ||
| await view.selectSectionAsync('year'); | ||
| await view.user.keyboard('[ArrowUp][ArrowUp][ArrowDown]'); | ||
| await view.user.tab(); | ||
|
|
||
| expect(input).to.have.attribute('aria-invalid', 'true'); | ||
|
|
||
| view.unmount(); | ||
| }); | ||
| }, | ||
| ); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,16 +107,6 @@ export const useFieldState = < | |
| onError: internalPropsWithDefaults.onError, | ||
| }); | ||
|
|
||
| const error = React.useMemo(() => { | ||
| // only override when `error` is undefined. | ||
| // in case of multi input fields, the `error` value is provided externally and will always be defined. | ||
| if (errorProp !== undefined) { | ||
| return errorProp; | ||
| } | ||
|
|
||
| return hasValidationError; | ||
| }, [hasValidationError, errorProp]); | ||
|
|
||
| const localizedDigits = React.useMemo(() => getLocalizedDigits(adapter), [adapter]); | ||
|
|
||
| const sectionsValueBoundaries = React.useMemo( | ||
|
|
@@ -209,6 +199,55 @@ export const useFieldState = < | |
| [state.sections], | ||
| ); | ||
|
|
||
| // Keep invalid state "sticky" while sections still represent an invalid date. | ||
| const hasInvalidSectionValue = React.useMemo(() => { | ||
| if (state.sections.every((s) => s.value === '')) { | ||
| return false; | ||
| } | ||
|
|
||
| const allFilled = state.sections.every((s) => s.value !== ''); | ||
| if (!allFilled) { | ||
| return false; | ||
| } | ||
|
||
|
|
||
| if (activeSectionIndex == null) { | ||
| return false; | ||
| } | ||
|
|
||
| const activeDateSections = fieldValueManager.getDateSectionsFromValue( | ||
| state.sections, | ||
| state.sections[activeSectionIndex] as any, | ||
| ); | ||
|
|
||
| const dateFromSections = getDateFromDateSections( | ||
| adapter, | ||
| activeDateSections as any, | ||
| localizedDigits, | ||
| ); | ||
| return !adapter.isValid(dateFromSections); | ||
| }, [adapter, fieldValueManager, state.sections, activeSectionIndex, localizedDigits]); | ||
|
|
||
| // When the field loses focus (no active section), consider partially filled sections as invalid. | ||
| // This enforces that the field must be entirely filled or entirely empty on blur. | ||
| const hasPartiallyFilledSectionsOnBlur = React.useMemo(() => { | ||
| if (activeSectionIndex != null) { | ||
| return false; | ||
| } | ||
|
|
||
| const someFilled = state.sections.some((s) => s.value !== ''); | ||
| const someEmpty = state.sections.some((s) => s.value === ''); | ||
flaviendelangle marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return someFilled && someEmpty; | ||
| }, [state.sections, activeSectionIndex]); | ||
|
|
||
| const error = React.useMemo(() => { | ||
| if (errorProp !== undefined) { | ||
| return errorProp; | ||
| } | ||
|
|
||
| return hasValidationError || hasInvalidSectionValue || hasPartiallyFilledSectionsOnBlur; | ||
| }, [hasValidationError, hasInvalidSectionValue, hasPartiallyFilledSectionsOnBlur, errorProp]); | ||
|
|
||
| const publishValue = (newValue: TValue) => { | ||
| const context: FieldChangeHandlerContext<TError> = { | ||
| validationError: validator({ | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -517,6 +517,7 @@ export const useFieldV6TextField = < | |
| // Forwarded | ||
| ...forwardedProps, | ||
| error, | ||
| 'aria-invalid': error, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just curious, does this mean introducing new behavior? |
||
| clearable: Boolean(clearable && !areAllSectionsEmpty && !readOnly && !disabled), | ||
| onBlur: handleContainerBlur, | ||
| onClick: handleInputClick, | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.