@spark-web/date-picker
Version:
--- title: DatePicker storybookPath: forms-date-picker isExperimentalPackage: true ---
1,052 lines (1,011 loc) • 37.7 kB
JavaScript
'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;