diff --git a/packages/react-core/src/components/DatePicker/DatePicker.tsx b/packages/react-core/src/components/DatePicker/DatePicker.tsx index a10f4f9640e..4ab92e187dc 100644 --- a/packages/react-core/src/components/DatePicker/DatePicker.tsx +++ b/packages/react-core/src/components/DatePicker/DatePicker.tsx @@ -12,6 +12,14 @@ import { KeyTypes } from '../../helpers'; import { isValidDate } from '../../helpers/datetimeUtils'; import { HelperText, HelperTextItem } from '../HelperText'; +/** Props that customize the requirement of a date */ +export interface DatePickerRequiredObject { + /** Flag indicating the date is required. */ + isRequired?: boolean; + /** Error message to display when the text input is empty and the isRequired prop is also passed in. */ + emptyDateText?: string; +} + /** The main date picker component. */ export interface DatePickerProps @@ -32,7 +40,7 @@ export interface DatePickerProps className?: string; /** How to format the date in the text input. */ dateFormat?: (date: Date) => string; - /** How to format the date in the text input. */ + /** How to parse the date in the text input. */ dateParse?: (value: string) => Date; /** Helper text to display alongside the date picker. Expects a HelperText component. */ helperText?: React.ReactNode; @@ -40,7 +48,7 @@ export interface DatePickerProps inputProps?: TextInputProps; /** Flag indicating the date picker is disabled. */ isDisabled?: boolean; - /** Error message to display when the text input cannot be parsed. */ + /** Error message to display when the text input contains a non-empty value in an invalid format. */ invalidFormatText?: string; /** Callback called every time the text input loses focus. */ onBlur?: (event: any, value: string, date?: Date) => void; @@ -50,6 +58,8 @@ export interface DatePickerProps placeholder?: string; /** Props to pass to the popover that contains the calendar month component. */ popoverProps?: Partial>; + /** Options to customize the requirement of a date */ + requiredDateOptions?: DatePickerRequiredObject; /** Functions that returns an error message if a date is invalid. */ validators?: ((date: Date) => string)[]; /** Value of the text input. */ @@ -95,6 +105,7 @@ const DatePickerBase = ( onChange = (): any => undefined, onBlur = (): any => undefined, invalidFormatText = 'Invalid date', + requiredDateOptions, helperText, appendTo = 'inline', popoverProps, @@ -122,6 +133,7 @@ const DatePickerBase = ( const buttonRef = React.useRef(); const datePickerWrapperRef = React.useRef(); const triggerRef = React.useRef(); + const emptyDateText = requiredDateOptions?.emptyDateText || 'Date cannot be blank'; React.useEffect(() => { setValue(valueProp); @@ -153,17 +165,22 @@ const DatePickerBase = ( }; const onInputBlur = (event: any) => { - if (pristine) { - return; - } const newValueDate = dateParse(value); - if (isValidDate(newValueDate)) { - onBlur(event, value, new Date(newValueDate)); + const dateIsValid = isValidDate(newValueDate); + const onBlurDateArg = dateIsValid ? new Date(newValueDate) : undefined; + onBlur(event, value, onBlurDateArg); + + if (dateIsValid) { setError(newValueDate); - } else { - onBlur(event, value); + } + + if (!dateIsValid && !pristine) { setErrorText(invalidFormatText); } + + if (!dateIsValid && pristine && requiredDateOptions?.isRequired) { + setErrorText(emptyDateText); + } }; const onDateClick = (_event: React.MouseEvent, newValueDate: Date) => { @@ -236,6 +253,10 @@ const DatePickerBase = ( event.stopPropagation(); setPopoverOpen(false); hideFunction(); + // If datepicker is required and the popover is opened without the text input + // first receiving focus, we want to validate that the text input is not blank upon + // closing the popover + requiredDateOptions?.isRequired && !value && setErrorText(emptyDateText); } if (event.key === KeyTypes.Escape && popoverOpen) { event.stopPropagation(); @@ -254,6 +275,7 @@ const DatePickerBase = ( { expect(screen.getByText('Help me')).toBeVisible(); }); -test('Shows "Invalid date" instead of helperText when an error exists', async () => { +test('Shows "Invalid date" instead of helperText when text input contains invalid date', async () => { const user = userEvent.setup(); render( @@ -99,3 +99,90 @@ test('Shows "Invalid date" instead of helperText when an error exists', async () expect(screen.queryByText('Help me')).not.toBeInTheDocument(); expect(screen.getByText('Invalid date')).toBeVisible(); }); + +test('Does not render text input as invalid when requiredDateOptions.isRequired is false', async () => { + const user = userEvent.setup(); + + render(); + + await user.click(screen.getByRole('textbox')); + await user.click(document.body); + + expect(screen.getByRole('textbox')).not.toHaveAttribute('aria-invalid', 'true'); +}); + +test('Does not render emptyDateText when requiredDateOptions.isRequired is false', async () => { + const user = userEvent.setup(); + + render(); + + await user.click(screen.getByRole('textbox')); + await user.click(document.body); + + expect(screen.queryByText('Date cannot be blank')).not.toBeInTheDocument; +}); + +test('Renders text input as invalid on blur when requiredDateOptions.isRequired is true', async () => { + const user = userEvent.setup(); + + render(); + + await user.click(screen.getByRole('textbox')); + await user.click(document.body); + + expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'true'); +}); + +test('Renders default emptyDateText on blur when requiredDateOptions.isRequired is true', async () => { + const user = userEvent.setup(); + + render(); + + await user.click(screen.getByRole('textbox')); + await user.click(document.body); + + expect(screen.getByText('Date cannot be blank')).toBeInTheDocument(); +}); + +test('Renders custom emptyDateText when requiredDateOptions.isRequired is true', async () => { + const user = userEvent.setup(); + + render(); + + await user.click(screen.getByRole('textbox')); + await user.click(document.body); + + expect(screen.getByText('Required in test')).toBeInTheDocument(); +}); + +test('Shows emptyDateText instead of helperText when text input is empty and requiredDateOptions.isRequired is true', async () => { + const user = userEvent.setup(); + + render( + + Help me + + } + /> + ); + + await user.click(screen.getByRole('textbox')); + await user.click(document.body); + + expect(screen.queryByText('Help me')).not.toBeInTheDocument(); + expect(screen.getByText('Date cannot be blank')).toBeVisible(); +}); + +test('Renders text input as invalid when requiredDateOptions.isRequired is true and popover is closed without selection', async () => { + const user = userEvent.setup(); + + render(); + + await user.click(screen.getByRole('button', { name: 'Toggle date picker' })); + await user.click(document.body); + + expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'true'); +}); diff --git a/packages/react-core/src/components/DatePicker/examples/DatePicker.md b/packages/react-core/src/components/DatePicker/examples/DatePicker.md index 6648cc1a33e..4d5959b29b0 100644 --- a/packages/react-core/src/components/DatePicker/examples/DatePicker.md +++ b/packages/react-core/src/components/DatePicker/examples/DatePicker.md @@ -3,7 +3,7 @@ id: Date picker section: components subsection: date-and-time cssPrefix: pf-v5-c-date-picker -propComponents: ['DatePicker', 'CalendarFormat', 'DatePickerRef'] +propComponents: ['DatePicker', 'CalendarFormat', 'DatePickerRef', 'DatePickerRequiredObject'] --- ## Examples @@ -11,34 +11,53 @@ propComponents: ['DatePicker', 'CalendarFormat', 'DatePickerRef'] ### Basic ```ts file="./DatePickerBasic.tsx" + +``` + +### Required + +To require users to select a date before continuing, use the `requiredDateOptions.isRequired` property. + +A required date picker will be invalid when the text input is empty and either the text input loses focus or the date picker popover is closed. + +The error message can be customized via the `requiredDateOptions.emptyDateText` property. + +```ts file="./DatePickerRequired.tsx" + ``` ### American format ```ts file="./DatePickerAmerican.tsx" + ``` ### Helper text ```ts file="./DatePickerHelperText.tsx" + ``` ### Min and max date ```ts file="./DatePickerMinMax.tsx" + ``` ### French ```ts file="./DatePickerFrench.tsx" + ``` ### Controlled ```ts file="./DatePickerControlled.tsx" + ``` ### Controlling the date picker calendar state ```ts file="./DatePickerControlledCalendar.tsx" + ``` diff --git a/packages/react-core/src/components/DatePicker/examples/DatePickerRequired.tsx b/packages/react-core/src/components/DatePicker/examples/DatePickerRequired.tsx new file mode 100644 index 00000000000..fbeb0f1febb --- /dev/null +++ b/packages/react-core/src/components/DatePicker/examples/DatePickerRequired.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { DatePicker } from '@patternfly/react-core'; + +export const DatePickerRequired: React.FunctionComponent = () => ( + +);