UNPKG

@mskcc/carbon-react

Version:

Carbon react components for the MSKCC DSM

714 lines (700 loc) 22.6 kB
/** * MSKCC 2021, 2024 */ import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js'; import PropTypes from 'prop-types'; import React__default, { useContext, useState, useCallback, useRef, useEffect, useImperativeHandle } from 'react'; import cx from 'classnames'; import flatpickr from 'flatpickr'; import l10n from 'flatpickr/dist/l10n/index'; import DatePickerInput from '../DatePickerInput/DatePickerInput.js'; import carbonFlatpickrAppendToPlugin from './plugins/appendToPlugin.js'; import carbonFlatpickrFixEventsPlugin from './plugins/fixEventsPlugin.js'; import carbonFlatpickrRangePlugin from './plugins/rangePlugin.js'; import deprecate from '../../prop-types/deprecate.js'; import { usePrefix } from '../../internal/usePrefix.js'; import { useSavedCallback } from '../../internal/useSavedCallback.js'; import '../FluidForm/FluidForm.js'; import { FormContext } from '../FluidForm/FormContext.js'; import { match } from '../../internal/keyboard/match.js'; import { Escape, ArrowDown } from '../../internal/keyboard/keys.js'; // Weekdays shorthand for english locale l10n.en.weekdays.shorthand.forEach((_day, index) => { const currentDay = l10n.en.weekdays.shorthand; if (currentDay[index] === 'Thu' || currentDay[index] === 'Th') { currentDay[index] = 'Th'; } else { currentDay[index] = currentDay[index].charAt(0); } }); const forEach = Array.prototype.forEach; /** * @param {number} monthNumber The month number. * @param {boolean} shorthand `true` to use shorthand month. * @param {Locale} locale The Flatpickr locale data. * @returns {string} The month string. */ const monthToStr = (monthNumber, shorthand, locale) => locale.months[shorthand ? 'shorthand' : 'longhand'][monthNumber]; /** * @param {object} config Plugin configuration. * @param {boolean} [config.shorthand] `true` to use shorthand month. * @param {string} config.selectorFlatpickrMonthYearContainer The CSS selector for the container of month/year selection UI. * @param {string} config.selectorFlatpickrYearContainer The CSS selector for the container of year selection UI. * @param {string} config.selectorFlatpickrCurrentMonth The CSS selector for the text-based month selection UI. * @param {string} config.classFlatpickrCurrentMonth The CSS class for the text-based month selection UI. * @returns {Plugin} A Flatpickr plugin to use text instead of `<select>` for month picker. */ const carbonFlatpickrMonthSelectPlugin = config => fp => { const setupElements = () => { if (!fp.monthElements) { return; } fp.monthElements.forEach(elem => { if (!elem.parentNode) { return; } elem.parentNode.removeChild(elem); }); fp.monthElements.splice(0, fp.monthElements.length, ...fp.monthElements.map(() => { // eslint-disable-next-line no-underscore-dangle const monthElement = fp._createElement('span', config.classFlatpickrCurrentMonth); monthElement.textContent = monthToStr(fp.currentMonth, config.shorthand === true, fp.l10n); fp.yearElements[0].closest(config.selectorFlatpickrMonthYearContainer).insertBefore(monthElement, fp.yearElements[0].closest(config.selectorFlatpickrYearContainer)); return monthElement; })); }; const updateCurrentMonth = () => { if (fp.monthElements) { const monthStr = monthToStr(fp.currentMonth, config.shorthand === true, fp.l10n); fp.yearElements.forEach(elem => { const currentMonthContainer = elem.closest(config.selectorFlatpickrMonthYearContainer); Array.prototype.forEach.call(currentMonthContainer.querySelectorAll('.cur-month'), monthElement => { monthElement.textContent = monthStr; }); }); } }; const register = () => { fp.loadedPlugins.push('carbonFlatpickrMonthSelectPlugin'); }; return { onMonthChange: updateCurrentMonth, onValueUpdate: updateCurrentMonth, onOpen: updateCurrentMonth, onReady: [setupElements, updateCurrentMonth, register] }; }; /** * Determine if every child in a list of children has no label specified * @param {Array<ReactElement>} children * @returns {boolean} */ function isLabelTextEmpty(children) { return children.every(child => !child.props.labelText); } const rightArrowHTML = `<svg width="16px" height="16px" viewBox="0 0 16 16"> <polygon points="11,8 6,13 5.3,12.3 9.6,8 5.3,3.7 6,3 "/> </svg>`; const leftArrowHTML = `<svg width="16px" height="16px" viewBox="0 0 16 16"> <polygon points="5,8 10,3 10.7,3.7 6.4,8 10.7,12.3 10,13 "/> </svg>`; function updateClassNames(calendar, prefix) { const calendarContainer = calendar.calendarContainer; const daysContainer = calendar.days; if (calendarContainer && daysContainer) { // calendarContainer and daysContainer are undefined if flatpickr detects a mobile device calendarContainer.classList.add(`${prefix}--date-picker__calendar`); calendarContainer.querySelector('.flatpickr-month').classList.add(`${prefix}--date-picker__month`); calendarContainer.querySelector('.flatpickr-weekdays').classList.add(`${prefix}--date-picker__weekdays`); calendarContainer.querySelector('.flatpickr-days').classList.add(`${prefix}--date-picker__days`); forEach.call(calendarContainer.querySelectorAll('.flatpickr-weekday'), item => { const currentItem = item; currentItem.innerHTML = currentItem.innerHTML.replace(/\s+/g, ''); currentItem.classList.add(`${prefix}--date-picker__weekday`); }); forEach.call(daysContainer.querySelectorAll('.flatpickr-day'), item => { item.classList.add(`${prefix}--date-picker__day`); if (item.classList.contains('today') && calendar.selectedDates.length > 0) { item.classList.add('no-border'); } else if (item.classList.contains('today') && calendar.selectedDates.length === 0) { item.classList.remove('no-border'); } }); } } const DatePicker = /*#__PURE__*/React__default.forwardRef(function DatePicker(_ref, ref) { let { allowInput, appendTo, children, className, closeOnSelect = true, dateFormat = 'm/d/Y', datePickerType, disable, enable, inline, invalid, invalidText, warn, warnText, light = false, locale = 'en', maxDate, minDate, onChange, onClose, onOpen, readOnly = false, short = false, value, ...rest } = _ref; const prefix = usePrefix(); const { isFluid } = useContext(FormContext); const [hasInput, setHasInput] = useState(false); const startInputField = useCallback(node => { if (node !== null) { startInputField.current = node; setHasInput(true); } }, []); const lastStartValue = useRef(''); // fix datepicker deleting the selectedDate when the calendar closes const onCalendarClose = (selectedDates, dateStr) => { setTimeout(() => { if (lastStartValue.current && selectedDates[0] && !startInputField.current.value) { startInputField.current.value = lastStartValue.current; calendarRef.current.setDate([startInputField.current.value, endInputField?.current?.value], true, calendarRef.current.config.dateFormat); } if (onClose) { onClose(calendarRef.current.selectedDates, dateStr, calendarRef.current); } }); }; const endInputField = useRef(null); const calendarRef = useRef(null); const savedOnChange = useSavedCallback(onChange); const savedOnClose = useSavedCallback(datePickerType === 'range' ? onCalendarClose : onClose); const savedOnOpen = useSavedCallback(onOpen); const datePickerClasses = cx(`${prefix}--date-picker`, { [`${prefix}--date-picker--short`]: short, [`${prefix}--date-picker--light`]: light, [`${prefix}--date-picker--simple`]: datePickerType === 'simple', [`${prefix}--date-picker--single`]: datePickerType === 'single', [`${prefix}--date-picker--range`]: datePickerType === 'range', [`${prefix}--date-picker--nolabel`]: datePickerType === 'range' && isLabelTextEmpty(children) }); const wrapperClasses = cx(`${prefix}--form-item`, { [String(className)]: className }); const childrenWithProps = React__default.Children.toArray(children).map((child, index) => { if (index === 0 && child.type === React__default.createElement(DatePickerInput, child.props).type) { return /*#__PURE__*/React__default.cloneElement(child, { datePickerType, ref: startInputField, readOnly }); } if (index === 1 && child.type === React__default.createElement(DatePickerInput, child.props).type) { return /*#__PURE__*/React__default.cloneElement(child, { datePickerType, ref: endInputField, readOnly }); } if (index === 0) { return /*#__PURE__*/React__default.cloneElement(child, { ref: startInputField, readOnly }); } if (index === 1) { return /*#__PURE__*/React__default.cloneElement(child, { ref: endInputField, readOnly }); } }); useEffect(() => { if (datePickerType !== 'single' && datePickerType !== 'range') { return; } if (!startInputField.current) { return; } const onHook = (_electedDates, _dateStr, instance) => { updateClassNames(instance, prefix); if (startInputField?.current) { startInputField.current.readOnly = readOnly; } if (endInputField?.current) { endInputField.current.readOnly = readOnly; } }; // Logic to determine if `enable` or `disable` will be passed down. If neither // is provided, we return the default empty disabled array, allowing all dates. const enableOrDisable = enable ? 'enable' : 'disable'; let enableOrDisableArr; if (!enable && !disable) { enableOrDisableArr = []; } else if (enable) { enableOrDisableArr = enable; } else { enableOrDisableArr = disable; } let localeData; if (typeof locale === 'object') { const location = locale.locale ? locale.locale : 'en'; localeData = { ...l10n[location], ...locale }; } else { localeData = l10n[locale]; } const { current: start } = startInputField; const { current: end } = endInputField; const flatpickerconfig = { inline: inline ?? false, disableMobile: true, defaultDate: value, closeOnSelect: closeOnSelect, mode: datePickerType, allowInput: allowInput ?? true, dateFormat: dateFormat, locale: localeData, [enableOrDisable]: enableOrDisableArr, minDate: minDate, maxDate: maxDate, plugins: [datePickerType === 'range' ? carbonFlatpickrRangePlugin({ input: endInputField.current }) : () => {}, appendTo ? carbonFlatpickrAppendToPlugin({ appendTo }) : () => {}, carbonFlatpickrMonthSelectPlugin({ selectorFlatpickrMonthYearContainer: '.flatpickr-current-month', selectorFlatpickrYearContainer: '.numInputWrapper', selectorFlatpickrCurrentMonth: '.cur-month', classFlatpickrCurrentMonth: 'cur-month' }), carbonFlatpickrFixEventsPlugin({ inputFrom: startInputField.current, inputTo: endInputField.current, lastStartValue })], clickOpens: !readOnly, noCalendar: readOnly, nextArrow: rightArrowHTML, prevArrow: leftArrowHTML, onChange: function () { if (savedOnChange && !readOnly) { savedOnChange(...arguments); } }, onClose: savedOnClose, onReady: onHook, onMonthChange: onHook, onYearChange: onHook, onOpen: function () { onHook(...arguments); savedOnOpen(...arguments); }, onValueUpdate: onHook }; const calendar = flatpickr(start, flatpickerconfig); calendarRef.current = calendar; function handleArrowDown(event) { if (match(event, Escape)) { calendar.calendarContainer.classList.remove('open'); } if (match(event, ArrowDown)) { const { calendarContainer, selectedDateElem: fpSelectedDateElem, todayDateElem: fptodayDateElem } = calendar; const selectedDateElem = calendarContainer.querySelector('.selected') && fpSelectedDateElem; const todayDateElem = calendarContainer.querySelector('.today') && fptodayDateElem; (selectedDateElem || todayDateElem || calendarContainer.querySelector('.flatpickr-day[tabindex]') || calendarContainer).focus(); } } function handleOnChange(event) { if (datePickerType == 'single') { calendar.calendarContainer.classList.remove('open'); } const { target } = event; if (target === start) { lastStartValue.current = start.value; } if (start.value !== '') { return; } if (!calendar.selectedDates) { return; } if (calendar.selectedDates.length === 0) { return; } calendar.clear(); calendar.input.focus(); } if (start) { start.addEventListener('keydown', handleArrowDown); start.addEventListener('change', handleOnChange); if (calendar && calendar.calendarContainer) { // Flatpickr's calendar dialog is not rendered in a landmark causing an // error with IBM Equal Access Accessibility Checker so we add an aria // role to the container div. calendar.calendarContainer.setAttribute('role', 'application'); // IBM EAAC requires an aria-label on a role='region' calendar.calendarContainer.setAttribute('aria-label', 'calendar-container'); } } if (end) { end.addEventListener('keydown', handleArrowDown); end.addEventListener('change', handleOnChange); } //component did unmount equivalent return () => { // Note: if the `startInputField` ref is undefined then calendar will be // of type: Array and `destroy` will not be defined if (calendar && calendar.destroy) { calendar.destroy(); } // prevent a duplicate date selection when a default value is set if (value) { if (startInputField?.current) { startInputField.current.value = ''; } if (endInputField?.current) { // eslint-disable-next-line react-hooks/exhaustive-deps endInputField.current.value = ''; } } if (start) { start.removeEventListener('keydown', handleArrowDown); start.removeEventListener('change', handleOnChange); } if (end) { end.removeEventListener('keydown', handleArrowDown); end.removeEventListener('change', handleOnChange); } }; }, [savedOnChange, savedOnClose, savedOnOpen, readOnly, hasInput]); //eslint-disable-line react-hooks/exhaustive-deps // this hook allows consumers to access the flatpickr calendar // instance for cases where functions like open() or close() // need to be imperatively called on the calendar useImperativeHandle(ref, () => ({ get calendar() { return calendarRef.current; } })); useEffect(() => { if (calendarRef?.current?.set) { calendarRef.current.set({ dateFormat }); } }, [dateFormat]); useEffect(() => { if (calendarRef?.current?.set) { calendarRef.current.set('minDate', minDate); } }, [minDate]); useEffect(() => { if (calendarRef?.current?.set) { calendarRef.current.set('maxDate', maxDate); } }, [maxDate]); useEffect(() => { if (calendarRef?.current?.set && disable) { calendarRef.current.set('disable', disable); } }, [disable]); useEffect(() => { if (calendarRef?.current?.set && enable) { calendarRef.current.set('enable', enable); } }, [enable]); useEffect(() => { if (calendarRef?.current?.set && inline) { calendarRef.current.set('inline', inline); } }, [inline]); useEffect(() => { if (calendarRef?.current?.set) { if (value !== undefined) { calendarRef.current.setDate(value); } updateClassNames(calendarRef.current, prefix); //for simple date picker w/o calendar; initial mount may not have value } else if (!calendarRef.current && value) { startInputField.current.value = value; } }, [value, prefix]); //eslint-disable-line react-hooks/exhaustive-deps let fluidError; if (isFluid) { if (invalid) { fluidError = /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", { className: `${prefix}--date-picker__icon ${prefix}--date-picker__icon--invalid msk-icon` }, "error"), /*#__PURE__*/React__default.createElement("hr", { className: `${prefix}--date-picker__divider` }), /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--form-requirement` }, invalidText)); } if (warn && !invalid) { fluidError = /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", { className: `${prefix}--date-picker__icon ${prefix}--date-picker__icon--warn msk-icon` }, "warning"), /*#__PURE__*/React__default.createElement("hr", { className: `${prefix}--date-picker__divider` }), /*#__PURE__*/React__default.createElement("div", { className: `${prefix}--form-requirement` }, warnText)); } } return /*#__PURE__*/React__default.createElement("div", _extends({ className: wrapperClasses, ref: ref }, rest), /*#__PURE__*/React__default.createElement("div", { className: datePickerClasses }, childrenWithProps), fluidError); }); DatePicker.propTypes = { /** * flatpickr prop passthrough. Allows the user to enter a date directly * into the input field */ allowInput: PropTypes.bool, /** * The DOM element the Flatpicker should be inserted into. `<body>` by default. */ appendTo: PropTypes.object, /** * The child nodes. */ children: PropTypes.node, /** * The CSS class names. */ className: PropTypes.string, /** * flatpickr prop passthrough. Controls whether the calendar dropdown closes upon selection. */ closeOnSelect: PropTypes.bool, /** * The date format. */ dateFormat: PropTypes.string, /** * The type of the date picker: * * * `simple` - Without calendar dropdown. * * `single` - With calendar dropdown and single date. * * `range` - With calendar dropdown and a date range. */ datePickerType: PropTypes.oneOf(['simple', 'single', 'range']), /** * The flatpickr `disable` option that allows a user to disable certain dates. */ disable: PropTypes.array, /** * The flatpickr `enable` option that allows a user to enable certain dates. */ enable: PropTypes.array, /** * The flatpickr `inline` option. */ inline: PropTypes.bool, /** * Specify whether or not the control is invalid (Fluid only) */ invalid: PropTypes.bool, /** * Provide the text that is displayed when the control is in error state (Fluid Only) */ invalidText: PropTypes.node, /** * `true` to use the light version. */ light: deprecate(PropTypes.bool, 'The `light` prop for `DatePicker` has ' + 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.'), /** * The language locale used to format the days of the week, months, and numbers. The full list of supported locales can be found here https://github.com/flatpickr/flatpickr/tree/master/src/l10n */ locale: PropTypes.oneOfType([PropTypes.object, PropTypes.oneOf(['ar', // Arabic 'at', // Austria 'az', // Azerbaijan 'be', // Belarusian 'bg', // Bulgarian 'bn', // Bangla 'bs', // Bosnia 'cat', // Catalan 'cs', // Czech 'cy', // Welsh 'da', // Danish 'de', // German 'en', // English 'eo', // Esperanto 'es', // Spanish 'et', // Estonian 'fa', // Persian 'fi', // Finnish 'fo', // Faroese 'fr', // French 'ga', // Gaelic 'gr', // Greek 'he', // Hebrew 'hi', // Hindi 'hr', // Croatian 'hu', // Hungarian 'id', // Indonesian 'is', // Icelandic 'it', // Italian 'ja', // Japanese 'ka', // Georgian 'km', // Khmer 'ko', // Korean 'kz', // Kazakh 'lt', // Lithuanian 'lv', // Latvian 'mk', // Macedonian 'mn', // Mongolian 'ms', // Malaysian 'my', // Burmese 'nl', // Dutch 'no', // Norwegian 'pa', // Punjabi 'pl', // Polish 'pt', // Portuguese 'ro', // Romanian 'ru', // Russian 'si', // Sinhala 'sk', // Slovak 'sl', // Slovenian 'sq', // Albanian 'sr', // Serbian 'sv', // Swedish 'th', // Thai 'tr', // Turkish 'uk', // Ukrainian 'uz', // Uzbek 'uz_latn', // Uzbek Latin 'vn', // Vietnamese 'zh_tw', // Mandarin Traditional 'zh' // Mandarin ])]), /** * The maximum date that a user can pick to. */ maxDate: PropTypes.string, /** * The minimum date that a user can start picking from. */ minDate: PropTypes.string, /** * The `change` event handler. * `(dates: Date[], dStr: string, fp: Instance, data?: any):void;` */ onChange: PropTypes.func, /** * The `close` event handler. * `(dates: Date[], dStr: string, fp: Instance, data?: any):void;` */ onClose: PropTypes.func, /** * The `open` event handler. * `(dates: Date[], dStr: string, fp: Instance, data?: any):void;` */ onOpen: PropTypes.func, /** * whether the DatePicker is to be readOnly * if boolean applies to all inputs * if array applies to each input in order */ readOnly: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]), /** * `true` to use the short version. */ short: PropTypes.bool, /** * The value of the date value provided to flatpickr, could * be a date, a date number, a date string, an array of dates. */ value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object])), PropTypes.object, PropTypes.number]), /** * Specify whether the control is currently in warning state (Fluid only) */ warn: PropTypes.bool, /** * Provide the text that is displayed when the control is in warning state (Fluid only) */ warnText: PropTypes.node }; export { DatePicker as default };