@carbon/react
Version:
React components for the Carbon Design System
494 lines (492 loc) • 21.7 kB
JavaScript
/**
* 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;