terra-date-picker
Version:
The terra-date-picker component provides users a way to enter or select a date from the date picker.
900 lines (817 loc) • 33.2 kB
JSX
import React, {
useState,
useEffect,
useMemo,
useRef,
} from 'react';
import {
KEY_UP,
KEY_DOWN,
KEY_LEFT,
KEY_RIGHT,
KEY_DELETE,
KEY_BACK_SPACE,
KEY_TAB,
} from 'keycode-js';
import { injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import classNames from 'classnames/bind';
import moment from 'moment-timezone';
import { v4 as uuidv4 } from 'uuid';
import Button from 'terra-button';
import IconCalendar from 'terra-icon/lib/icon/IconCalendar';
import Input from 'terra-form-input';
import ThemeContext from 'terra-theme-context';
import VisuallyHiddenText from 'terra-visually-hidden-text';
import DateUtil from './DateUtil';
import DateInputLayout from './_DateInputLayout';
import { getLocalizedDateForScreenReader } from './react-datepicker/date_utils';
import styles from './DatePicker.module.scss';
const cx = classNames.bind(styles);
const propTypes = {
/**
* String that labels the current element. 'aria-label' must be present for accessibility.
*/
ariaLabel: PropTypes.string,
/**
* Callback ref to pass into the calendar button dom element.
*/
buttonRefCallback: PropTypes.func,
/**
* Callback ref to pass into the first input dom element from Date Input components based on the date format order.
*/
firstInputRefCallback: PropTypes.func,
/**
* The id to append to the date input wrapper.
*/
id: PropTypes.string,
/**
* @private
* Timezone value to indicate in which timezone the date component is rendered.
* The value provided should be a valid [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) string, else will default to browser/local timezone.
*/
initialTimeZone: PropTypes.string,
/**
* Custom input attributes to apply to the date input.
*/
inputAttributes: PropTypes.object,
/**
* @private
* intl object programmatically imported through injectIntl from react-intl.
* */
intl: PropTypes.shape({ formatMessage: PropTypes.func, locale: PropTypes.string }).isRequired,
/**
* Whether the input displays as Incomplete. Use when no value has been provided. _(usage note: `required` must also be set)_.
*/
isIncomplete: PropTypes.bool,
/**
* Whether the input displays as Invalid. Use when value does not meet validation pattern.
*/
isInvalid: PropTypes.bool,
/**
* Name of the date input.
*/
name: PropTypes.string,
/**
* A callback function triggered when the input or calendar button loses focus.
*/
onBlur: PropTypes.func,
/**
* A callback function triggered when the calendar button receives focus.
*/
onButtonFocus: PropTypes.func,
/**
* A callback function to execute when a valid date is selected or entered.
*/
onChange: PropTypes.func,
/**
* The onInputClick callback function from react-datepicker to show the picker when clicked.
*/
onClick: PropTypes.func,
/**
* A callback function triggered when the date input receives focus.
*/
onFocus: PropTypes.func,
/**
* The onInputKeyDown callback function from react-datepicker to handle keyboard navigation.
*/
onKeyDown: PropTypes.func,
/**
* Whether or not the date is required.
*/
required: PropTypes.bool,
/**
* @private
* Internal prop for showing date picker.
*/
shouldShowPicker: PropTypes.bool,
/**
* @private
* NOTICE: Internal prop to be used only by Terra framework. This component provides a built-in format mask that is
* required to be displayed to users for proper accessibility and must not be removed. 'DatePickerField' is permitted to set
* this prop because it provides the same format mask in its 'help' prop.
*/
useExternalFormatMask: PropTypes.bool,
/**
* The selected or entered date value to display in the date input.
*/
value: PropTypes.string,
/**
* @private
* An array of {@link moment} date objects of the dates to disable in the picker.
*/
excludeDates: PropTypes.arrayOf(PropTypes.objectOf(moment)),
/**
* @private
* An array of {@link moment} date objects of the dates to enable in the picker. All Other dates will be disabled.
*/
includeDates: PropTypes.arrayOf(PropTypes.objectOf(moment)),
/**
* @private
* An ISO 8601 string representation of the maximum date that can be selected. The value must be in the `YYYY-MM-DD` format. Must be on or before `12/31/2100`.
*/
maxDate: PropTypes.string,
/**
* @private
* An ISO 8601 string representation of the minimum date that can be selected. The value must be in the `YYYY-MM-DD` format. Must be on or after `01/01/1900`
*/
minDate: PropTypes.string,
/**
* A function that gets called for each date in the picker to evaluate which date should be disabled.
* A return value of true will be enabled and false will be disabled.
*/
filterDate: PropTypes.func,
/**
* .
* If invalid error text is used, provide a string containing the IDs for error html element.
* ID must be htmlFor prop value with error text.
*/
errorId: PropTypes.string,
};
const defaultProps = {
ariaLabel: undefined,
buttonRefCallback: undefined,
id: undefined,
inputAttributes: undefined,
isIncomplete: false,
isInvalid: false,
name: undefined,
onBlur: undefined,
onButtonFocus: undefined,
onChange: undefined,
onClick: undefined,
onFocus: undefined,
onKeyDown: undefined,
required: false,
useExternalFormatMask: false,
value: undefined,
excludeDates: undefined,
includeDates: undefined,
maxDate: '2100-12-31',
minDate: '1900-01-01',
filterDate: undefined,
errorId: '',
};
const DatePickerInput = (props) => {
const {
ariaLabel,
buttonRefCallback,
firstInputRefCallback,
id,
initialTimeZone,
inputAttributes,
intl,
isIncomplete,
isInvalid,
name,
onBlur,
onButtonFocus,
onChange,
onClick,
onFocus,
onKeyDown,
required,
useExternalFormatMask,
value,
excludeDates,
includeDates,
maxDate,
minDate,
filterDate,
errorId,
...customProps
} = props;
const [isFocused, setFocused] = useState(false);
const [dayInitialFocused, setDayInitialFocused] = useState(false);
const [monthInitialFocused, setMonthInitialFocused] = useState(false);
const [yearInitialFocused, setYearInitialFocused] = useState(false);
const editOnkeyDown = useRef(false);
const theme = React.useContext(ThemeContext);
// variables to store ref's for day, month and year input
let dayInputRef;
let monthInputRef;
let yearInputRef;
let visuallyHiddenComponent = null;
const { onCalendarButtonClick, shouldShowPicker } = customProps;
delete customProps.onCalendarButtonClick;
delete customProps.shouldShowPicker;
let idFromInputAttributes;
let monthInputId;
let dayInputId;
let yearInputId;
if (inputAttributes && inputAttributes.id) {
// Get the inputAttributes.id and set it on the outer div and delete inputAttributes.id to prevent from setting the same id on all three inputs.
// Create new ids to set on each input using the inputAttributes.id.
idFromInputAttributes = inputAttributes.id;
monthInputId = idFromInputAttributes.concat('-terra-date-picker-month');
dayInputId = idFromInputAttributes.concat('-terra-date-picker-day');
yearInputId = idFromInputAttributes.concat('-terra-date-picker-year');
delete inputAttributes.id;
}
const additionalInputProps = { ...customProps, ...inputAttributes };
const momentDateFormat = useMemo(() => DateUtil.getFormatByLocale(intl.locale), [intl.locale]);
const dateValue = useMemo(() => (DateUtil.convertToISO8601(value, momentDateFormat)), [momentDateFormat, value]);
const dateFormatOrder = DateUtil.getDateFormatOrder(momentDateFormat);
const separator = DateUtil.getDateSeparator(intl.locale);
const previousDateValueRef = useRef();
let date = useMemo(() => ({ day: '', month: '', year: '' }), []);
// Sets the date state based on the passed in value prop, or if it changes via a calendar click.
if (DateUtil.isValidDate(value, momentDateFormat)) {
date = DateUtil.getDateInputValues(DateUtil.dateOrder.YMD, dateValue, '-');
previousDateValueRef.current = date;
editOnkeyDown.current = false;
} else {
date = DateUtil.validdateDateValues(value, dateFormatOrder, editOnkeyDown, previousDateValueRef);
}
// Triggers the onClick callback to launch the dropdown picker for the scenario when the default date is invalid and
// the calendar button is clicked which should clear the value and launch the dropdown picker
useEffect(() => {
if (shouldShowPicker && onClick) {
onClick();
}
}, [shouldShowPicker, onClick]);
/**
* Moves focus to the correct input depending on date ordering. Focus changing is
* disabled if a complete date has been entered in order to make single input
* corrections easier, and is re-enabled if the whole date is erased.
* @param {string} inputValue - The value from the current input.
* @param {number} type - The input type, based on DateUtil.inputType.
*/
const moveFocusOnChange = (inputValue, type) => {
if (dateFormatOrder === DateUtil.dateOrder.MDY) {
if (inputValue.length === 2) {
if (type === DateUtil.inputType.MONTH) {
dayInputRef.focus();
} else {
yearInputRef.focus();
}
}
} else if (dateFormatOrder === DateUtil.dateOrder.DMY) {
if (inputValue.length === 2) {
if (type === DateUtil.inputType.DAY) {
monthInputRef.focus();
} else {
yearInputRef.focus();
}
}
} else if (dateFormatOrder === DateUtil.dateOrder.YMD) {
if (inputValue.length === 4) {
monthInputRef.focus();
} else if (inputValue.length === 2 && type === DateUtil.inputType.MONTH) {
dayInputRef.focus();
}
}
};
/**
* Sets date value for the day, month, and year.
* @param {object} event - Event object
* @param {string} inputValue - The input value to set in state.
* @param {number} type - The inputType (day, month, or year).
*/
const setDate = (event, inputValue, type) => {
if (type === DateUtil.inputType.DAY) {
date.day = inputValue;
} else if (type === DateUtil.inputType.MONTH) {
date.month = inputValue;
} else {
date.year = inputValue;
}
if (event.type !== 'keydown') {
moveFocusOnChange(inputValue, type);
}
};
const handleInvalidInputChange = (inputValue) => {
if (visuallyHiddenComponent && inputValue !== '+' && inputValue !== '=' && inputValue !== '-'
&& inputValue !== '_' && inputValue !== 't' && inputValue !== 'T') {
visuallyHiddenComponent.innerText = intl.formatMessage({ id: 'Terra.datePicker.invalidDate' });
}
};
const setVisuallyHiddenComponent = (node) => {
visuallyHiddenComponent = node;
};
/**
* Sets the day, month and year based on input values, formats them
* based on the date format variant, and passes the formatted date to onChange.
*/
const handleDateChange = (event, inputValue, type) => {
let { day, month, year } = date;
if (type === DateUtil.inputType.DAY) {
day = inputValue;
setDayInitialFocused(false);
} else if (type === DateUtil.inputType.MONTH) {
month = inputValue;
setMonthInitialFocused(false);
} else {
year = inputValue;
setYearInitialFocused(false);
}
let inputDate;
let formattedDate;
if (day.length === 2 && month.length === 2 && year.length === 4) {
inputDate = DateUtil.convertToISO8601(`${year}-${month}-${day}`, DateUtil.ISO_EXTENDED_DATE_FORMAT);
formattedDate = DateUtil.strictFormatISODate(inputDate, momentDateFormat);
}
if (onChange) {
if (DateUtil.isValidDate(formattedDate, momentDateFormat)) {
onChange(event, formattedDate);
} else if (day === '' && month === '' && year === '') {
onChange(event, '');
} else {
let dateString;
if (dateFormatOrder === DateUtil.dateOrder.MDY) {
dateString = month + separator + day + separator + year;
} else if (dateFormatOrder === DateUtil.dateOrder.DMY) {
dateString = day + separator + month + separator + year;
} else {
dateString = year + separator + month + separator + day;
}
onChange(event, dateString);
}
}
setDate(event, inputValue, type);
};
const handleDayChange = (event) => {
let inputValue = event.target.value;
if (!DateUtil.validDateInput(inputValue)) {
handleInvalidInputChange(inputValue);
return;
}
// Ignore the entry if the value did not change or it is invalid.
// When 'Predictive text' is enabled on Android the maxLength attribute on the input is ignored so we have to
// check the length of inputValue to make sure that it is less then 2.
if (inputValue === date.day || inputValue.length > 2 || Number(inputValue) > 31 || inputValue === '00') {
handleInvalidInputChange(inputValue);
return;
}
// If the change made was not a deletion of a digit, then prepend '0'
// when the input value is a single digit value between 4 and 9
if (inputValue.length >= date.day.length) {
const digitsToPrependZero = ['4', '5', '6', '7', '8', '9'];
if (digitsToPrependZero.indexOf(inputValue) > -1) {
inputValue = `0${inputValue}`;
}
}
handleDateChange(event, inputValue, DateUtil.inputType.DAY);
};
const handleMonthChange = (event) => {
let inputValue = event.target.value;
if (!DateUtil.validDateInput(inputValue)) {
handleInvalidInputChange(inputValue);
return;
}
// Ignore the entry if the value did not change or it is invalid.
// When 'Predictive text' is enabled on Android the maxLength attribute on the input is ignored so we have to
// check the length of inputValue to make sure that it is less then 2.
if (inputValue === date.month || inputValue.length > 2 || Number(inputValue) > 12 || inputValue === '00') {
handleInvalidInputChange(inputValue);
return;
}
// If the change made was not a deletion of a digit, then prepend '0'
// when the input value is a single digit value between 2 and 9
if (inputValue.length >= date.month.length) {
const digitsToPrependZero = ['2', '3', '4', '5', '6', '7', '8', '9'];
if (digitsToPrependZero.indexOf(inputValue) > -1) {
inputValue = `0${inputValue}`;
}
}
handleDateChange(event, inputValue, DateUtil.inputType.MONTH);
};
const handleYearChange = (event) => {
const inputValue = event.target.value;
if (!DateUtil.validDateInput(inputValue)) {
handleInvalidInputChange(inputValue);
return;
}
// Ignore the entry if the value did not change or it is invalid.
// When 'Predictive text' is enabled on Android the maxLength attribute on the input is ignored so we have to
// check the length of inputValue to make sure that it is less then 4.
if (inputValue === date.year || inputValue.length > 4) {
handleInvalidInputChange(inputValue);
return;
}
// Ignore the 3rd entry if the first two digits are not 19, 20 or 21
if (inputValue.length === 3 && (Number(inputValue) < 190 || Number(inputValue) > 210)) {
handleInvalidInputChange(inputValue);
return;
}
// Ignore the 4th entry if the year value is not between MIN_YEAR and MAX_YEAR
if (inputValue.length === 4 && (Number(inputValue) < Number(DateUtil.MIN_YEAR) || Number(inputValue) > Number(DateUtil.MAX_YEAR))) {
handleInvalidInputChange(inputValue);
return;
}
handleDateChange(event, inputValue, DateUtil.inputType.YEAR);
};
const setInputFocus = (event, inputRef) => {
inputRef.focus();
event.preventDefault();
};
const handleDayInputKeydown = (event) => {
if (inputAttributes?.readOnly) {
return;
}
if (dateFormatOrder === DateUtil.dateOrder.MDY) {
if ((event.keyCode === KEY_LEFT || event.keyCode === KEY_DELETE || event.keyCode === KEY_BACK_SPACE) && date.day.length === 0) {
setInputFocus(event, monthInputRef);
} else if (event.keyCode === KEY_RIGHT && date.day.length === 0) {
setInputFocus(event, yearInputRef);
}
} else if (dateFormatOrder === DateUtil.dateOrder.DMY) {
if (event.keyCode === KEY_RIGHT && date.day.length === 0) {
setInputFocus(event, monthInputRef);
}
} else if (dateFormatOrder === DateUtil.dateOrder.YMD) {
if ((event.keyCode === KEY_LEFT || event.keyCode === KEY_DELETE || event.keyCode === KEY_BACK_SPACE) && date.day.length === 0) {
setInputFocus(event, monthInputRef);
}
}
};
const handleMonthInputKeydown = (event) => {
if (inputAttributes?.readOnly) {
return;
}
if (dateFormatOrder === DateUtil.dateOrder.MDY) {
if (event.keyCode === KEY_RIGHT && date.month.length === 0) {
setInputFocus(event, dayInputRef);
}
} else if (dateFormatOrder === DateUtil.dateOrder.DMY) {
if ((event.keyCode === KEY_LEFT || event.keyCode === KEY_DELETE || event.keyCode === KEY_BACK_SPACE) && date.month.length === 0) {
setInputFocus(event, dayInputRef);
} else if (event.keyCode === KEY_RIGHT && date.month.length === 0) {
setInputFocus(event, yearInputRef);
}
} else if (dateFormatOrder === DateUtil.dateOrder.YMD) {
if ((event.keyCode === KEY_LEFT || event.keyCode === KEY_DELETE || event.keyCode === KEY_BACK_SPACE) && date.month.length === 0) {
setInputFocus(event, yearInputRef);
} else if (event.keyCode === KEY_RIGHT && date.month.length === 0) {
setInputFocus(event, dayInputRef);
}
}
};
const handleYearInputKeydown = (event) => {
if (inputAttributes?.readOnly) {
return;
}
if (dateFormatOrder === DateUtil.dateOrder.MDY) {
if ((event.keyCode === KEY_LEFT || event.keyCode === KEY_DELETE || event.keyCode === KEY_BACK_SPACE) && date.year.length === 0) {
setInputFocus(event, dayInputRef);
}
} else if (dateFormatOrder === DateUtil.dateOrder.DMY) {
if ((event.keyCode === KEY_LEFT || event.keyCode === KEY_DELETE || event.keyCode === KEY_BACK_SPACE) && date.year.length === 0) {
setInputFocus(event, monthInputRef);
}
} else if (dateFormatOrder === DateUtil.dateOrder.YMD) {
if (event.keyCode === KEY_RIGHT && date.year.length === 0) {
setInputFocus(event, monthInputRef);
}
}
};
const handleInputKeydown = (event, inputType) => {
editOnkeyDown.current = true;
const { day, month, year } = date;
let inputDate;
let formattedDate;
let inputTypeValue;
if (inputType === DateUtil.inputType.DAY) {
inputTypeValue = DateUtil.inputTypeString.DAYVALUE;
} else if (inputType === DateUtil.inputType.MONTH) {
inputTypeValue = DateUtil.inputTypeString.MONTHVALUE;
} else {
inputTypeValue = DateUtil.inputTypeString.YEARVALUE;
}
if ((day.length === 2 && month.length === 2 && year.length === 4) && event.key.match(/^[0-9]/g)) {
event.currentTarget.value = ''; // eslint-disable-line no-param-reassign
}
if (day.length === 2 && month.length === 2 && year.length === 4) {
inputDate = DateUtil.convertToISO8601(`${year}-${month}-${day}`, DateUtil.ISO_EXTENDED_DATE_FORMAT);
formattedDate = DateUtil.strictFormatISODate(inputDate, momentDateFormat);
}
const validDate = DateUtil.isValidDate(formattedDate, momentDateFormat);
// set date to today
if (event.key === 't' || event.key === 'T') {
inputDate = DateUtil.getCurrentDate();
formattedDate = DateUtil.strictFormatISODate(inputDate, momentDateFormat);
if (onChange) {
onChange(event, formattedDate);
}
const nextDayValues = DateUtil.getDateInputValues(DateUtil.dateOrder.YMD, inputDate, '-');
date = { day: nextDayValues.day, month: nextDayValues.month, year: nextDayValues.year };
event.preventDefault();
return;
}
if (event.key === '-' || event.key === '_' || event.keyCode === KEY_DOWN) {
if (validDate) {
inputDate = DateUtil.decrementDate(inputDate, DateUtil.ISO_EXTENDED_DATE_FORMAT, inputTypeValue);
} else {
inputDate = DateUtil.decrementDate(DateUtil.getCurrentDate(), DateUtil.ISO_EXTENDED_DATE_FORMAT, inputTypeValue);
}
formattedDate = DateUtil.strictFormatISODate(inputDate, momentDateFormat);
if (onChange) {
onChange(event, formattedDate);
}
const nextDayValues = DateUtil.getDateInputValues(DateUtil.dateOrder.YMD, inputDate, '-');
date = { day: nextDayValues.day, month: nextDayValues.month, year: nextDayValues.year };
event.preventDefault();
return;
}
if (event.key === '=' || event.key === '+' || event.keyCode === KEY_UP) {
if (validDate) {
inputDate = DateUtil.incrementDate(inputDate, DateUtil.ISO_EXTENDED_DATE_FORMAT, inputTypeValue);
} else {
inputDate = DateUtil.incrementDate(DateUtil.getCurrentDate(), DateUtil.ISO_EXTENDED_DATE_FORMAT, inputTypeValue);
}
formattedDate = DateUtil.strictFormatISODate(inputDate, momentDateFormat);
if (onChange) {
onChange(event, formattedDate);
}
const nextDayValues = DateUtil.getDateInputValues(DateUtil.dateOrder.YMD, inputDate, '-');
date = { day: nextDayValues.day, month: nextDayValues.month, year: nextDayValues.year };
event.preventDefault();
return;
}
if (inputType === DateUtil.inputType.YEAR) {
handleYearInputKeydown(event);
} else if (inputType === DateUtil.inputType.MONTH) {
handleMonthInputKeydown(event);
} else if (inputType === DateUtil.inputType.DAY) {
handleDayInputKeydown(event);
}
if (DateUtil.isMac() && !event.key.match(/^[0-9]/g) && !(event.keyCode === KEY_BACK_SPACE || event.keyCode === KEY_DELETE || event.keyCode === KEY_TAB || event.keyCode === KEY_RIGHT || event.keyCode === KEY_LEFT)) {
event.preventDefault();
}
};
const handleOnInputFocus = (event, type) => {
if (onFocus) {
onFocus(event);
}
setFocused(true);
if (type === DateUtil.inputType.DAY) {
setDayInitialFocused(true);
} else if (type === DateUtil.inputType.MONTH) {
setMonthInitialFocused(true);
} else {
setYearInitialFocused(true);
}
};
const handleOnInputBlur = (event, type) => {
if (onBlur) {
onBlur(event);
}
setFocused(false);
if (type === DateUtil.inputType.DAY) {
setDayInitialFocused(false);
} else if (type === DateUtil.inputType.MONTH) {
setMonthInitialFocused(false);
} else {
setYearInitialFocused(false);
}
if (type === DateUtil.inputType.DAY || type === DateUtil.inputType.MONTH) {
let inputValue = event.target.value;
// Prepend a 0 to the value when losing focus and the value is single digit except 0.
// Append a 1 to the value when the single digit is 0
if (inputValue.length === 1) {
inputValue = inputValue === '0' ? '01' : '0'.concat(inputValue);
handleDateChange(event, inputValue, type);
}
} else if (type === DateUtil.inputType.YEAR) {
let inputValue = event.target.value;
if (inputValue.length === 1) {
// Prepend a 200 to the value when losing focus and the value is single digit.
inputValue = '200'.concat(inputValue);
handleDateChange(event, inputValue, type);
} else if (inputValue.length === 2) {
// Prepend a 20 to the value when losing focus and the value is two digits.
inputValue = '20'.concat(inputValue);
handleDateChange(event, inputValue, type);
} else if (inputValue.length === 3 && (Number(inputValue) >= 190 || Number(inputValue) <= 210)) {
// Append a 0 to the value when losing focus and the value is three digits between 190 to 210.
inputValue = inputValue.concat('0');
handleDateChange(event, inputValue, type);
}
}
};
const handleOnButtonClick = (event) => {
const readOnly = inputAttributes?.readOnly;
if (!readOnly && onCalendarButtonClick && onClick) {
onCalendarButtonClick(event, onClick);
}
};
const handleOnButtonKeyDown = (event) => {
if (onKeyDown) {
onKeyDown(event);
}
};
const formatDescriptionId = `terra-date-picker-description-format-${uuidv4()}`;
let ariaDescriptionIds;
if (useExternalFormatMask === false) {
if (inputAttributes && inputAttributes['aria-describedby']) {
ariaDescriptionIds = `${formatDescriptionId} ${inputAttributes['aria-describedby']}`;
} else {
ariaDescriptionIds = formatDescriptionId;
}
} else if (inputAttributes && inputAttributes['aria-describedby']) {
ariaDescriptionIds = inputAttributes['aria-describedby'];
}
const dayInputClasses = cx([
'date-input-day',
{ 'initial-focus': dayInitialFocused },
]);
const dateDayInput = (
<Input
{...additionalInputProps}
// Both 'ref' and 'refCallback' are required here because:
// 'refCallback' returns the DOM element of the HTML input element
// 'ref' when used on a class component returns the mounted instance of the component
refCallback={(node) => { dayInputRef = node; }}
ref={dateFormatOrder === DateUtil.dateOrder.DMY ? firstInputRefCallback : undefined}
className={dayInputClasses}
type="number"
name={`terra-date-day-${name}`}
value={date.day}
onChange={handleDayChange}
onFocus={(e) => handleOnInputFocus(e, DateUtil.inputType.DAY)}
onBlur={(e) => handleOnInputBlur(e, DateUtil.inputType.DAY)}
onKeyDown={(e) => handleInputKeydown(e, DateUtil.inputType.DAY)}
maxLength="2"
size="2"
pattern="\d*"
aria-invalid={isInvalid}
aria-required={required}
aria-label={`${ariaLabel ? `${ariaLabel} ${intl.formatMessage({ id: 'Terra.datePicker.dayLabel' })}` : intl.formatMessage({ id: 'Terra.datePicker.dayLabel' })}`}
aria-describedby={`${ariaDescriptionIds} ${errorId}`}
id={dayInputId}
/>
);
const monthInputClasses = cx([
'date-input-month',
{ 'initial-focus': monthInitialFocused },
]);
const dateMonthInput = (
<Input
{...additionalInputProps}
// Both 'ref' and 'refCallback' are required here because:
// 'refCallback' returns the DOM element of the HTML input element
// 'ref' when used on a class component returns the mounted instance of the component
refCallback={(node) => { monthInputRef = node; }}
ref={dateFormatOrder === DateUtil.dateOrder.MDY ? firstInputRefCallback : undefined}
className={monthInputClasses}
type="number"
name={`terra-date-month-${name}`}
value={date.month}
onChange={handleMonthChange}
onFocus={(e) => handleOnInputFocus(e, DateUtil.inputType.MONTH)}
onBlur={(e) => handleOnInputBlur(e, DateUtil.inputType.MONTH)}
onKeyDown={(e) => handleInputKeydown(e, DateUtil.inputType.MONTH)}
maxLength="2"
size="2"
pattern="\d*"
aria-invalid={isInvalid}
aria-required={required}
aria-label={`${ariaLabel ? `${ariaLabel} ${intl.formatMessage({ id: 'Terra.datePicker.monthLabel' })}` : intl.formatMessage({ id: 'Terra.datePicker.monthLabel' })}`}
aria-describedby={`${ariaDescriptionIds} ${errorId}`}
id={monthInputId}
/>
);
const yearInputClasses = cx([
'date-input-year',
{ 'initial-focus': yearInitialFocused },
]);
const dateYearInput = (
<Input
{...additionalInputProps}
// Both 'ref' and 'refCallback' are required here because:
// 'refCallback' returns the DOM element of the HTML input element
// 'ref' when used on a class component returns the mounted instance of the component
refCallback={(node) => { yearInputRef = node; }}
ref={dateFormatOrder === DateUtil.dateOrder.YMD ? firstInputRefCallback : undefined}
className={yearInputClasses}
type="number"
name={`terra-date-year-${name}`}
value={date.year}
onChange={handleYearChange}
onFocus={(e) => handleOnInputFocus(e, DateUtil.inputType.YEAR)}
onBlur={(e) => handleOnInputBlur(e, DateUtil.inputType.YEAR)}
onKeyDown={(e) => handleInputKeydown(e, DateUtil.inputType.YEAR)}
maxLength="4"
size="4"
pattern="\d*"
aria-invalid={isInvalid}
aria-required={required}
aria-label={`${ariaLabel ? `${ariaLabel} ${intl.formatMessage({ id: 'Terra.datePicker.yearLabel' })}` : intl.formatMessage({ id: 'Terra.datePicker.yearLabel' })}`}
aria-describedby={`${ariaDescriptionIds} ${errorId}`}
id={yearInputId}
/>
);
const dateSpacer = <span className={cx('date-spacer')}>{separator}</span>;
const dateInputClasses = cx([
'date-input',
{ 'is-focused': isFocused },
{ 'is-invalid': isInvalid },
{ 'is-incomplete': isIncomplete && required && !isInvalid },
]);
const label = ariaLabel || intl.formatMessage({ id: 'Terra.datePicker.date' });
const format = intl.formatMessage({ id: 'Terra.datePicker.dateFormat' });
const buttonClasses = cx([
'button',
{ 'is-invalid': isInvalid },
]);
// Indicates selected date from calendar popup for SR
let inputDate;
if (DateUtil.isValidDate(value, momentDateFormat)) {
inputDate = `${getLocalizedDateForScreenReader(DateUtil.createSafeDate(dateValue, initialTimeZone), { intl, locale: intl.locale })}`;
}
let calendarDate = inputDate ? `${inputDate} ${intl.formatMessage({ id: 'Terra.datePicker.selected' })}` : '';
// Check if date is excluded or out of range or not included or filtered
let invalidEntry = '';
if (DateUtil.isDateExcluded(DateUtil.createSafeDate(dateValue, initialTimeZone), props.excludeDates)
|| DateUtil.isDateOutOfRange(DateUtil.createSafeDate(dateValue, initialTimeZone), DateUtil.createSafeDate(DateUtil.getMinDate(props.minDate), initialTimeZone), DateUtil.createSafeDate(DateUtil.getMaxDate(props.maxDate), initialTimeZone))
|| DateUtil.isDateNotIncluded(DateUtil.createSafeDate(dateValue, initialTimeZone), props.includeDates)
|| (props.filterDate && !props.filterDate(DateUtil.createSafeDate(dateValue, initialTimeZone)))) {
invalidEntry = `${intl.formatMessage({ id: 'Terra.datePicker.invalidDate' })}.`;
calendarDate = '';
}
return (
<div className={cx(theme.className)}>
<div className={cx('date-input-container')}>
<div
className={dateInputClasses}
id={id || idFromInputAttributes}
disabled={additionalInputProps.disabled}
>
<input
// Create a hidden input for storing the name and value attributes to use when submitting the form.
// The data stored in the value attribute will be the visible date in the date input but in ISO 8601 format.
data-terra-date-input-hidden
type="hidden"
name={name}
value={dateValue}
/>
<VisuallyHiddenText text={value ? `${label}, ${getLocalizedDateForScreenReader(DateUtil.createSafeDate(dateValue, initialTimeZone), { intl, locale: intl.locale })}` : label} />
<VisuallyHiddenText
refCallback={setVisuallyHiddenComponent}
aria-atomic="true"
aria-relevant="all"
aria-live="assertive"
/>
<DateInputLayout
dateFormatOrder={dateFormatOrder}
separator={dateSpacer}
day={dateDayInput}
month={dateMonthInput}
year={dateYearInput}
/>
</div>
<Button
data-terra-open-calendar-button
className={buttonClasses}
text={intl.formatMessage({ id: 'Terra.datePicker.openCalendar' })}
onClick={handleOnButtonClick}
onKeyDown={handleOnButtonKeyDown}
icon={<IconCalendar />}
isIconOnly
isCompact
isDisabled={additionalInputProps.disabled}
onBlur={onBlur}
onFocus={onButtonFocus}
refCallback={buttonRefCallback}
aria-label={`${calendarDate} ${intl.formatMessage({ id: 'Terra.datePicker.openCalendar' })}`}
/>
</div>
{!useExternalFormatMask && (
<div id={formatDescriptionId} className={cx('format-text')}>
<VisuallyHiddenText
aria-live={DateUtil.isMac() ? 'polite' : 'off'}
text={`${invalidEntry}
${intl.formatMessage({ id: 'Terra.datePicker.dateFormatLabel' })}
${format}. ${inputDate ? `${inputDate},` : ''} ${intl.formatMessage({ id: 'Terra.datePicker.hotKey' })}, `}
/>
<div aria-hidden="true">
{`(${format})`}
</div>
</div>
)}
</div>
);
};
DatePickerInput.propTypes = propTypes;
DatePickerInput.defaultProps = defaultProps;
export default injectIntl(DatePickerInput);