UNPKG

18.6 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5var _objectSpread = require('@babel/runtime/helpers/objectSpread2');
6var _slicedToArray = require('@babel/runtime/helpers/slicedToArray');
7var _objectWithoutProperties = require('@babel/runtime/helpers/objectWithoutProperties');
8var stack = require('@spark-web/stack');
9var react = require('react');
10var reactPopper = require('react-popper');
11var icon = require('@spark-web/icon');
12var reactDayPicker = require('react-day-picker');
13var FocusLock = require('react-focus-lock');
14var css = require('@emotion/css');
15var a11y = require('@spark-web/a11y');
16var box = require('@spark-web/box');
17var button = require('@spark-web/button');
18var heading = require('@spark-web/heading');
19var text = require('@spark-web/text');
20var theme = require('@spark-web/theme');
21var jsxRuntime = require('react/jsx-runtime');
22var field = require('@spark-web/field');
23var textInput = require('@spark-web/text-input');
24var dateFns = require('date-fns');
25
26function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
27
28var FocusLock__default = /*#__PURE__*/_interopDefault(FocusLock);
29
30function CalendarContainer(_ref) {
31 var children = _ref.children;
32 var dayPickerStyles = useDayPickerStyles();
33 return /*#__PURE__*/jsxRuntime.jsx(box.Box, {
34 background: "surface",
35 border: "standard",
36 borderRadius: "medium",
37 display: "inline-block",
38 padding: "small",
39 position: "relative",
40 shadow: "medium",
41 className: css.css(dayPickerStyles),
42 children: children
43 });
44}
45function useDayPickerStyles() {
46 var theme$1 = theme.useTheme();
47 var cellSize = theme$1.sizing.medium;
48 var _useHeading = heading.useHeading({
49 level: '3',
50 align: 'left'
51 }),
52 _useHeading2 = _slicedToArray(_useHeading, 2),
53 typographyHeadingStyles = _useHeading2[0],
54 responsiveHeadingStyles = _useHeading2[1];
55 var _useText = text.useText({
56 baseline: true,
57 tone: 'neutral',
58 size: 'small',
59 weight: 'regular'
60 }),
61 _useText2 = _slicedToArray(_useText, 2),
62 typographyTextStyles = _useText2[0],
63 responsiveTextStyles = _useText2[1];
64 var _useButtonStyles = button.useButtonStyles({
65 iconOnly: false,
66 prominence: 'none',
67 size: 'medium',
68 tone: 'primary'
69 }),
70 _useButtonStyles2 = _slicedToArray(_useButtonStyles, 2),
71 buttonStyles = _useButtonStyles2[1];
72 var focusStyles = a11y.useFocusRing({
73 always: true
74 });
75 return {
76 '.rdp-vhidden': a11y.visuallyHiddenStyles,
77 // Base button
78 '.rdp-button_reset': {
79 appearance: 'none',
80 background: 'none',
81 border: 'none',
82 margin: 0,
83 padding: 0,
84 cursor: 'pointer',
85 color: 'inherit',
86 font: 'inherit'
87 },
88 // Header
89 '.rdp-caption': {
90 display: 'flex',
91 alignItems: 'center',
92 justifyContent: 'center',
93 height: theme$1.sizing.medium,
94 position: 'relative'
95 },
96 '.rdp-caption_label': _objectSpread(_objectSpread(_objectSpread({}, typographyHeadingStyles), responsiveHeadingStyles), {}, {
97 margin: 0,
98 whiteSpace: 'nowrap'
99 }),
100 // Left / right arrows
101 '.rdp-nav': {
102 position: 'absolute',
103 top: 0,
104 bottom: 0,
105 left: 0,
106 right: 0,
107 display: 'flex',
108 alignItems: 'center',
109 justifyContent: 'space-between',
110 paddingLeft: theme$1.spacing.medium - theme$1.spacing.small,
111 paddingRight: theme$1.spacing.medium - theme$1.spacing.small
112 },
113 '.rdp-nav_button': _objectSpread(_objectSpread({}, buttonStyles), {}, {
114 display: 'inline-flex',
115 alignItems: 'center',
116 justifyContent: 'center',
117 cursor: 'pointer',
118 height: theme$1.sizing.small,
119 width: theme$1.sizing.small,
120 borderRadius: theme$1.border.radius.full
121 }),
122 '.rdp-nav_button:focus': _objectSpread(_objectSpread({}, focusStyles), {}, {
123 position: 'relative',
124 backgroundColor: theme$1.backgroundInteractions.primaryLowHover
125 }),
126 // Days of week
127 '.rdp-head_cell': _objectSpread(_objectSpread(_objectSpread({}, typographyTextStyles), responsiveTextStyles), {}, {
128 fontWeight: theme$1.typography.fontWeight.semibold,
129 margin: 0,
130 padding: 0,
131 textAlign: 'center',
132 verticalAlign: 'middle',
133 height: cellSize,
134 width: cellSize
135 }),
136 // Day button
137 '.rdp-day': _objectSpread(_objectSpread(_objectSpread(_objectSpread({}, typographyTextStyles), responsiveTextStyles), buttonStyles), {}, {
138 borderRadius: theme$1.border.radius.small
139 }),
140 '.rdp-day:focus': _objectSpread(_objectSpread({}, focusStyles), {}, {
141 position: 'relative',
142 backgroundColor: theme$1.backgroundInteractions.primaryLowHover
143 }),
144 ".rdp-button:disabled, .rdp-button[aria-disabled='true']": {
145 color: theme$1.color.foreground.disabled,
146 pointerEvents: 'none',
147 userSelect: 'none'
148 },
149 '.rdp-weeknumber, .rdp-day': {
150 display: 'flex',
151 justifyContent: 'center',
152 alignItems: 'center',
153 width: cellSize,
154 height: cellSize
155 },
156 // Table
157 '.rdp-months': {
158 display: 'flex'
159 },
160 '.rdp-month:first-of-type': {
161 marginLeft: 0
162 },
163 '.rdp-month:last-of-type': {
164 marginRight: 0
165 },
166 '.rdp-table': {
167 margin: 0,
168 maxWidth: "calc(".concat(cellSize, " * 7)"),
169 borderCollapse: 'collapse'
170 },
171 '.rdp-tbody': {
172 border: 0
173 },
174 '.rdp-cell': {
175 width: cellSize,
176 height: cellSize,
177 padding: 0,
178 textAlign: 'center'
179 },
180 ".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'])": {
181 backgroundColor: theme$1.color.background.primary,
182 color: theme$1.color.foreground.neutralInverted
183 }
184 };
185}
186
187function CalendarSingle(props) {
188 return /*#__PURE__*/jsxRuntime.jsx(FocusLock__default["default"], {
189 autoFocus: false,
190 returnFocus: true,
191 children: /*#__PURE__*/jsxRuntime.jsx(CalendarContainer, {
192 children: /*#__PURE__*/jsxRuntime.jsx(reactDayPicker.DayPicker, _objectSpread(_objectSpread({}, props), {}, {
193 mode: "single",
194 components: calendarComponents
195 }))
196 })
197 });
198}
199var calendarComponents = {
200 IconRight: function IconRight() {
201 return /*#__PURE__*/jsxRuntime.jsx(icon.ChevronRightIcon, {
202 size: "xsmall"
203 });
204 },
205 IconLeft: function IconLeft() {
206 return /*#__PURE__*/jsxRuntime.jsx(icon.ChevronLeftIcon, {
207 size: "xsmall"
208 });
209 }
210};
211
212/** Date format is not configurable. */
213var dateFormat = 'dd/MM/yyyy';
214
215/** Formats a date to 'dd/MM/yyyy'. */
216function formatDate(date) {
217 return dateFns.format(new Date(date), dateFormat);
218}
219
220/** Formats a date object into a more human readable form. */
221function formatHumanReadableDate(date) {
222 return dateFns.format(date, 'eeee MMMM do, yyyy');
223}
224
225/** Checks whether a value is a Date. */
226function isDate(value) {
227 return dateFns.isDate(value);
228}
229
230/**
231 * Returns a date parsed from a string that is in 'dd/MM/yyyy' format.
232 *
233 * @see https://github.com/date-fns/date-fns/issues/942
234 */
235function parseDate(value) {
236 if (value.length !== dateFormat.length) {
237 return undefined;
238 }
239 var parsedDate = dateFns.parse(value, dateFormat, new Date());
240 if (isDate(parsedDate) && dateFns.isValid(parsedDate)) {
241 return parsedDate;
242 }
243 return undefined;
244}
245
246/**
247 * Constrains a date to be within a range:
248 *
249 * @see
250 * [min](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#min)
251 * and [max](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#max).
252 */
253function constrainDate(date, minDate, maxDate) {
254 if (!date) {
255 return date;
256 }
257 if (minDate && dateFns.isBefore(date, minDate)) {
258 return minDate;
259 }
260 if (maxDate && dateFns.isAfter(date, maxDate)) {
261 return maxDate;
262 }
263 return date;
264}
265
266/**
267 * Returns the indexes of the separators in the date format
268 */
269function getSeparatorIndexes() {
270 var indexes = [];
271 for (var i = 0; i < dateFormat.length; i++) {
272 if (dateFormat[i] === '/') {
273 indexes.push(i);
274 }
275 }
276 return indexes;
277}
278
279/**
280 * This is used to format the date as the user types based on the given format
281 * 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.
282 * @param date
283 * @returns string
284 */
285function formatDateOnChange(date, inputValue, cursorPosition) {
286 var indexes = getSeparatorIndexes();
287 var format = dateFormat.toUpperCase();
288 var dateFormatParts = format.split('/');
289 var dateParts = date.split('/');
290 var newDate = [];
291
292 // If the date is empty, defaults to format
293 if (!inputValue || !date) return format;
294 if (indexes.includes(cursorPosition)) {
295 return date.slice(0, cursorPosition) + inputValue.slice(cursorPosition);
296 }
297 dateFormatParts.forEach(function (part, index) {
298 var cleanValue = part;
299 var cleanDate = dateParts[index].replace(/\D/g, '');
300 var length = part.length;
301 if (Boolean(cleanDate)) {
302 var _char = part.charAt(0);
303 var formattedDate = cleanDate.padEnd(length, _char).slice(0, length);
304 cleanValue = formattedDate;
305 }
306 newDate.push(cleanValue);
307 });
308 return newDate.join('/');
309}
310
311var _excluded$1 = ["buttonRef", "buttonOnClick", "value"];
312var DateInput = /*#__PURE__*/react.forwardRef(function DateInput(_ref, forwardedRef) {
313 var buttonRef = _ref.buttonRef,
314 buttonOnClick = _ref.buttonOnClick,
315 value = _ref.value,
316 consumerProps = _objectWithoutProperties(_ref, _excluded$1);
317 var _useIconButtonStyles = useIconButtonStyles(),
318 _useIconButtonStyles2 = _slicedToArray(_useIconButtonStyles, 2),
319 boxProps = _useIconButtonStyles2[0],
320 buttonStyles = _useIconButtonStyles2[1];
321 var _useFieldContext = field.useFieldContext(),
322 _useFieldContext2 = _slicedToArray(_useFieldContext, 1),
323 disabled = _useFieldContext2[0].disabled;
324 var buttonLabel = react.useMemo(function () {
325 if (typeof value !== 'string') {
326 return 'Choose date';
327 }
328 var parsed = parseDate(value);
329 if (!parsed) {
330 return 'Choose date';
331 }
332 return "Change Date, ".concat(formatHumanReadableDate(parsed));
333 }, [value]);
334 return /*#__PURE__*/jsxRuntime.jsx(textInput.TextInput, _objectSpread(_objectSpread({}, consumerProps), {}, {
335 ref: forwardedRef,
336 value: value,
337 children: /*#__PURE__*/jsxRuntime.jsx(textInput.InputAdornment, {
338 placement: "end",
339 children: /*#__PURE__*/jsxRuntime.jsx(button.BaseButton, _objectSpread(_objectSpread({}, boxProps), {}, {
340 "aria-label": buttonLabel,
341 onClick: buttonOnClick,
342 ref: buttonRef,
343 disabled: disabled,
344 className: css.css(buttonStyles)
345 // The input is not keyboard navigable when disabled and so we are
346 // also removing the button from the tab index to make it less
347 // confusing to keyboard and assistive technology users.
348 ,
349 tabIndex: disabled ? -1 : undefined,
350 children: /*#__PURE__*/jsxRuntime.jsx(icon.CalendarIcon, {
351 tone: disabled ? 'disabled' : 'neutral'
352 })
353 }))
354 })
355 }));
356});
357function useIconButtonStyles() {
358 var _useButtonStyles = button.useButtonStyles({
359 iconOnly: false,
360 prominence: 'none',
361 size: 'medium',
362 tone: 'neutral'
363 }),
364 _useButtonStyles2 = _slicedToArray(_useButtonStyles, 2),
365 buttonStyles = _useButtonStyles2[1];
366 return [{
367 alignItems: 'center',
368 borderRadius: 'full',
369 cursor: 'pointer',
370 display: 'inline-flex',
371 gap: 'small',
372 height: 'small',
373 justifyContent: 'center',
374 paddingX: 'xsmall',
375 position: 'relative',
376 width: 'small'
377 }, buttonStyles];
378}
379
380////////////////////////////////////////////////////////////////////////////////
381
382/**
383 * Useful for situations where you have separate buttons to open/close
384 * or expand/collapse an element.
385 */
386function useTernaryState(initialValue) {
387 var _useState = react.useState(initialValue),
388 _useState2 = _slicedToArray(_useState, 2),
389 state = _useState2[0],
390 setState = _useState2[1];
391 var setTrue = react.useCallback(function () {
392 return setState(true);
393 }, []);
394 var setFalse = react.useCallback(function () {
395 return setState(false);
396 }, []);
397 return [state, setTrue, setFalse];
398}
399////////////////////////////////////////////////////////////////////////////////
400
401/**
402 * Calls the provided handler function if a click is detected outside of a
403 * specified element.
404 */
405function useClickOutside(ref, handler) {
406 react.useEffect(function () {
407 function listener(event) {
408 var element = ref === null || ref === void 0 ? void 0 : ref.current;
409
410 // Do nothing if clicking ref's element or descendent elements
411 if (!element || element.contains(event.target)) {
412 return;
413 }
414 handler(event);
415 }
416 window.addEventListener('mousedown', listener);
417 return function () {
418 return window.removeEventListener('mousedown', listener);
419 };
420 }, [handler, ref]);
421}
422
423var _excluded = ["data", "initialMonth", "maxDate", "minDate", "onChange", "value"];
424var DatePicker = /*#__PURE__*/react.forwardRef(function DatePicker(_ref, forwardedRef) {
425 var data = _ref.data,
426 initialMonth = _ref.initialMonth,
427 maxDate = _ref.maxDate,
428 minDate = _ref.minDate,
429 onChange = _ref.onChange,
430 value = _ref.value,
431 consumerProps = _objectWithoutProperties(_ref, _excluded);
432 var _useTernaryState = useTernaryState(false),
433 _useTernaryState2 = _slicedToArray(_useTernaryState, 3),
434 isCalendarOpen = _useTernaryState2[0],
435 openCalendar = _useTernaryState2[1],
436 closeCalendar = _useTernaryState2[2];
437
438 // Popper state
439 var triggerRef = react.useRef(null);
440 var _useState = react.useState(null),
441 _useState2 = _slicedToArray(_useState, 2),
442 refEl = _useState2[0],
443 setRefEl = _useState2[1];
444 var _useState3 = react.useState(null),
445 _useState4 = _slicedToArray(_useState3, 2),
446 popperEl = _useState4[0],
447 setPopperEl = _useState4[1];
448 var _usePopper = reactPopper.usePopper(refEl, popperEl, {
449 placement: 'bottom-start',
450 modifiers: [{
451 name: 'offset',
452 options: {
453 offset: [0, 8]
454 }
455 }]
456 }),
457 styles = _usePopper.styles,
458 attributes = _usePopper.attributes;
459 var defaultValue = dateFormat.toUpperCase();
460 var _useState5 = react.useState(''),
461 _useState6 = _slicedToArray(_useState5, 2),
462 inputValue = _useState6[0],
463 setInputValue = _useState6[1];
464 var onSelect = react.useCallback(function (_, selectedDay, modifiers) {
465 // If the day is disabled, do nothing
466 if (modifiers.disabled) {
467 return;
468 }
469 // Update the input field with the selected day
470 setInputValue(formatDate(selectedDay));
471 // Trigger the callback
472 onChange(selectedDay);
473 // Close the calendar and focus the calendar icon
474 closeCalendar();
475 }, [onChange, closeCalendar]);
476 var onInputChange = react.useCallback(function (event) {
477 var _event$target$selecti;
478 var indexes = getSeparatorIndexes();
479 var eventValue = event.target.value;
480 var startPos = (_event$target$selecti = event.target.selectionStart) !== null && _event$target$selecti !== void 0 ? _event$target$selecti : 0;
481 var formattedDate = formatDateOnChange(eventValue, inputValue, startPos);
482 var nextPos = startPos;
483
484 // to fix issue where cursor jumps to end of input when formatting value
485 if (indexes.includes(startPos) && eventValue.length > inputValue.length) {
486 nextPos = startPos + 1;
487 }
488 setInputValue(formattedDate);
489 setCursorPosition(event, nextPos);
490 var parsedDate = parseDate(formattedDate);
491 var constrainedDate = constrainDate(parsedDate, minDate, maxDate);
492 onChange(constrainedDate);
493 }, [maxDate, minDate, onChange, inputValue]);
494
495 // Update the text inputs when the value updates
496 react.useEffect(function () {
497 if (value) {
498 setInputValue(formatDate(value));
499 }
500 }, [value]);
501
502 // Close the calendar when the user clicks outside
503 var clickOutsideRef = react.useRef(popperEl);
504 clickOutsideRef.current = popperEl;
505 var handleClickOutside = react.useCallback(function () {
506 if (isCalendarOpen) {
507 closeCalendar();
508 }
509 }, [isCalendarOpen, closeCalendar]);
510 useClickOutside(clickOutsideRef, handleClickOutside);
511
512 // Close the calendar when the user presses escape
513 var handleEscape = react.useCallback(function (event) {
514 if (isCalendarOpen && event.code === 'Escape') {
515 event.preventDefault();
516 event.stopPropagation();
517 // Close the calendar and focus the calendar icon
518 closeCalendar();
519 }
520 }, [isCalendarOpen, closeCalendar]);
521 var disabledCalendarDays = react.useMemo(function () {
522 if (!(minDate || maxDate)) {
523 return;
524 }
525 return [minDate ? {
526 before: minDate
527 } : undefined, maxDate ? {
528 after: maxDate
529 } : undefined].filter(function (x) {
530 return Boolean(x);
531 });
532 }, [minDate, maxDate]);
533
534 // sets the next cursor position
535 var setCursorPosition = function setCursorPosition(event, position) {
536 setTimeout(function () {
537 event.target.setSelectionRange(position, position);
538 }, 0);
539 };
540 return /*#__PURE__*/jsxRuntime.jsxs(stack.Stack, {
541 ref: setRefEl,
542 onKeyDown: handleEscape,
543 data: data,
544 width: "full",
545 children: [/*#__PURE__*/jsxRuntime.jsx(DateInput, _objectSpread(_objectSpread({}, consumerProps), {}, {
546 buttonOnClick: openCalendar,
547 buttonRef: triggerRef,
548 onChange: onInputChange,
549 ref: forwardedRef,
550 value: inputValue,
551 placeholder: defaultValue,
552 onFocus: function onFocus(e) {
553 if (!inputValue) setInputValue(defaultValue);
554 setCursorPosition(e, 0);
555 },
556 onBlur: function onBlur() {
557 if (inputValue === defaultValue) setInputValue('');
558 }
559 })), isCalendarOpen && /*#__PURE__*/jsxRuntime.jsx("div", _objectSpread(_objectSpread({}, attributes.popper), {}, {
560 ref: setPopperEl,
561 style: _objectSpread(_objectSpread({}, styles.popper), {}, {
562 zIndex: 1
563 }),
564 children: /*#__PURE__*/jsxRuntime.jsx(CalendarSingle, {
565 defaultMonth: value || initialMonth,
566 disabled: disabledCalendarDays,
567 initialFocus: true,
568 numberOfMonths: 1,
569 onSelect: onSelect,
570 selected: value
571 })
572 }))]
573 });
574});
575
576exports.DatePicker = DatePicker;