Skip to content

Commit

Permalink
fix(DatePicker): updated onBlur logic for empty date (patternfly#9373)
Browse files Browse the repository at this point in the history
* fix(DatePicker): updated onBlur logic for empty date

* Updated example description

* Grouped isRequired and emptyDateText props as one
  • Loading branch information
thatblindgeye authored and nicolethoen committed Sep 1, 2023
1 parent d10c025 commit 481b134
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 11 deletions.
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' }} />
);

0 comments on commit 481b134

Please sign in to comment.