Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 31 additions & 9 deletions packages/react-core/src/components/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,15 +40,15 @@ 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;
/** Additional props for the text input. */
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;
Expand All @@ -50,6 +58,8 @@ export interface DatePickerProps
placeholder?: string;
/** Props to pass to the popover that contains the calendar month component. */
popoverProps?: Partial<Omit<PopoverProps, 'appendTo'>>;
/** 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. */
Expand Down Expand Up @@ -95,6 +105,7 @@ const DatePickerBase = (
onChange = (): any => undefined,
onBlur = (): any => undefined,
invalidFormatText = 'Invalid date',
requiredDateOptions,
helperText,
appendTo = 'inline',
popoverProps,
Expand Down Expand Up @@ -122,6 +133,7 @@ const DatePickerBase = (
const buttonRef = React.useRef<HTMLButtonElement>();
const datePickerWrapperRef = React.useRef<HTMLDivElement>();
const triggerRef = React.useRef<HTMLDivElement>();
const emptyDateText = requiredDateOptions?.emptyDateText || 'Date cannot be blank';

React.useEffect(() => {
setValue(valueProp);
Expand Down Expand Up @@ -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<HTMLButtonElement, MouseEvent>, newValueDate: Date) => {
Expand Down Expand Up @@ -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();
Expand All @@ -254,6 +275,7 @@ const DatePickerBase = (
<InputGroupItem isFill>
<TextInput
isDisabled={isDisabled}
isRequired={requiredDateOptions?.isRequired}
aria-label={ariaLabel}
placeholder={placeholder}
validated={errorText.trim() ? 'error' : 'default'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ test('Shows helperText instead of "Invalid date" when no error exists', () => {
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(
Expand All @@ -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(<DatePicker />);

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(<DatePicker />);

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(<DatePicker requiredDateOptions={{ isRequired: true }} />);

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(<DatePicker requiredDateOptions={{ isRequired: true }} />);

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(<DatePicker requiredDateOptions={{ isRequired: true, emptyDateText: 'Required in test' }} />);

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(
<DatePicker
requiredDateOptions={{ isRequired: true }}
helperText={
<HelperText>
<HelperTextItem>Help me</HelperTextItem>
</HelperText>
}
/>
);

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(<DatePicker requiredDateOptions={{ isRequired: true }} />);

await user.click(screen.getByRole('button', { name: 'Toggle date picker' }));
await user.click(document.body);

expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'true');
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,61 @@ 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

### 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"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import { DatePicker } from '@patternfly/react-core';

export const DatePickerRequired: React.FunctionComponent = () => (
<DatePicker requiredDateOptions={{ isRequired: true, emptyDateText: 'Date is required' }} />
);