@mskcc/carbon-react
Version:
Carbon react components for the MSKCC DSM
714 lines (700 loc) • 22.6 kB
JavaScript
/**
* 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 };