package.src.components.DatePicker.DatePicker.tsx Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of react-core Show documentation
Show all versions of react-core Show documentation
This library provides a set of common React components for use with the PatternFly reference implementation.
The newest version!
import * as React from 'react';
import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/DatePicker/date-picker';
import buttonStyles from '@patternfly/react-styles/css/components/Button/button';
import calendarMonthStyles from '@patternfly/react-styles/css/components/CalendarMonth/calendar-month';
import { TextInput, TextInputProps } from '../TextInput/TextInput';
import { Popover, PopoverProps } from '../Popover/Popover';
import { InputGroup, InputGroupItem } from '../InputGroup';
import OutlinedCalendarAltIcon from '@patternfly/react-icons/dist/esm/icons/outlined-calendar-alt-icon';
import { CalendarMonth, CalendarFormat } from '../CalendarMonth';
import { useImperativeHandle } from 'react';
import { KeyTypes } from '../../helpers';
import { isValidDate } from '../../helpers/datetimeUtils';
import { HelperText, HelperTextItem } from '../HelperText';
import cssFormControlWidthChars from '@patternfly/react-tokens/dist/esm/c_date_picker__input_c_form_control_width_chars';
/** 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
extends CalendarFormat,
Omit, 'onChange' | 'onFocus' | 'onBlur' | 'disabled' | 'ref'> {
/** The container to append the menu to. Defaults to 'inline'.
* If your menu is being cut off you can append it to an element higher up the DOM tree.
* Some examples:
* menuAppendTo={() => document.body};
* menuAppendTo={document.getElementById('target')}
*/
appendTo?: HTMLElement | ((ref?: HTMLElement) => HTMLElement) | 'inline';
/** Accessible label for the date picker. */
'aria-label'?: string;
/** Accessible label for the button to open the date picker. */
buttonAriaLabel?: string;
/** Additional classes added to the date picker. */
className?: string;
/** How to format the date in the text input. */
dateFormat?: (date: Date) => string;
/** 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 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;
/** Callback called every time the text input value changes. */
onChange?: (event: React.FormEvent, value: string, date?: Date) => void;
/** String to display in the empty text input as a hint for the expected date format. */
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. */
value?: string;
}
/** Allows finer control over the calendar's open state when a React ref is passed into the
* date picker component. Accessed via ref.current[property], e.g. ref.current.toggleCalendar().
*/
export interface DatePickerRef {
/** Current calendar open status. */
isCalendarOpen: boolean;
/** Sets the calendar open status. */
setCalendarOpen: (isOpen: boolean) => void;
/** Toggles the calendar open status. If no parameters are passed, the calendar will simply
* toggle its open status.
* If the isOpen parameter is passed, that will set the calendar open status to the value
* of the isOpen parameter.
* If the eventKey parameter is set to 'Escape', that will invoke the date pickers
* onEscapePress event to toggle the correct control appropriately.
*/
toggleCalendar: (isOpen?: boolean) => void;
}
export const yyyyMMddFormat = (date: Date) =>
`${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date
.getDate()
.toString()
.padStart(2, '0')}`;
const DatePickerBase = (
{
className,
locale = undefined,
dateFormat = yyyyMMddFormat,
dateParse = (val: string) => (val.split('-').length === 3 ? new Date(`${val}T00:00:00`) : new Date(undefined)),
isDisabled = false,
placeholder = 'YYYY-MM-DD',
value: valueProp = '',
'aria-label': ariaLabel = 'Date picker',
buttonAriaLabel = 'Toggle date picker',
onChange = (): any => undefined,
onBlur = (): any => undefined,
invalidFormatText = 'Invalid date',
requiredDateOptions,
helperText,
appendTo = 'inline',
popoverProps,
monthFormat,
weekdayFormat,
longWeekdayFormat,
dayFormat,
weekStart,
validators = [],
rangeStart,
style: styleProps = {},
inputProps = {},
...props
}: DatePickerProps,
ref: React.Ref
) => {
const [value, setValue] = React.useState(valueProp);
const [valueDate, setValueDate] = React.useState(dateParse(value));
const [errorText, setErrorText] = React.useState('');
const [popoverOpen, setPopoverOpen] = React.useState(false);
const [selectOpen, setSelectOpen] = React.useState(false);
const [pristine, setPristine] = React.useState(true);
const [textInputFocused, setTextInputFocused] = React.useState(false);
const widthChars = React.useMemo(() => Math.max(dateFormat(new Date()).length, placeholder.length), [dateFormat]);
const style = { [cssFormControlWidthChars.name]: widthChars, ...styleProps };
const buttonRef = React.useRef();
const datePickerWrapperRef = React.useRef();
const triggerRef = React.useRef();
const dateIsRequired = requiredDateOptions?.isRequired || false;
const emptyDateText = requiredDateOptions?.emptyDateText || 'Date cannot be blank';
React.useEffect(() => {
setValue(valueProp);
setValueDate(dateParse(valueProp));
}, [valueProp]);
React.useEffect(() => {
setPristine(!value);
const newValueDate = dateParse(value);
if (errorText && isValidDate(newValueDate)) {
setError(newValueDate);
}
if (value === '' && !pristine && !textInputFocused) {
dateIsRequired ? setErrorText(emptyDateText) : setErrorText('');
}
}, [value]);
const setError = (date: Date) => {
setErrorText(validators.map((validator) => validator(date)).join('\n') || '');
};
const onTextInput = (event: React.FormEvent, value: string) => {
setValue(value);
setErrorText('');
const newValueDate = dateParse(value);
setValueDate(newValueDate);
if (isValidDate(newValueDate)) {
onChange(event, value, new Date(newValueDate));
} else {
onChange(event, value);
}
};
const onInputBlur = (event: any) => {
setTextInputFocused(false);
const newValueDate = dateParse(value);
const dateIsValid = isValidDate(newValueDate);
const onBlurDateArg = dateIsValid ? new Date(newValueDate) : undefined;
onBlur(event, value, onBlurDateArg);
if (dateIsValid) {
setError(newValueDate);
}
if (!dateIsValid && !pristine) {
setErrorText(invalidFormatText);
}
if (!dateIsValid && pristine && requiredDateOptions?.isRequired) {
setErrorText(emptyDateText);
}
};
const onDateClick = (_event: React.MouseEvent, newValueDate: Date) => {
const newValue = dateFormat(newValueDate);
setValue(newValue);
setValueDate(newValueDate);
setError(newValueDate);
setPopoverOpen(false);
onChange(null, newValue, new Date(newValueDate));
};
const onKeyPress = (ev: React.KeyboardEvent) => {
if (ev.key === 'Enter' && value) {
if (isValidDate(valueDate)) {
setError(valueDate);
} else {
setErrorText(invalidFormatText);
}
}
};
useImperativeHandle(
ref,
() => ({
setCalendarOpen: (isOpen: boolean) => setPopoverOpen(isOpen),
toggleCalendar: (setOpen?: boolean) => {
setPopoverOpen((prev) => (setOpen !== undefined ? setOpen : !prev));
},
isCalendarOpen: popoverOpen
}),
[setPopoverOpen, popoverOpen, selectOpen]
);
const createFocusSelectorString = (modifierClass: string) =>
`.${calendarMonthStyles.calendarMonthDatesCell}.${modifierClass} .${calendarMonthStyles.calendarMonthDate}`;
const focusSelectorForSelectedDate = createFocusSelectorString(calendarMonthStyles.modifiers.selected);
const focusSelectorForUnselectedDate = createFocusSelectorString(calendarMonthStyles.modifiers.current);
return (
(date: Date) => !validator(date))}
onSelectToggle={(open) => setSelectOpen(open)}
monthFormat={monthFormat}
weekdayFormat={weekdayFormat}
longWeekdayFormat={longWeekdayFormat}
dayFormat={dayFormat}
weekStart={weekStart}
rangeStart={rangeStart}
/>
}
showClose={false}
isVisible={popoverOpen}
shouldClose={(event, hideFunction) => {
event = event as KeyboardEvent;
if (event.key === KeyTypes.Escape && selectOpen) {
event.stopPropagation();
setSelectOpen(false);
return false;
}
// Let our button handle toggling
if (buttonRef.current && buttonRef.current.contains(event.target as Node)) {
return false;
}
if (popoverOpen) {
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();
}
return true;
}}
withFocusTrap
hasNoPadding
hasAutoWidth
appendTo={appendTo}
triggerRef={triggerRef}
{...popoverProps}
>
setTextInputFocused(true)}
onKeyPress={onKeyPress}
{...inputProps}
/>
{(errorText || helperText) && (
{errorText ? (
{errorText}
) : (
helperText
)}
)}
);
};
export const DatePicker = React.forwardRef(DatePickerBase);
DatePicker.displayName = 'DatePicker';