UNPKG

@spark-web/date-picker

Version:

--- title: DatePicker storybookPath: forms-date-picker isExperimentalPackage: true ---

1,052 lines (1,011 loc) 37.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _objectSpread = require('@babel/runtime/helpers/objectSpread2'); var _slicedToArray = require('@babel/runtime/helpers/slicedToArray'); var _objectWithoutProperties = require('@babel/runtime/helpers/objectWithoutProperties'); var stack = require('@spark-web/stack'); var react = require('react'); var reactPopper = require('react-popper'); var reactDayPicker = require('react-day-picker'); var FocusLock = require('react-focus-lock'); var box = require('@spark-web/box'); var button = require('@spark-web/button'); var icon = require('@spark-web/icon'); var text = require('@spark-web/text'); var theme = require('@spark-web/theme'); var dateFns = require('date-fns'); var Select = require('react-select'); var jsxRuntime = require('@emotion/react/jsx-runtime'); var react$1 = require('@emotion/react'); var a11y = require('@spark-web/a11y'); var field = require('@spark-web/field'); var textInput = require('@spark-web/text-input'); function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; } var FocusLock__default = /*#__PURE__*/_interopDefault(FocusLock); var Select__default = /*#__PURE__*/_interopDefault(Select); var YearRangeContext = /*#__PURE__*/react.createContext(undefined); function YearRangeProvider(_ref) { var fromYear = _ref.fromYear, toYear = _ref.toYear, children = _ref.children; var value = react.useMemo(function () { return { fromYear: fromYear, toYear: toYear }; }, [fromYear, toYear]); return jsxRuntime.jsx(YearRangeContext.Provider, { value: value, children: children }); } function useYearRange() { var context = react.useContext(YearRangeContext); if (!context) { throw new Error('useYearRange must be used within a YearRangeProvider'); } return context; } //////////////////////////////////////////////////////////////////////////////// // MonthYearCaption — the whole-`Caption` override // // Overrides react-day-picker's entire caption (label + navigation) so we own // the full 44px header row: prev arrow, month trigger, year trigger, next // arrow. Opening a dropdown covers the day grid with a full-card option list // (rendered here so react-day-picker hooks stay available), while the grid // underneath stays mounted so `role="grid"` remains in the DOM. //////////////////////////////////////////////////////////////////////////////// function MonthYearCaption(_ref2) { var id = _ref2.id, displayMonth = _ref2.displayMonth; var theme$1 = theme.useTheme(); var _useNavigation = reactDayPicker.useNavigation(), goToMonth = _useNavigation.goToMonth, previousMonth = _useNavigation.previousMonth, nextMonth = _useNavigation.nextMonth; var _useYearRange = useYearRange(), fromYear = _useYearRange.fromYear, toYear = _useYearRange.toYear; var _useState = react.useState(null), _useState2 = _slicedToArray(_useState, 2), openDropdown = _useState2[0], setOpenDropdown = _useState2[1]; var monthTriggerRef = react.useRef(null); var yearTriggerRef = react.useRef(null); var generatedId = react.useId(); var monthListboxId = "calendar-month-listbox-".concat(generatedId); var yearListboxId = "calendar-year-listbox-".concat(generatedId); var displayedMonth = displayMonth.getMonth(); var displayedYear = displayMonth.getFullYear(); // The trigger shows the abbreviated month ("Jul"); the dropdown list keeps // the full month names ("July"). var monthTriggerLabel = dateFns.format(displayMonth, 'MMM'); var monthOptions = react.useMemo(function () { return Array.from({ length: 12 }, function (_, i) { return { label: dateFns.format(new Date(2000, i, 1), 'MMMM'), value: i }; }); }, []); var yearOptions = react.useMemo(function () { var years = new Set(); for (var year = fromYear; year <= toYear; year++) { years.add(year); } // Always include the currently displayed year, even if it falls outside // [fromYear, toYear], so the trigger never shows a year missing from its // list (backward-compatibility requirement). years.add(displayedYear); return Array.from(years).sort(function (a, b) { return a - b; }).map(function (year) { return { label: String(year), value: year }; }); }, [fromYear, toYear, displayedYear]); var closeDropdown = function closeDropdown() { var returnFocus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; var which = openDropdown; setOpenDropdown(null); if (returnFocus) { var _ref$current; var ref = which === 'month' ? monthTriggerRef : yearTriggerRef; (_ref$current = ref.current) === null || _ref$current === void 0 || _ref$current.focus(); } }; var handleMonthChange = function handleMonthChange(month) { goToMonth(new Date(displayedYear, month, 1)); closeDropdown(); }; var handleYearChange = function handleYearChange(year) { goToMonth(new Date(year, displayedMonth, 1)); closeDropdown(); }; var navArrowsHidden = openDropdown !== null; return jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs(box.Box // react-day-picker labels the month `role="grid"` table via // `aria-labelledby={id}`; keep that id on the header so the grid // retains an accessible name now that the default caption is replaced. , { id: id, display: "flex", flexDirection: "row", alignItems: "center", justifyContent: "spaceBetween", css: { height: theme$1.sizing.medium }, children: [jsxRuntime.jsx(NavArrowButton, { "aria-label": "Previous month", hidden: navArrowsHidden, disabled: !previousMonth, onClick: function onClick() { return previousMonth && goToMonth(previousMonth); }, icon: jsxRuntime.jsx(icon.ArrowLeftIcon, { size: "xsmall", tone: previousMonth ? 'neutral' : 'disabled' }) }), jsxRuntime.jsxs(box.Box, { display: "flex", flexDirection: "row", alignItems: "center", children: [jsxRuntime.jsx(CalendarSelectTrigger, { ref: monthTriggerRef, "aria-label": "Month", label: monthTriggerLabel, isOpen: openDropdown === 'month', disabled: openDropdown === 'year', onToggle: function onToggle() { return setOpenDropdown(function (prev) { return prev === 'month' ? null : 'month'; }); } }), jsxRuntime.jsx(CalendarSelectTrigger, { ref: yearTriggerRef, "aria-label": "Year", label: String(displayedYear), isOpen: openDropdown === 'year', disabled: openDropdown === 'month', onToggle: function onToggle() { return setOpenDropdown(function (prev) { return prev === 'year' ? null : 'year'; }); } })] }), jsxRuntime.jsx(NavArrowButton, { "aria-label": "Next month", hidden: navArrowsHidden, disabled: !nextMonth, onClick: function onClick() { return nextMonth && goToMonth(nextMonth); }, icon: jsxRuntime.jsx(icon.ArrowRightIcon, { size: "xsmall", tone: nextMonth ? 'neutral' : 'disabled' }) })] }), openDropdown === 'month' && jsxRuntime.jsx(CalendarSelectOverlay, { id: monthListboxId, "aria-label": "Month", options: monthOptions, value: displayedMonth, onChange: handleMonthChange, onClose: closeDropdown }), openDropdown === 'year' && jsxRuntime.jsx(CalendarSelectOverlay, { id: yearListboxId, "aria-label": "Year", options: yearOptions, value: displayedYear, onChange: handleYearChange, onClose: closeDropdown })] }); } //////////////////////////////////////////////////////////////////////////////// // NavArrowButton — a 44x44 prev/next navigation arrow //////////////////////////////////////////////////////////////////////////////// function NavArrowButton(_ref3) { var ariaLabel = _ref3['aria-label'], icon = _ref3.icon, disabled = _ref3.disabled, hidden = _ref3.hidden, onClick = _ref3.onClick; var theme$1 = theme.useTheme(); return jsxRuntime.jsx(button.BaseButton, { "aria-label": ariaLabel, "aria-hidden": hidden || undefined, tabIndex: hidden ? -1 : undefined, disabled: disabled, onClick: onClick, cursor: disabled ? 'default' : 'pointer', css: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: theme$1.sizing.medium, height: theme$1.sizing.medium, // Match the month/year triggers' rounded-rectangle hover shape. borderRadius: theme$1.border.radius.small, visibility: hidden ? 'hidden' : 'visible', '&:hover': disabled ? undefined : { backgroundColor: theme$1.backgroundInteractions.neutralHover } }, children: icon }); } //////////////////////////////////////////////////////////////////////////////// // CalendarSelectTrigger — the month / year label button //////////////////////////////////////////////////////////////////////////////// var CalendarSelectTrigger = /*#__PURE__*/react.forwardRef(function CalendarSelectTrigger(_ref4, ref) { var ariaLabel = _ref4['aria-label'], label = _ref4.label, isOpen = _ref4.isOpen, disabled = _ref4.disabled, onToggle = _ref4.onToggle; var theme$1 = theme.useTheme(); return jsxRuntime.jsxs(button.BaseButton, { ref: ref, "aria-label": ariaLabel, "aria-haspopup": "listbox", "aria-expanded": isOpen // No `aria-controls`: once open, react-select owns the combobox/listbox // relationship for the focused input; pointing at a non-react-select id // here would be a dangling reference. , disabled: disabled // Keep the greyed (aria-disabled) trigger out of the tab order while the // other dropdown is open, so Tab doesn't land on a non-interactive control. , tabIndex: disabled ? -1 : undefined, onClick: onToggle, cursor: disabled ? 'default' : 'pointer', css: { display: 'inline-flex', alignItems: 'center', gap: theme$1.spacing.xxsmall, borderRadius: theme$1.border.radius.small, height: theme$1.sizing.medium, paddingLeft: theme$1.spacing.small, paddingRight: theme$1.spacing.small, '&:hover': disabled ? undefined : { backgroundColor: theme$1.backgroundInteractions.neutralHover } }, children: [jsxRuntime.jsx(text.Text, { size: "large", weight: "semibold", tone: disabled ? 'disabled' : 'neutral', children: label }), jsxRuntime.jsx(box.Box, { display: "flex", css: { transition: 'transform 150ms ease', transform: isOpen ? 'rotate(180deg)' : undefined }, children: jsxRuntime.jsx(icon.ChevronDownIcon, { size: "xsmall", tone: disabled ? 'disabled' : 'neutral' }) })] }); }); //////////////////////////////////////////////////////////////////////////////// // CalendarSelectOverlay — the full-card scrolling option list // // Absolutely positioned to cover the day grid (weekday row + days). Positions // against `.rdp-month` (set to `position: relative` in calendar-container), // starting below the 44px header row. //////////////////////////////////////////////////////////////////////////////// function CalendarSelectOverlay(_ref5) { var _theme$components$tex, _options$find; var id = _ref5.id, ariaLabel = _ref5['aria-label'], options = _ref5.options, value = _ref5.value, _onChange = _ref5.onChange, onClose = _ref5.onClose; var theme$1 = theme.useTheme(); var menuOption = (_theme$components$tex = theme$1.components.textInput) === null || _theme$components$tex === void 0 ? void 0 : _theme$components$tex.menuOption; var wrapperRef = react.useRef(null); var selectedOption = (_options$find = options.find(function (option) { return option.value === value; })) !== null && _options$find !== void 0 ? _options$find : null; // Hide the day grid beneath the overlay from the tab order + assistive tech // while the dropdown is open, restoring it on close. react.useEffect(function () { var _wrapperRef$current; var grid = (_wrapperRef$current = wrapperRef.current) === null || _wrapperRef$current === void 0 || (_wrapperRef$current = _wrapperRef$current.closest('.rdp-month')) === null || _wrapperRef$current === void 0 ? void 0 : _wrapperRef$current.querySelector('.rdp-table'); if (!grid) { return; } grid.setAttribute('inert', ''); return function () { return grid.removeAttribute('inert'); }; }, []); // Centre the selected option on open. react-select scrolls the focused // (= selected) option to the top of the list in its own mount; a parent // layout effect runs after that but before paint, so we re-scroll the LISTBOX // — never the page — to sit the selection mid-list (~2 options above/below) // with no visible flash. The selected option is tagged with `data-selected` // by the Option override because react-select omits `aria-selected` on Apple. react.useLayoutEffect(function () { var _wrapperRef$current2; var list = (_wrapperRef$current2 = wrapperRef.current) === null || _wrapperRef$current2 === void 0 ? void 0 : _wrapperRef$current2.querySelector('[role="listbox"]'); var selected = list === null || list === void 0 ? void 0 : list.querySelector('[data-selected="true"]'); if (!list || !selected) { return; } list.scrollTop = selected.offsetTop - (list.clientHeight - selected.offsetHeight) / 2; }, []); // Close only this dropdown on Escape, and stop the event from bubbling to // date-picker.tsx's Stack handler (which would otherwise close the whole // calendar). We drive close from here rather than react-select's // `onMenuClose` because that also fires on blur — e.g. when clicking the // trigger to close, the blur-close would race the trigger's toggle and // immediately reopen the dropdown. var handleWrapperKeyDown = function handleWrapperKeyDown(event) { if (event.key === 'Escape') { event.stopPropagation(); onClose(); } }; var selectComponents = react.useMemo(function () { return { // Strip the popover chrome — the overlay Box is the surface. DropdownIndicator: function DropdownIndicator() { return null; }, IndicatorSeparator: function IndicatorSeparator() { return null; }, ClearIndicator: function ClearIndicator() { return null; }, // Add an accessible name to the listbox (react-select's `aria-label` lands // on the combobox input, not the menu list) so existing queries resolve. MenuList: function MenuList(props) { return jsxRuntime.jsx(Select.components.MenuList, _objectSpread(_objectSpread({}, props), {}, { innerProps: _objectSpread(_objectSpread({}, props.innerProps), {}, { 'aria-label': ariaLabel }), children: props.children })); }, // Spark-styled row: month/year label + a check on the selected row. The // `data-selected` marker drives the centre-on-open scroll above. Option: function Option(props) { return jsxRuntime.jsxs(Select.components.Option, _objectSpread(_objectSpread({}, props), {}, { innerProps: _objectSpread(_objectSpread({}, props.innerProps), {}, { 'data-selected': props.isSelected ? 'true' : undefined }), children: [jsxRuntime.jsx(text.Text, { size: "standard", weight: props.isSelected ? 'semibold' : 'regular', tone: props.isSelected ? 'primaryActive' : 'neutral', children: props.data.label }), props.isSelected ? jsxRuntime.jsx(icon.CheckIcon, { size: "xsmall", tone: "primaryActive" }) : null] })); } }; }, [ariaLabel]); var selectStyles = react.useMemo(function () { return { // Fill the overlay as a flex column: collapsed control on top, scrolling // list below. container: function container(base) { return _objectSpread(_objectSpread({}, base), {}, { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }); }, // Keep react-select's input mounted (needed for keyboard nav) but collapse // it to zero height and clip it so it is invisible yet still focusable. control: function control() { return { height: 0, minHeight: 0, padding: 0, border: 0, overflow: 'hidden' }; }, // The menu is rendered inline (no popover): static position, no shadow / // radius / margin, filling the remaining height. menu: function menu(base) { return _objectSpread(_objectSpread({}, base), {}, { position: 'static', width: '100%', margin: 0, boxShadow: 'none', borderRadius: 0, backgroundColor: 'transparent', display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }); }, menuList: function menuList(base) { return _objectSpread(_objectSpread({}, base), {}, { position: 'relative', flex: 1, minHeight: 0, maxHeight: 'none', overflowY: 'auto', padding: 0 }); }, option: function option(base, state) { var _menuOption$selected, _menuOption$focused, _menuOption$selected2, _menuOption$active; return _objectSpread(_objectSpread({}, base), {}, { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: theme$1.spacing.small, height: theme$1.sizing.medium, paddingLeft: theme$1.spacing.large, paddingRight: theme$1.spacing.large, cursor: 'pointer', backgroundColor: state.isSelected ? menuOption === null || menuOption === void 0 || (_menuOption$selected = menuOption.selected) === null || _menuOption$selected === void 0 ? void 0 : _menuOption$selected.backgroundColor : state.isFocused ? menuOption === null || menuOption === void 0 || (_menuOption$focused = menuOption.focused) === null || _menuOption$focused === void 0 ? void 0 : _menuOption$focused.backgroundColor : 'transparent', ':active': { backgroundColor: state.isSelected ? menuOption === null || menuOption === void 0 || (_menuOption$selected2 = menuOption.selected) === null || _menuOption$selected2 === void 0 || (_menuOption$selected2 = _menuOption$selected2.active) === null || _menuOption$selected2 === void 0 ? void 0 : _menuOption$selected2.backgroundColor : menuOption === null || menuOption === void 0 || (_menuOption$active = menuOption.active) === null || _menuOption$active === void 0 ? void 0 : _menuOption$active.backgroundColor } }); } }; }, [theme$1, menuOption]); return jsxRuntime.jsx(box.Box, { ref: wrapperRef, position: "absolute", background: "surface", zIndex: "dropdown", onKeyDown: handleWrapperKeyDown, css: { top: theme$1.sizing.medium, left: 0, right: 0, bottom: 0, display: 'flex', flexDirection: 'column', borderTop: "".concat(theme$1.border.width.standard, "px solid ").concat(theme$1.border.color.standard) }, children: jsxRuntime.jsx(Select__default["default"], { instanceId: id, "aria-label": ariaLabel, options: options, value: selectedOption, onChange: function onChange(option) { return option && _onChange(option.value); }, menuIsOpen: true, isSearchable: false, isClearable: false, backspaceRemovesValue: false, controlShouldRenderValue: false, hideSelectedOptions: false, tabSelectsValue: false, closeMenuOnSelect: false, menuShouldScrollIntoView: false, menuPortalTarget: null, autoFocus: true, components: selectComponents, styles: selectStyles }) }); } function CalendarContainer(_ref) { var children = _ref.children; var dayPickerStyles = useDayPickerStyles(); return jsxRuntime.jsx(box.Box, { background: "surface", border: "standard", borderRadius: "medium", display: "inline-block", padding: "small", position: "relative", shadow: "medium", css: react$1.css(dayPickerStyles), children: children }); } function useDayPickerStyles() { var theme$1 = theme.useTheme(); var cellSize = theme$1.sizing.medium; var _useText = text.useText({ baseline: true, tone: 'neutral', size: 'small', weight: 'regular' }), _useText2 = _slicedToArray(_useText, 2), typographyTextStyles = _useText2[0], responsiveTextStyles = _useText2[1]; var _useButtonStyles = button.useButtonStyles({ iconOnly: false, prominence: 'none', size: 'medium', tone: 'primary' }), _useButtonStyles2 = _slicedToArray(_useButtonStyles, 2), buttonStyles = _useButtonStyles2[1]; var focusStyles = a11y.useFocusRing({ always: true }); return { '.rdp-vhidden': a11y.visuallyHiddenStyles, // Base button '.rdp-button_reset': { appearance: 'none', background: 'none', border: 'none', margin: 0, padding: 0, cursor: 'pointer', color: 'inherit', font: 'inherit' }, // Days of week '.rdp-head_cell': _objectSpread(_objectSpread(_objectSpread({}, typographyTextStyles), responsiveTextStyles), {}, { fontWeight: theme$1.typography.fontWeight.semibold, margin: 0, padding: 0, textAlign: 'center', verticalAlign: 'middle', height: cellSize, width: cellSize }), // Day button '.rdp-day': _objectSpread(_objectSpread(_objectSpread(_objectSpread({}, typographyTextStyles), responsiveTextStyles), buttonStyles), {}, { borderRadius: theme$1.border.radius.small }), '.rdp-day:focus': _objectSpread(_objectSpread({}, focusStyles), {}, { position: 'relative', backgroundColor: theme$1.backgroundInteractions.primaryLowHover }), ".rdp-button:disabled, .rdp-button[aria-disabled='true']": { color: theme$1.color.foreground.disabled, pointerEvents: 'none', userSelect: 'none' }, '.rdp-weeknumber, .rdp-day': { display: 'flex', justifyContent: 'center', alignItems: 'center', width: cellSize, height: cellSize }, // Table '.rdp-months': { display: 'flex' }, // Positioning context for the month/year dropdown overlay, which is // absolutely positioned to cover the day grid while a dropdown is open. '.rdp-month': { position: 'relative' }, '.rdp-month:first-of-type': { marginLeft: 0 }, '.rdp-month:last-of-type': { marginRight: 0 }, '.rdp-table': { margin: 0, maxWidth: "calc(".concat(cellSize, " * 7)"), borderCollapse: 'collapse' }, '.rdp-tbody': { border: 0 }, '.rdp-cell': { width: cellSize, height: cellSize, padding: 0, textAlign: 'center' }, ".rdp-day_selected:not([aria-disabled='true']), .rdp-day_selected:focus:not([aria-disabled='true']), .rdp-day_selected:active:not([aria-disabled='true']), .rdp-day_selected:hover:not([aria-disabled='true']), .rdp-day_selected:hover:not([aria-disabled='true'])": { backgroundColor: theme$1.color.background.primary, color: theme$1.color.foreground.neutralInverted } }; } var _excluded$2 = ["fromYear", "toYear"]; function CalendarSingle(_ref) { var fromYear = _ref.fromYear, toYear = _ref.toYear, props = _objectWithoutProperties(_ref, _excluded$2); return jsxRuntime.jsx(FocusLock__default["default"], { autoFocus: false, returnFocus: true, children: jsxRuntime.jsx(YearRangeProvider, { fromYear: fromYear, toYear: toYear, children: jsxRuntime.jsx(CalendarContainer, { children: jsxRuntime.jsx(reactDayPicker.DayPicker, _objectSpread(_objectSpread({}, props), {}, { mode: "single", components: calendarComponents })) }) }) }); } var calendarComponents = { // Override the whole Caption (not just CaptionLabel) so we render the entire // header row — nav arrows + month/year triggers — and react-day-picker's // default prev/next Nav is no longer rendered. Caption: MonthYearCaption }; /** Date format is not configurable. */ var dateFormat = 'dd/MM/yyyy'; /** Formats a date to 'dd/MM/yyyy'. */ function formatDate(date) { return dateFns.format(new Date(date), dateFormat); } /** Formats a date object into a more human readable form. */ function formatHumanReadableDate(date) { return dateFns.format(date, 'eeee MMMM do, yyyy'); } /** Checks whether a value is a Date. */ function isDate(value) { return dateFns.isDate(value); } /** * Returns a date parsed from a string that is in 'dd/MM/yyyy' format. * * @see https://github.com/date-fns/date-fns/issues/942 */ function parseDate(value) { if (value.length !== dateFormat.length) { return undefined; } var parsedDate = dateFns.parse(value, dateFormat, new Date()); if (isDate(parsedDate) && dateFns.isValid(parsedDate)) { return parsedDate; } return undefined; } /** * Constrains a date to be within a range: * * @see * [min](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#min) * and [max](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#max). */ function constrainDate(date, minDate, maxDate) { if (!date) { return date; } if (minDate && dateFns.isBefore(date, minDate)) { return minDate; } if (maxDate && dateFns.isAfter(date, maxDate)) { return maxDate; } return date; } /** * Returns the indexes of the separators in the date format */ function getSeparatorIndexes() { var indexes = []; for (var i = 0; i < dateFormat.length; i++) { if (dateFormat[i] === '/') { indexes.push(i); } } return indexes; } /** * This is used to format the date as the user types based on the given format * This should be used in the `onInputChange` event of the input field to allow formatting the date on type change even if the date is incomplete. * @param date * @returns string */ function formatDateOnChange(date, inputValue, cursorPosition) { var indexes = getSeparatorIndexes(); var format = dateFormat.toUpperCase(); var dateFormatParts = format.split('/'); var dateParts = date.split('/'); var newDate = []; // If the date is empty, defaults to format if (!inputValue || !date) return format; if (indexes.includes(cursorPosition)) { return date.slice(0, cursorPosition) + inputValue.slice(cursorPosition); } dateFormatParts.forEach(function (part, index) { var cleanValue = part; var cleanDate = dateParts[index].replace(/\D/g, ''); var length = part.length; if (cleanDate) { var _char = part.charAt(0); var formattedDate = cleanDate.padEnd(length, _char).slice(0, length); cleanValue = formattedDate; } newDate.push(cleanValue); }); return newDate.join('/'); } var _excluded$1 = ["buttonRef", "buttonOnClick", "value"]; var DateInput = /*#__PURE__*/react.forwardRef(function DateInput(_ref, forwardedRef) { var buttonRef = _ref.buttonRef, buttonOnClick = _ref.buttonOnClick, value = _ref.value, consumerProps = _objectWithoutProperties(_ref, _excluded$1); var _useIconButtonStyles = useIconButtonStyles(), _useIconButtonStyles2 = _slicedToArray(_useIconButtonStyles, 2), boxProps = _useIconButtonStyles2[0], buttonStyles = _useIconButtonStyles2[1]; var _useFieldContext = field.useFieldContext(), _useFieldContext2 = _slicedToArray(_useFieldContext, 1), disabled = _useFieldContext2[0].disabled; var buttonLabel = react.useMemo(function () { if (typeof value !== 'string') { return 'Choose date'; } var parsed = parseDate(value); if (!parsed) { return 'Choose date'; } return "Change Date, ".concat(formatHumanReadableDate(parsed)); }, [value]); return jsxRuntime.jsx(textInput.TextInput, _objectSpread(_objectSpread({}, consumerProps), {}, { ref: forwardedRef, value: value, children: jsxRuntime.jsx(textInput.InputAdornment, { placement: "end", children: jsxRuntime.jsx(button.BaseButton, _objectSpread(_objectSpread({}, boxProps), {}, { "aria-label": buttonLabel, onClick: buttonOnClick, ref: buttonRef, disabled: disabled, css: react$1.css(buttonStyles) // The input is not keyboard navigable when disabled and so we are // also removing the button from the tab index to make it less // confusing to keyboard and assistive technology users. , tabIndex: disabled ? -1 : undefined, children: jsxRuntime.jsx(icon.CalendarIcon, { tone: disabled ? 'disabled' : 'neutral' }) })) }) })); }); function useIconButtonStyles() { var _useButtonStyles = button.useButtonStyles({ iconOnly: false, prominence: 'none', size: 'medium', tone: 'neutral' }), _useButtonStyles2 = _slicedToArray(_useButtonStyles, 2), buttonStyles = _useButtonStyles2[1]; return [{ alignItems: 'center', borderRadius: 'full', cursor: 'pointer', display: 'inline-flex', gap: 'small', height: 'small', justifyContent: 'center', paddingX: 'xsmall', position: 'relative', width: 'small' }, buttonStyles]; } //////////////////////////////////////////////////////////////////////////////// /** * Useful for situations where you have separate buttons to open/close * or expand/collapse an element. */ function useTernaryState(initialValue) { var _useState = react.useState(initialValue), _useState2 = _slicedToArray(_useState, 2), state = _useState2[0], setState = _useState2[1]; var setTrue = react.useCallback(function () { return setState(true); }, []); var setFalse = react.useCallback(function () { return setState(false); }, []); return [state, setTrue, setFalse]; } //////////////////////////////////////////////////////////////////////////////// /** * Calls the provided handler function if a click is detected outside of a * specified element. */ function useClickOutside(ref, handler) { react.useEffect(function () { function listener(event) { var element = ref === null || ref === void 0 ? void 0 : ref.current; // Do nothing if clicking ref's element or descendent elements if (!element || element.contains(event.target)) { return; } handler(event); } window.addEventListener('mousedown', listener); return function () { return window.removeEventListener('mousedown', listener); }; }, [handler, ref]); } var _excluded = ["data", "fromYear", "initialMonth", "maxDate", "minDate", "onChange", "toYear", "value"]; var DatePicker = /*#__PURE__*/react.forwardRef(function DatePicker(_ref, forwardedRef) { var data = _ref.data, fromYear = _ref.fromYear, initialMonth = _ref.initialMonth, maxDate = _ref.maxDate, minDate = _ref.minDate, onChange = _ref.onChange, toYear = _ref.toYear, value = _ref.value, consumerProps = _objectWithoutProperties(_ref, _excluded); var _useTernaryState = useTernaryState(false), _useTernaryState2 = _slicedToArray(_useTernaryState, 3), isCalendarOpen = _useTernaryState2[0], openCalendar = _useTernaryState2[1], closeCalendar = _useTernaryState2[2]; // Popper state var triggerRef = react.useRef(null); var _useState = react.useState(null), _useState2 = _slicedToArray(_useState, 2), refEl = _useState2[0], setRefEl = _useState2[1]; var _useState3 = react.useState(null), _useState4 = _slicedToArray(_useState3, 2), popperEl = _useState4[0], setPopperEl = _useState4[1]; var _usePopper = reactPopper.usePopper(refEl, popperEl, { placement: 'bottom-start', modifiers: [{ name: 'offset', options: { offset: [0, 8] } }] }), styles = _usePopper.styles, attributes = _usePopper.attributes; var defaultValue = dateFormat.toUpperCase(); var _useState5 = react.useState(''), _useState6 = _slicedToArray(_useState5, 2), inputValue = _useState6[0], setInputValue = _useState6[1]; var onSelect = react.useCallback(function (_, selectedDay, modifiers) { // If the day is disabled, do nothing if (modifiers.disabled) { return; } // Update the input field with the selected day setInputValue(formatDate(selectedDay)); // Trigger the callback onChange(selectedDay); // Close the calendar and focus the calendar icon closeCalendar(); }, [onChange, closeCalendar]); var onInputChange = react.useCallback(function (event) { var _event$target$selecti; var indexes = getSeparatorIndexes(); var eventValue = event.target.value; var startPos = (_event$target$selecti = event.target.selectionStart) !== null && _event$target$selecti !== void 0 ? _event$target$selecti : 0; var formattedDate = formatDateOnChange(eventValue, inputValue, startPos); var nextPos = startPos; // to fix issue where cursor jumps to end of input when formatting value if (indexes.includes(startPos) && eventValue.length > inputValue.length) { nextPos = startPos + 1; } setInputValue(formattedDate); setCursorPosition(event, nextPos); var parsedDate = parseDate(formattedDate); var constrainedDate = constrainDate(parsedDate, minDate, maxDate); onChange(constrainedDate); }, [maxDate, minDate, onChange, inputValue]); // Update the text inputs when the value updates react.useEffect(function () { if (value) { setInputValue(formatDate(value)); } }, [value]); // Close the calendar when the user clicks outside var clickOutsideRef = react.useRef(popperEl); clickOutsideRef.current = popperEl; var handleClickOutside = react.useCallback(function () { if (isCalendarOpen) { closeCalendar(); } }, [isCalendarOpen, closeCalendar]); useClickOutside(clickOutsideRef, handleClickOutside); // Close the calendar when the user presses escape var handleEscape = react.useCallback(function (event) { if (isCalendarOpen && event.code === 'Escape') { event.preventDefault(); event.stopPropagation(); // Close the calendar and focus the calendar icon closeCalendar(); } }, [isCalendarOpen, closeCalendar]); // Resolve the year range offered by the calendar caption dropdowns. This // only affects the caption's year list — it does NOT bound day navigation. var _useMemo = react.useMemo(function () { var _ref2, _ref3; var currentYear = new Date().getFullYear(); return { resolvedFromYear: (_ref2 = fromYear !== null && fromYear !== void 0 ? fromYear : minDate === null || minDate === void 0 ? void 0 : minDate.getFullYear()) !== null && _ref2 !== void 0 ? _ref2 : currentYear - 100, resolvedToYear: (_ref3 = toYear !== null && toYear !== void 0 ? toYear : maxDate === null || maxDate === void 0 ? void 0 : maxDate.getFullYear()) !== null && _ref3 !== void 0 ? _ref3 : currentYear + 10 }; }, [fromYear, toYear, minDate, maxDate]), resolvedFromYear = _useMemo.resolvedFromYear, resolvedToYear = _useMemo.resolvedToYear; var disabledCalendarDays = react.useMemo(function () { if (!(minDate || maxDate)) { return; } return [minDate ? { before: minDate } : undefined, maxDate ? { after: maxDate } : undefined].filter(function (x) { return Boolean(x); }); }, [minDate, maxDate]); // sets the next cursor position var setCursorPosition = function setCursorPosition(event, position) { setTimeout(function () { event.target.setSelectionRange(position, position); }, 0); }; return jsxRuntime.jsxs(stack.Stack, { ref: setRefEl, onKeyDown: handleEscape, data: data, width: "full", children: [jsxRuntime.jsx(DateInput, _objectSpread(_objectSpread({}, consumerProps), {}, { buttonOnClick: openCalendar, buttonRef: triggerRef, onChange: onInputChange, ref: forwardedRef, value: inputValue, placeholder: defaultValue, onFocus: function onFocus(e) { if (!inputValue) setInputValue(defaultValue); setCursorPosition(e, 0); }, onBlur: function onBlur() { if (inputValue === defaultValue) setInputValue(''); } })), isCalendarOpen && jsxRuntime.jsx("div", _objectSpread(_objectSpread({}, attributes.popper), {}, { ref: setPopperEl, style: _objectSpread(_objectSpread({}, styles.popper), {}, { zIndex: 1 }), children: jsxRuntime.jsx(CalendarSingle, { defaultMonth: value || initialMonth, disabled: disabledCalendarDays, fromYear: resolvedFromYear, initialFocus: true, numberOfMonths: 1, onSelect: onSelect, selected: value, toYear: resolvedToYear }) }))] }); }); exports.DatePicker = DatePicker;