UNPKG

@carbon/react

Version:

React components for the Carbon Design System

494 lines (492 loc) 21.7 kB
/** * Copyright IBM Corp. 2016, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ const require_runtime = require("../../_virtual/_rolldown/runtime.js"); const require_usePrefix = require("../../internal/usePrefix.js"); const require_keys = require("../../internal/keyboard/keys.js"); const require_match = require("../../internal/keyboard/match.js"); const require_deprecate = require("../../prop-types/deprecate.js"); const require_utils = require("../../internal/utils.js"); const require_index = require("../DatePickerInput/index.js"); const require_appendToPlugin = require("./plugins/appendToPlugin.js"); const require_fixEventsPlugin = require("./plugins/fixEventsPlugin.js"); const require_utils$1 = require("./utils.js"); const require_rangePlugin = require("./plugins/rangePlugin.js"); const require_useSavedCallback = require("../../internal/useSavedCallback.js"); const require_DatePickerLocales = require("./DatePickerLocales.js"); let classnames = require("classnames"); classnames = require_runtime.__toESM(classnames); let react = require("react"); react = require_runtime.__toESM(react); let prop_types = require("prop-types"); prop_types = require_runtime.__toESM(prop_types); let react_jsx_runtime = require("react/jsx-runtime"); let flatpickr = require("flatpickr"); flatpickr = require_runtime.__toESM(flatpickr); let flatpickr_dist_l10n_index = require("flatpickr/dist/l10n/index"); flatpickr_dist_l10n_index = require_runtime.__toESM(flatpickr_dist_l10n_index); let _carbon_utilities = require("@carbon/utilities"); //#region src/components/DatePicker/DatePicker.tsx /** * Copyright IBM Corp. 2016, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ function initializeWeekdayShorthand() { if (flatpickr_dist_l10n_index.default?.en?.weekdays?.shorthand) flatpickr_dist_l10n_index.default.en.weekdays.shorthand.forEach((_day, index) => { const currentDay = flatpickr_dist_l10n_index.default.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. * @param {string} config.locale The locale code. * @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(() => { const monthElement = fp._createElement("span", config.classFlatpickrCurrentMonth); monthElement.textContent = monthToStr(fp.currentMonth, config.shorthand === true, fp.l10n); if (_carbon_utilities.datePartsOrder.isMonthFirst(config.locale)) fp.yearElements[0].closest(config.selectorFlatpickrMonthYearContainer).insertBefore(monthElement, fp.yearElements[0].closest(config.selectorFlatpickrYearContainer)); else fp.yearElements[0].closest(config.selectorFlatpickrMonthYearContainer).insertAdjacentElement("beforeend", monthElement); 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); } function updateClassNames(calendar, prefix) { const calendarContainer = calendar.calendarContainer; const daysContainer = calendar.days; if (calendarContainer && daysContainer) { 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`); item.setAttribute("role", "button"); 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 = (0, react.forwardRef)((props, ref) => { const { allowInput, appendTo, children, className, closeOnSelect = true, dateFormat = "m/d/Y", datePickerType, disable, enable, inline, invalid, warn, light = false, locale = "en", maxDate, minDate, onChange, onClose, onOpen, readOnly = false, short = false, value, parseDate: parseDateProp, nextMonthAriaLabel = "Next month", prevMonthAriaLabel = "Previous month", ...rest } = props; const prefix = require_usePrefix.usePrefix(); const [hasInput, setHasInput] = (0, react.useState)(false); const startInputField = (0, react.useCallback)((node) => { if (node !== null) { startInputField.current = node; setHasInput(true); } }, []); const lastStartValue = (0, react.useRef)(""); const calendarRef = (0, react.useRef)(null); const [calendarCloseEvent, setCalendarCloseEvent] = (0, react.useState)(null); const handleCalendarClose = (0, react.useCallback)((selectedDates, dateStr, instance) => { 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(selectedDates, dateStr, instance); }, [onClose]); const onCalendarClose = (selectedDates, dateStr, instance, e) => { if (e && e.type === "clickOutside") return; setCalendarCloseEvent({ selectedDates, dateStr, instance }); }; (0, react.useEffect)(() => { if (calendarCloseEvent) { const { selectedDates, dateStr, instance } = calendarCloseEvent; handleCalendarClose(selectedDates, dateStr, instance); setCalendarCloseEvent(null); } }, [calendarCloseEvent, handleCalendarClose]); const endInputField = (0, react.useRef)(null); const lastFocusedField = (0, react.useRef)(null); const savedOnChange = require_useSavedCallback.useSavedCallback(onChange); const savedOnOpen = require_useSavedCallback.useSavedCallback(onOpen); const effectiveWarn = warn && !invalid; const wrapperRef = (0, react.useRef)(null); const datePickerClasses = (0, classnames.default)(`${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 = (0, classnames.default)(`${prefix}--form-item`, { [String(className)]: className }); const childrenWithProps = react.default.Children.toArray(children).map((child, index) => { if (index === 0 && require_utils.isComponentElement(child, require_index.default)) return react.default.cloneElement(child, { datePickerType, ref: startInputField, readOnly, invalid, warn: effectiveWarn }); if (index === 1 && require_utils.isComponentElement(child, require_index.default)) return react.default.cloneElement(child, { datePickerType, ref: endInputField, readOnly, invalid, warn: effectiveWarn }); if (index === 0) return react.default.cloneElement(child, { ref: startInputField, readOnly, invalid, warn: effectiveWarn }); if (index === 1) return react.default.cloneElement(child, { ref: endInputField, readOnly, invalid, warn: effectiveWarn }); }); (0, react.useEffect)(() => { initializeWeekdayShorthand(); }, []); (0, react.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; }; 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") localeData = { ...flatpickr_dist_l10n_index.default[locale.locale ? locale.locale : "en"], ...locale }; else localeData = flatpickr_dist_l10n_index.default[locale]; /** * parseDate is called before the date is actually set. * It attempts to parse the input value and return a valid date string. * Flatpickr's default parser results in odd dates when given invalid * values, so instead here we normalize the month/day to `1` if given * a value outside the acceptable range. */ let parseDate; if (!parseDateProp && dateFormat === "m/d/Y") parseDate = (date) => { const month = date.split("/")[0] <= 12 && date.split("/")[0] > 0 ? parseInt(date.split("/")[0]) : 1; const year = parseInt(date.split("/")[2]); if (month && year) { const daysInMonth = new Date(year, month, 0).getDate(); const day = date.split("/")[1] <= daysInMonth && date.split("/")[1] > 0 ? parseInt(date.split("/")[1]) : 1; return /* @__PURE__ */ new Date(`${year}/${month}/${day}`); } else return false; }; else if (parseDateProp) parseDate = parseDateProp; const rightArrowHTML = `<svg aria-label="${nextMonthAriaLabel}" role="img" 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 aria-label="${prevMonthAriaLabel}" role="img" 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>`; const { current: start } = startInputField; const { current: end } = endInputField; const calendar = (0, flatpickr.default)(start, { inline: inline ?? false, onClose: onCalendarClose, disableMobile: true, defaultDate: value, closeOnSelect, mode: datePickerType, allowInput: allowInput ?? true, dateFormat, locale: localeData, [enableOrDisable]: enableOrDisableArr, minDate, maxDate, parseDate, plugins: [ datePickerType === "range" ? require_rangePlugin.rangePlugin({ input: endInputField.current ?? void 0 }) : (() => {}), appendTo ? require_appendToPlugin.appendToPlugin({ appendTo }) : (() => {}), carbonFlatpickrMonthSelectPlugin({ selectorFlatpickrMonthYearContainer: ".flatpickr-current-month", selectorFlatpickrYearContainer: ".numInputWrapper", selectorFlatpickrCurrentMonth: ".cur-month", classFlatpickrCurrentMonth: "cur-month", locale }), require_fixEventsPlugin.default({ inputFrom: startInputField.current, inputTo: endInputField.current, lastStartValue, container: wrapperRef.current }) ], clickOpens: !readOnly, noCalendar: readOnly, nextArrow: rightArrowHTML, prevArrow: leftArrowHTML, onChange: (...args) => { if (!readOnly) savedOnChange(...args); }, onReady: onHook, onMonthChange: onHook, onYearChange: onHook, onOpen: (...args) => { onHook(...args); savedOnOpen(...args); }, onValueUpdate: onHook }); calendarRef.current = calendar; const handleInputFieldKeyDown = (event) => { if (readOnly && require_match.match(event, require_keys.Tab)) return; const { calendarContainer, selectedDateElem: fpSelectedDateElem, todayDateElem: fpTodayDateElem } = calendar; if (require_match.match(event, require_keys.Escape)) calendarContainer.classList.remove("open"); if (require_match.match(event, require_keys.Tab)) { if (!event.shiftKey) { event.preventDefault(); calendarContainer.classList.add("open"); const selectedDateElem = calendarContainer.querySelector(".selected") && fpSelectedDateElem; const todayDateElem = calendarContainer.querySelector(".today") && fpTodayDateElem; const focusTarget = selectedDateElem || todayDateElem || calendarContainer.querySelector(".flatpickr-day[tabindex]") || calendarContainer; if (focusTarget instanceof HTMLElement) focusTarget.focus(); if (event.target === startInputField.current) lastFocusedField.current = startInputField.current; else if (event.target === endInputField.current) lastFocusedField.current = endInputField.current; } else if (calendarRef.current?.isOpen && event.target === startInputField.current) { calendarRef.current.close(); onCalendarClose(calendarRef.current.selectedDates, "", calendarRef.current, event); } } }; const handleCalendarKeyDown = (event) => { if (!calendarRef.current || !startInputField.current) return; const lastInputField = datePickerType == "range" ? endInputField.current : startInputField.current; if (require_match.match(event, require_keys.Tab)) if (!event.shiftKey) if (lastFocusedField.current === lastInputField) { lastInputField.focus(); calendarRef.current.close(); onCalendarClose(calendarRef.current.selectedDates, "", calendarRef.current, event); } else { event.preventDefault(); lastInputField.focus(); } else { event.preventDefault(); (lastFocusedField.current || startInputField.current).focus(); } }; function handleOnChange(event) { const { target } = event; if (target === start) lastStartValue.current = start.value; if (start.value !== "") return; if (!calendar.selectedDates) return; if (calendar.selectedDates.length === 0) return; } function handleKeyPress(event) { if (require_match.match(event, require_keys.Enter) && closeOnSelect && datePickerType == "single") calendar.calendarContainer.classList.remove("open"); } if (start) { start.addEventListener("keydown", handleInputFieldKeyDown); start.addEventListener("change", handleOnChange); start.addEventListener("keypress", handleKeyPress); if (calendar && calendar.calendarContainer) { calendar.calendarContainer.setAttribute("role", "application"); calendar.calendarContainer.setAttribute("aria-label", "calendar-container"); } } if (end) { end.addEventListener("keydown", handleInputFieldKeyDown); end.addEventListener("change", handleOnChange); end.addEventListener("keypress", handleKeyPress); } if (calendar.calendarContainer) calendar.calendarContainer.addEventListener("keydown", handleCalendarKeyDown); return () => { if (calendar && calendar.destroy) calendar.destroy(); if (value) { if (start) start.value = ""; if (end) end.value = ""; } if (start) { start.removeEventListener("keydown", handleInputFieldKeyDown); start.removeEventListener("change", handleOnChange); start.removeEventListener("keypress", handleKeyPress); } if (end) { end.removeEventListener("keydown", handleInputFieldKeyDown); end.removeEventListener("change", handleOnChange); end.removeEventListener("keypress", handleKeyPress); } if (calendar.calendarContainer) calendar.calendarContainer.removeEventListener("keydown", handleCalendarKeyDown); }; }, [ savedOnChange, savedOnOpen, readOnly, closeOnSelect, hasInput, datePickerType, nextMonthAriaLabel, prevMonthAriaLabel ]); (0, react.useImperativeHandle)(ref, () => ({ get calendar() { return calendarRef.current; } })); (0, react.useEffect)(() => { if (calendarRef.current?.set) calendarRef.current.set({ dateFormat }); }, [dateFormat]); (0, react.useEffect)(() => { if (calendarRef.current?.set) calendarRef.current.set("minDate", minDate); }, [minDate]); (0, react.useEffect)(() => { if (calendarRef.current?.set) calendarRef.current.set("allowInput", allowInput); }, [allowInput]); (0, react.useEffect)(() => { if (calendarRef.current?.set) calendarRef.current.set("maxDate", maxDate); }, [maxDate]); (0, react.useEffect)(() => { if (calendarRef.current?.set && disable) calendarRef.current.set("disable", disable); }, [disable]); (0, react.useEffect)(() => { if (calendarRef.current?.set && enable) calendarRef.current.set("enable", enable); }, [enable]); (0, react.useEffect)(() => { if (calendarRef.current?.set && inline) calendarRef.current.set("inline", inline); }, [inline]); (0, react.useEffect)(() => { if ((require_utils$1.isEmptyDateValue(value) || Array.isArray(value) && value.every(require_utils$1.isEmptyDateValue)) && calendarRef.current?.selectedDates.length) { calendarRef.current?.clear(); if (startInputField.current) startInputField.current.value = ""; if (endInputField.current) endInputField.current.value = ""; } }, [value, startInputField]); (0, react.useEffect)(() => { if (calendarRef.current?.set) { if (value !== void 0) if (value === "" || value === null || Array.isArray(value) && (value.length === 0 || value.every(require_utils$1.isEmptyDateValue))) { if (calendarRef.current.selectedDates.length > 0) calendarRef.current.clear(); } else calendarRef.current.setDate(value); updateClassNames(calendarRef.current, prefix); } else if (!calendarRef.current && typeof value !== "undefined" && value !== null) startInputField.current.value = value; }, [ value, prefix, startInputField ]); let fluidError; return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { className: wrapperClasses, ref, ...rest, children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: datePickerClasses, ref: wrapperRef, children: childrenWithProps }), fluidError] }); }); DatePicker.propTypes = { allowInput: prop_types.default.bool, appendTo: prop_types.default.object, children: prop_types.default.node, className: prop_types.default.string, closeOnSelect: prop_types.default.bool, dateFormat: prop_types.default.string, datePickerType: prop_types.default.oneOf([ "simple", "single", "range" ]), disable: prop_types.default.array, enable: prop_types.default.array, inline: prop_types.default.bool, invalid: prop_types.default.bool, light: require_deprecate.deprecate(prop_types.default.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."), locale: prop_types.default.oneOfType([prop_types.default.object, prop_types.default.oneOf(require_DatePickerLocales.SUPPORTED_LOCALES)]), maxDate: prop_types.default.oneOfType([prop_types.default.string, prop_types.default.number]), minDate: prop_types.default.oneOfType([prop_types.default.string, prop_types.default.number]), onChange: prop_types.default.func, onClose: prop_types.default.func, onOpen: prop_types.default.func, parseDate: prop_types.default.func, readOnly: prop_types.default.oneOfType([prop_types.default.bool, prop_types.default.array]), short: prop_types.default.bool, value: prop_types.default.oneOfType([ prop_types.default.string, prop_types.default.arrayOf(prop_types.default.oneOfType([ prop_types.default.string, prop_types.default.number, prop_types.default.object ])), prop_types.default.object, prop_types.default.number ]), warn: prop_types.default.bool, nextMonthAriaLabel: prop_types.default.string, prevMonthAriaLabel: prop_types.default.string }; //#endregion exports.default = DatePicker;