@spark-web/date-picker
Version:
--- title: DatePicker storybookPath: forms-date-picker isExperimentalPackage: true ---
577 lines (548 loc) • 18.6 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 icon = require('@spark-web/icon');
var reactDayPicker = require('react-day-picker');
var FocusLock = require('react-focus-lock');
var css = require('@emotion/css');
var a11y = require('@spark-web/a11y');
var box = require('@spark-web/box');
var button = require('@spark-web/button');
var heading = require('@spark-web/heading');
var text = require('@spark-web/text');
var theme = require('@spark-web/theme');
var jsxRuntime = require('react/jsx-runtime');
var field = require('@spark-web/field');
var textInput = require('@spark-web/text-input');
var dateFns = require('date-fns');
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
var FocusLock__default = /*#__PURE__*/_interopDefault(FocusLock);
function CalendarContainer(_ref) {
var children = _ref.children;
var dayPickerStyles = useDayPickerStyles();
return /*#__PURE__*/jsxRuntime.jsx(box.Box, {
background: "surface",
border: "standard",
borderRadius: "medium",
display: "inline-block",
padding: "small",
position: "relative",
shadow: "medium",
className: css.css(dayPickerStyles),
children: children
});
}
function useDayPickerStyles() {
var theme$1 = theme.useTheme();
var cellSize = theme$1.sizing.medium;
var _useHeading = heading.useHeading({
level: '3',
align: 'left'
}),
_useHeading2 = _slicedToArray(_useHeading, 2),
typographyHeadingStyles = _useHeading2[0],
responsiveHeadingStyles = _useHeading2[1];
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'
},
// Header
'.rdp-caption': {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: theme$1.sizing.medium,
position: 'relative'
},
'.rdp-caption_label': _objectSpread(_objectSpread(_objectSpread({}, typographyHeadingStyles), responsiveHeadingStyles), {}, {
margin: 0,
whiteSpace: 'nowrap'
}),
// Left / right arrows
'.rdp-nav': {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
paddingLeft: theme$1.spacing.medium - theme$1.spacing.small,
paddingRight: theme$1.spacing.medium - theme$1.spacing.small
},
'.rdp-nav_button': _objectSpread(_objectSpread({}, buttonStyles), {}, {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
height: theme$1.sizing.small,
width: theme$1.sizing.small,
borderRadius: theme$1.border.radius.full
}),
'.rdp-nav_button:focus': _objectSpread(_objectSpread({}, focusStyles), {}, {
position: 'relative',
backgroundColor: theme$1.backgroundInteractions.primaryLowHover
}),
// 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'
},
'.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
}
};
}
function CalendarSingle(props) {
return /*#__PURE__*/jsxRuntime.jsx(FocusLock__default["default"], {
autoFocus: false,
returnFocus: true,
children: /*#__PURE__*/jsxRuntime.jsx(CalendarContainer, {
children: /*#__PURE__*/jsxRuntime.jsx(reactDayPicker.DayPicker, _objectSpread(_objectSpread({}, props), {}, {
mode: "single",
components: calendarComponents
}))
})
});
}
var calendarComponents = {
IconRight: function IconRight() {
return /*#__PURE__*/jsxRuntime.jsx(icon.ChevronRightIcon, {
size: "xsmall"
});
},
IconLeft: function IconLeft() {
return /*#__PURE__*/jsxRuntime.jsx(icon.ChevronLeftIcon, {
size: "xsmall"
});
}
};
/** 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 (Boolean(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 /*#__PURE__*/jsxRuntime.jsx(textInput.TextInput, _objectSpread(_objectSpread({}, consumerProps), {}, {
ref: forwardedRef,
value: value,
children: /*#__PURE__*/jsxRuntime.jsx(textInput.InputAdornment, {
placement: "end",
children: /*#__PURE__*/jsxRuntime.jsx(button.BaseButton, _objectSpread(_objectSpread({}, boxProps), {}, {
"aria-label": buttonLabel,
onClick: buttonOnClick,
ref: buttonRef,
disabled: disabled,
className: css.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: /*#__PURE__*/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", "initialMonth", "maxDate", "minDate", "onChange", "value"];
var DatePicker = /*#__PURE__*/react.forwardRef(function DatePicker(_ref, forwardedRef) {
var data = _ref.data,
initialMonth = _ref.initialMonth,
maxDate = _ref.maxDate,
minDate = _ref.minDate,
onChange = _ref.onChange,
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]);
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 /*#__PURE__*/jsxRuntime.jsxs(stack.Stack, {
ref: setRefEl,
onKeyDown: handleEscape,
data: data,
width: "full",
children: [/*#__PURE__*/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 && /*#__PURE__*/jsxRuntime.jsx("div", _objectSpread(_objectSpread({}, attributes.popper), {}, {
ref: setPopperEl,
style: _objectSpread(_objectSpread({}, styles.popper), {}, {
zIndex: 1
}),
children: /*#__PURE__*/jsxRuntime.jsx(CalendarSingle, {
defaultMonth: value || initialMonth,
disabled: disabledCalendarDays,
initialFocus: true,
numberOfMonths: 1,
onSelect: onSelect,
selected: value
})
}))]
});
});
exports.DatePicker = DatePicker;