UNPKG

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
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, /** * ![IMPORTANT](https://badgen.net/badge/UX/Accessibility/blue). * 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);