Repository navigation
TimePicker: Add minYear prop and Invalid Date guard #75285
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
base: trunk
Are you sure you want to change the base?
Changes from all commits
6aed081
6bc1c0e
2694fdb
aa523f2
449e372
ae51f37
e765736
dd4cc13
5debd1d
9a63972
a15735c
f21f9d8
7b00a4e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -633,4 +633,236 @@ describe( 'TimePicker', () => { | |
| expect( outputDate ).not.toContain( 'Invalid' ); | ||
| } ); | ||
| } ); | ||
|
|
||
| describe( 'Invalid date handling', () => { | ||
| it( 'should not emit invalid dates when year is cleared and blurred', async () => { | ||
|
Member
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. Is this test necessary? It seems 90% like a repetition of the test introduced in #75343. If we want to keep both tests, let's ensure we remove the repetition, no need to test the same things multiple times. |
||
| const user = userEvent.setup(); | ||
| const onChangeSpy = jest.fn(); | ||
|
|
||
| render( | ||
| <TimePicker | ||
| currentTime="1986-10-18T11:00:00" | ||
| onChange={ onChangeSpy } | ||
| is12Hour | ||
| /> | ||
| ); | ||
|
|
||
| const yearInput = screen.getByLabelText( 'Year' ); | ||
|
|
||
| // Clear the year field and blur | ||
| await user.clear( yearInput ); | ||
| await user.keyboard( '{Tab}' ); | ||
|
|
||
| // Verify the input field value after browser coercion | ||
| expect( yearInput ).toHaveValue( 1 ); | ||
|
|
||
| // When the year is cleared, the browser may coerce to minYear. | ||
| // If onChange is called, it should only emit valid date strings. | ||
| onChangeSpy.mock.calls.forEach( ( call ) => { | ||
| const dateString = call[ 0 ]; | ||
| expect( dateString ).toMatch( | ||
| /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/ | ||
| ); | ||
| expect( dateString ).not.toContain( 'NaN' ); | ||
| expect( dateString ).not.toContain( 'Invalid' ); | ||
| } ); | ||
| } ); | ||
|
|
||
| it( 'should not call onChange with an invalid date when day is cleared and month is changed', async () => { | ||
| const user = userEvent.setup(); | ||
| const onChangeSpy = jest.fn(); | ||
|
|
||
| render( | ||
| <TimePicker | ||
| currentTime="1986-10-18T11:00:00" | ||
| onChange={ onChangeSpy } | ||
| is12Hour | ||
| /> | ||
| ); | ||
|
|
||
| const dayInput = screen.getByLabelText( 'Day' ); | ||
| const monthInput = screen.getByLabelText( 'Month' ); | ||
|
|
||
| // Clear the day field | ||
| await user.clear( dayInput ); | ||
| // Change the month (this would create an invalid date if day is 0) | ||
| await user.selectOptions( monthInput, '12' ); | ||
|
|
||
| // Verify the input field value after browser coercion | ||
| expect( dayInput ).toHaveValue( 1 ); | ||
|
|
||
| // Verify that if onChange was called, it only received valid date strings. | ||
| // Our guard should prevent invalid dates from being emitted, so any calls | ||
| // should have properly formatted dates without "NaN" or "Invalid". | ||
| onChangeSpy.mock.calls.forEach( ( call ) => { | ||
| const dateString = call[ 0 ]; | ||
| expect( dateString ).toMatch( | ||
| /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/ | ||
| ); | ||
| expect( dateString ).not.toContain( 'NaN' ); | ||
| expect( dateString ).not.toContain( 'Invalid' ); | ||
| } ); | ||
| } ); | ||
|
|
||
| it( 'should handle years below 1000 when minYear allows them', async () => { | ||
| const user = userEvent.setup(); | ||
| const onChangeSpy = jest.fn(); | ||
|
|
||
| render( | ||
| <TimePicker | ||
| currentTime="1986-10-18T11:00:00" | ||
| onChange={ onChangeSpy } | ||
| minYear={ 1 } | ||
|
Member
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. Why is this test relevant? |
||
| is12Hour | ||
| /> | ||
| ); | ||
|
|
||
| const yearInput = screen.getByLabelText( 'Year' ); | ||
|
|
||
| // Clear the year field and blur - browser will coerce to minYear (1) | ||
| await user.clear( yearInput ); | ||
| await user.keyboard( '{Tab}' ); | ||
|
|
||
| // Verify the input field value after browser coercion to minYear | ||
| expect( yearInput ).toHaveValue( 1 ); | ||
|
|
||
| // Verify that if onChange was called, it only received valid date strings | ||
| onChangeSpy.mock.calls.forEach( ( call ) => { | ||
| const dateString = call[ 0 ]; | ||
| expect( dateString ).toMatch( | ||
| /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/ | ||
| ); | ||
| expect( dateString ).not.toContain( 'NaN' ); | ||
| expect( dateString ).not.toContain( 'Invalid' ); | ||
| } ); | ||
| } ); | ||
| } ); | ||
|
|
||
| describe( 'Years below 1000 support', () => { | ||
| it( 'should handle year 999 with zero-padded output', async () => { | ||
| const user = userEvent.setup(); | ||
| const onChangeSpy = jest.fn(); | ||
|
|
||
| render( | ||
| <TimePicker | ||
| currentTime="0999-10-18T11:00:00" | ||
| onChange={ onChangeSpy } | ||
| minYear={ 1 } | ||
| /> | ||
| ); | ||
|
|
||
| const yearInput = screen.getByLabelText( 'Year' ); | ||
|
|
||
| // Verify input displays human-readable year (not zero-padded) | ||
| expect( yearInput ).toHaveValue( 999 ); | ||
|
|
||
| // Change to year 500 | ||
| await user.clear( yearInput ); | ||
| await user.type( yearInput, '500' ); | ||
| await user.keyboard( '{Tab}' ); | ||
|
|
||
| // Verify input displays human-readable year | ||
| expect( yearInput ).toHaveValue( 500 ); | ||
|
|
||
| // Verify onChange emits zero-padded ISO 8601 format | ||
| const lastCall = | ||
| onChangeSpy.mock.calls[ onChangeSpy.mock.calls.length - 1 ]; | ||
| expect( lastCall[ 0 ] ).toMatch( /^0500-10-18T/ ); | ||
| expect( lastCall[ 0 ] ).not.toContain( 'NaN' ); | ||
| } ); | ||
|
|
||
| it( 'should handle year 1 with zero-padded output', async () => { | ||
| const user = userEvent.setup(); | ||
| const onChangeSpy = jest.fn(); | ||
|
|
||
| render( | ||
| <TimePicker | ||
| currentTime="0001-01-01T12:00:00" | ||
| onChange={ onChangeSpy } | ||
| minYear={ 1 } | ||
| /> | ||
| ); | ||
|
|
||
| const yearInput = screen.getByLabelText( 'Year' ); | ||
|
|
||
| // Verify input displays human-readable year (not zero-padded) | ||
| expect( yearInput ).toHaveValue( 1 ); | ||
|
|
||
| // Change to year 99 | ||
| await user.clear( yearInput ); | ||
| await user.type( yearInput, '99' ); | ||
| await user.keyboard( '{Tab}' ); | ||
|
|
||
| // Verify input displays human-readable year | ||
| expect( yearInput ).toHaveValue( 99 ); | ||
|
|
||
| // Verify onChange emits zero-padded ISO 8601 format | ||
| const lastCall = | ||
| onChangeSpy.mock.calls[ onChangeSpy.mock.calls.length - 1 ]; | ||
| expect( lastCall[ 0 ] ).toMatch( /^0099-01-01T/ ); | ||
| } ); | ||
|
|
||
| it( 'should allow years below 1000 when minYear permits', async () => { | ||
| const user = userEvent.setup(); | ||
| const onChangeSpy = jest.fn(); | ||
|
|
||
| render( | ||
| <TimePicker | ||
| currentTime="1986-10-18T11:00:00" | ||
| onChange={ onChangeSpy } | ||
| minYear={ 1 } | ||
|
Member
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. Do we need to specify |
||
| /> | ||
| ); | ||
|
|
||
| const yearInput = screen.getByLabelText( 'Year' ); | ||
|
|
||
| // Verify initial input displays correctly | ||
| expect( yearInput ).toHaveValue( 1986 ); | ||
|
|
||
| // Type year 500 - should be allowed | ||
| await user.clear( yearInput ); | ||
| await user.type( yearInput, '500' ); | ||
| await user.keyboard( '{Tab}' ); | ||
|
|
||
| // Verify input displays human-readable year (not zero-padded) | ||
| expect( yearInput ).toHaveValue( 500 ); | ||
|
|
||
| // Verify onChange was called with zero-padded ISO 8601 format | ||
| expect( onChangeSpy ).toHaveBeenCalled(); | ||
| const lastCall = | ||
| onChangeSpy.mock.calls[ onChangeSpy.mock.calls.length - 1 ]; | ||
| expect( lastCall[ 0 ] ).toMatch( /^0500-/ ); | ||
| } ); | ||
|
|
||
| it( 'should handle year 500 correctly', async () => { | ||
|
Member
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. What meaningful addition does this contribute in addition to the "should handle year 999 with zero-padded output" test? |
||
| const user = userEvent.setup(); | ||
| const onChangeSpy = jest.fn(); | ||
|
|
||
| render( | ||
| <TimePicker | ||
| currentTime="0500-06-15T09:30:00" | ||
| onChange={ onChangeSpy } | ||
| minYear={ 1 } | ||
| /> | ||
| ); | ||
|
|
||
| const yearInput = screen.getByLabelText( 'Year' ); | ||
|
|
||
| // Verify input displays human-readable year (not zero-padded) | ||
| expect( yearInput ).toHaveValue( 500 ); | ||
|
|
||
| // Change to year 750 | ||
| await user.clear( yearInput ); | ||
| await user.type( yearInput, '750' ); | ||
| await user.keyboard( '{Tab}' ); | ||
|
|
||
| // Verify input displays human-readable year | ||
| expect( yearInput ).toHaveValue( 750 ); | ||
|
|
||
| // Verify onChange emits zero-padded ISO 8601 format | ||
| const lastCall = | ||
| onChangeSpy.mock.calls[ onChangeSpy.mock.calls.length - 1 ]; | ||
| expect( lastCall[ 0 ] ).toMatch( /^0750-06-15T/ ); | ||
| } ); | ||
| } ); | ||
| } ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,13 @@ export type TimePickerProps = { | |
| * @default false | ||
| */ | ||
| hideLabelFromVision?: boolean; | ||
|
|
||
| /** | ||
| * The minimum year that can be selected in the year input field. | ||
| * | ||
| * @default 1 | ||
|
Contributor
Author
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. @tyxla Shall we just default to 1900 or something realistic?
Member
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. I'm totally fine with keeping it |
||
| */ | ||
| minYear?: number; | ||
| }; | ||
|
|
||
| export type TimeInputValue = { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this check necessary now? How can the user input an invalid date?