UNPKG

@atlaskit/datetime-picker

Version:

A date time picker allows the user to select an associated date and time.

575 lines (566 loc) 24 kB
/* date-picker.tsx generated by @compiled/babel-plugin v3.0.2 */ import _extends from "@babel/runtime/helpers/extends"; import "./date-picker.compiled.css"; import * as React from 'react'; import { ax, ix } from "@compiled/react/runtime"; /// <reference types="node" /> // for typing `process` import { forwardRef, useCallback, useEffect, useReducer, useRef, useState } from 'react'; // oxlint-disable-next-line @atlassian/no-restricted-imports import { isValid, parseISO } from 'date-fns'; import { usePlatformLeafEventHandler } from '@atlaskit/analytics-next/usePlatformLeafEventHandler'; import IconButton from '@atlaskit/button/icon/button'; import { cx } from '@atlaskit/css'; import { useId } from '@atlaskit/ds-lib/use-id'; import CalendarIcon from '@atlaskit/icon/core/calendar'; import { createLocalizationProvider } from '@atlaskit/locale/localization-provider'; import { fg } from '@atlaskit/platform-feature-flags/fg'; import { Box } from '@atlaskit/primitives/compiled'; import Select from '@atlaskit/select/default'; import { mergeStyles } from '@atlaskit/react-select/styles'; import { EmptyComponent } from '../internal/empty-component'; import { formatDate } from '../internal/format-date'; import { getParsedISO } from '../internal/get-parsed-iso'; import { getPlaceholder } from '../internal/get-placeholder'; import { getSafeCalendarValue } from '../internal/get-safe-calendar-value'; import { getShortISOString } from '../internal/get-short-iso-string'; import { IndicatorsContainer } from '../internal/indicators-container'; import { isDateDisabled } from '../internal/is-date-disabled'; import { Menu } from '../internal/menu'; import { MenuTopLayer } from '../internal/menu-top-layer'; import { parseDate } from '../internal/parse-date'; import { makeSingleValue } from '../internal/single-value'; const packageName = "@atlaskit/datetime-picker"; const packageVersion = "18.7.2"; const styles = { pickerContainerStyle: "_kqswh2mm", dropdownIndicatorStyles: "_1ul91k8s _1tke1k8s _1e0c1txw _4cvr1h6o _1bah1h6o", iconContainerStyles: "_v564vrg3 _1e0c1txw _4t3i1osq _kqswstnw _4cvr1h6o _i0dl1kw7 _syaz1rpy _152tze3t _rjxpze3t _30l3azsu", iconSpacingWithClearButtonStyles: "_12l2xy5q", iconSpacingWithoutClearButtonStyles: "_12l21b66" }; const analyticsAttributes = { componentName: 'datePicker', packageName, packageVersion }; /** * __Date picker__ * * A date picker allows the user to select a particular date. * * - [Examples](https://atlassian.design/components/datetime-picker/date-picker/examples) * - [Code](https://atlassian.design/components/datetime-picker/date-picker/code) * - [Usage](https://atlassian.design/components/datetime-picker/date-picker/usage) */ const DatePicker = /*#__PURE__*/forwardRef((props, _forwardedRef) => { const containerRef = useRef(null); const calendarRef = useRef(null); const calendarButtonRef = useRef(null); // Track whether focus was caused by an active pointer gesture. Pointer-driven focus must defer // opening a top-layer popover until click, while keyboard and programmatic focus can open it now. const isPointerInteractionRef = useRef(false); const { appearance = 'default', autoFocus = false, clearControlLabel = 'Clear', hideIcon = false, openCalendarLabel = 'Open calendar', defaultIsOpen = false, defaultValue = '', disabled = [], disabledDateFilter = _ => false, icon: Icon = CalendarIcon, id = '', innerProps = {}, inputLabel = 'Date picker', inputLabelId, isDisabled = false, isInvalid = false, isRequired = false, label = '', name = '', /* eslint-disable @repo/internal/react/use-noop */ onBlur = _event => {}, onChange: onChangeProp = _value => {}, onFocus = _event => {}, /* eslint-enable @repo/internal/react/use-noop */ selectProps = {}, shouldShowCalendarButton, spacing = 'default', locale: propLocale = 'en-US', value: propValue, isOpen: isOpenProp, maxDate, minDate, weekStartDay, formatDisplayLabel, testId, 'aria-describedby': ariaDescribedBy, placeholder, nextMonthLabel, previousMonthLabel, ...rest } = props; const [isOpen, setIsOpen] = useState(defaultIsOpen); const [_, setIsFocused] = useState(false); const [clearingFromIcon, setClearingFromIcon] = useState(false); const [selectInputValue, setSelectInputValue] = useState(selectProps.inputValue); const [value, setValue] = useState(propValue || defaultValue); const [calendarValue, setCalendarValue] = useState(propValue || defaultValue || getShortISOString(new Date())); const [l10n, setL10n] = useState(createLocalizationProvider(propLocale)); const [locale, setLocale] = useState(propLocale); const [shouldSetFocusOnCurrentDay, setShouldSetFocusOnCurrentDay] = useState(false); const [isKeyDown, setIsKeyDown] = useState(false); const [wasOpenedFromCalendarButton, setWasOpenedFromCalendarButton] = useState(false); // Hack to force update: https://legacy.reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate const [, forceUpdate] = useReducer(x => !x, true); const onChangePropWithAnalytics = usePlatformLeafEventHandler({ fn: onChangeProp, action: 'selectedDate', actionSubject: 'datePicker', ...analyticsAttributes }); if (propLocale !== locale) { setL10n(createLocalizationProvider(propLocale)); setLocale(propLocale); } useEffect(() => { // Only move focus when the dedicated calendar button opened the menu. if (!isKeyDown) { return; } if (isOpen && wasOpenedFromCalendarButton) { var _calendarRef$current, _calendarRef$current$; setIsKeyDown(false); // Focus on the first button within the calendar calendarRef === null || calendarRef === void 0 ? void 0 : (_calendarRef$current = calendarRef.current) === null || _calendarRef$current === void 0 ? void 0 : (_calendarRef$current$ = _calendarRef$current.querySelector('button')) === null || _calendarRef$current$ === void 0 ? void 0 : _calendarRef$current$.focus(); } }, [isKeyDown, calendarRef, isOpen, wasOpenedFromCalendarButton]); const getValue = () => propValue !== null && propValue !== void 0 ? propValue : value; const getIsOpen = () => isOpenProp !== null && isOpenProp !== void 0 ? isOpenProp : isOpen; const onCalendarChange = ({ iso }) => { setCalendarValue(getParsedISO({ iso })); }; const onCalendarSelect = ({ iso }) => { setSelectInputValue(''); setIsOpen(false); setCalendarValue(iso); setValue(iso); setWasOpenedFromCalendarButton(false); onChangePropWithAnalytics(iso); // When using top-layer, PopupContent handles focus restoration automatically // on close based on the role. Only manually restore focus for the legacy path. if (!fg('platform-dst-top-layer')) { if (wasOpenedFromCalendarButton) { var _calendarButtonRef$cu; (_calendarButtonRef$cu = calendarButtonRef.current) === null || _calendarButtonRef$cu === void 0 ? void 0 : _calendarButtonRef$cu.focus(); } else { var _containerRef$current; const innerCombobox = containerRef === null || containerRef === void 0 ? void 0 : (_containerRef$current = containerRef.current) === null || _containerRef$current === void 0 ? void 0 : _containerRef$current.querySelector('[role="combobox"]'); innerCombobox === null || innerCombobox === void 0 ? void 0 : innerCombobox.focus(); } } setIsOpen(false); }; const onInputClick = () => { if (!isDisabled && !getIsOpen()) { setIsOpen(true); setWasOpenedFromCalendarButton(false); } }; const onContainerBlur = event => { var _containerRef$current2; const newlyFocusedElement = event.relatedTarget; isPointerInteractionRef.current = false; if (!(containerRef !== null && containerRef !== void 0 && (_containerRef$current2 = containerRef.current) !== null && _containerRef$current2 !== void 0 && _containerRef$current2.contains(newlyFocusedElement))) { setIsOpen(false); setShouldSetFocusOnCurrentDay(false); setWasOpenedFromCalendarButton(false); onBlur(event); } }; const onContainerFocus = () => { setShouldSetFocusOnCurrentDay(false); }; const onSelectBlur = event => { var _containerRef$current3; const newlyFocusedElement = event.relatedTarget; if (clearingFromIcon) { // Don't close menu if blurring after the user has clicked clear setClearingFromIcon(false); } else if (!(containerRef !== null && containerRef !== void 0 && (_containerRef$current3 = containerRef.current) !== null && _containerRef$current3 !== void 0 && _containerRef$current3.contains(newlyFocusedElement))) { // Don't close menu if focus is staying within the date picker's // container. Makes keyboard accessibility of calendar possible setIsOpen(false); setIsFocused(false); setWasOpenedFromCalendarButton(false); } }; const onSelectFocus = event => { const value = getValue(); if (clearingFromIcon) { // Don't open menu if focussing after the user has clicked clear setClearingFromIcon(false); } else { // Opening an auto popover during pointer focus allows the matching pointerup // to immediately light-dismiss it. Let the subsequent click open it instead. if (!fg('platform-dst-top-layer') || !isPointerInteractionRef.current) { // Don't open when focused into via keyboard if the calendar button is present setIsOpen(!shouldShowCalendarButton); } setCalendarValue(value); setIsFocused(true); setWasOpenedFromCalendarButton(false); } onFocus(event); }; const onTextInput = event => { const inputValue = event.target.value; if (inputValue) { const parsed = parseDate(inputValue, { parseInputValue: rest === null || rest === void 0 ? void 0 : rest.parseInputValue, dateFormat: rest === null || rest === void 0 ? void 0 : rest.dateFormat, l10n: l10n }); // Only try to set the date if we have month & day if (parsed && isValid(parsed)) { // We format the parsed date to YYYY-MM-DD here because // this is the format expected by the @atlaskit/calendar component setCalendarValue(getShortISOString(parsed)); } } setIsOpen(true); setWasOpenedFromCalendarButton(false); }; const onInputKeyDown = event => { const value = getValue(); const keyPressed = event.key.toLowerCase(); // If the input is focused and the calendar is not visible, handle space and enter clicks if (!isOpen && (keyPressed === 'enter' || keyPressed === ' ')) { setIsOpen(true); setWasOpenedFromCalendarButton(false); } switch (keyPressed) { case 'escape': // Restore focus because it may have moved from the input into the calendar. if (wasOpenedFromCalendarButton) { var _calendarButtonRef$cu2; (_calendarButtonRef$cu2 = calendarButtonRef.current) === null || _calendarButtonRef$cu2 === void 0 ? void 0 : _calendarButtonRef$cu2.focus(); } else { var _containerRef$current4; const innerCombobox = containerRef === null || containerRef === void 0 ? void 0 : (_containerRef$current4 = containerRef.current) === null || _containerRef$current4 === void 0 ? void 0 : _containerRef$current4.querySelector('[role="combobox"]'); innerCombobox === null || innerCombobox === void 0 ? void 0 : innerCombobox.focus(); } setIsOpen(false); setShouldSetFocusOnCurrentDay(false); setWasOpenedFromCalendarButton(false); break; case 'backspace': case 'delete': { const inputCount = 0; if (value && event.target instanceof HTMLInputElement && event.target.value.length <= inputCount) { // If being cleared from keyboard, don't change behaviour setClearingFromIcon(false); setValue(''); } break; } case 'enter': if (!isOpen) { return; } // Prevent form submission when a date is selected // using enter. See https://product-fabric.atlassian.net/browse/DSP-2501 // for more details. event.preventDefault(); if (!isDateDisabled(calendarValue, { disabled })) { // Get a safe `calendarValue` in case the value exceeds the maximum // allowed by ISO 8601 const safeCalendarValue = getSafeCalendarValue(calendarValue); const valueChanged = safeCalendarValue !== value; setSelectInputValue(''); setIsOpen(false); setValue(safeCalendarValue); setCalendarValue(safeCalendarValue); setWasOpenedFromCalendarButton(wasOpenedFromCalendarButton); if (valueChanged) { onChangePropWithAnalytics(safeCalendarValue); } } break; case 'arrowdown': case 'arrowup': if (!shouldSetFocusOnCurrentDay) { setIsOpen(true); setShouldSetFocusOnCurrentDay(true); } break; default: break; } }; const onCalendarButtonKeyDown = e => { // Don't allow an arrow up or down to open the menu, since the input key // down handler is actually on the parent. if (e.type === 'keydown' && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { e.stopPropagation(); } // We want to stop this from triggering other keydown events, particularly // for space and enter presses. Otherwise, it opens and then closes // immediately. if (e.type === 'keydown' && (e.key === ' ' || e.key === 'Enter')) { e.stopPropagation(); setIsKeyDown(true); setWasOpenedFromCalendarButton(true); } }; // This event handler is triggered from both keydown and click. It's weird. const onCalendarButtonClick = e => { if (!getIsOpen() && fg('platform-dst-top-layer')) { setIsKeyDown(true); } setIsOpen(isOpen => { if (isOpen) { var _props$selectProps, _props$selectProps$on; (_props$selectProps = props.selectProps) === null || _props$selectProps === void 0 ? void 0 : (_props$selectProps$on = _props$selectProps.onMenuClose) === null || _props$selectProps$on === void 0 ? void 0 : _props$selectProps$on.call(_props$selectProps); return false; } else { var _props$selectProps2, _props$selectProps2$o; (_props$selectProps2 = props.selectProps) === null || _props$selectProps2 === void 0 ? void 0 : (_props$selectProps2$o = _props$selectProps2.onMenuOpen) === null || _props$selectProps2$o === void 0 ? void 0 : _props$selectProps2$o.call(_props$selectProps2); return true; } }); setWasOpenedFromCalendarButton(true); e.stopPropagation(); }; const onClear = () => { setValue(''); setCalendarValue(defaultValue || getShortISOString(new Date())); if (!hideIcon) { setClearingFromIcon(true); } onChangePropWithAnalytics(''); }; // `unknown` is used because `value` is unused so it does not matter. const onSelectChange = (_value, action) => { // Used for native clear event in React Select // Triggered when clicking ClearIndicator or backspace with no value if (action.action === 'clear') { onClear(); } }; const handleSelectInputChange = (selectInputValue, actionMeta) => { if (selectProps.onInputChange) { selectProps.onInputChange(selectInputValue, actionMeta); } setSelectInputValue(selectInputValue); }; const getContainerRef = useCallback(ref => { const oldRef = containerRef.current; containerRef.current = ref; // Cause a re-render if we're getting the container ref for the first time // as the layered menu requires it for dimension calculation if (oldRef == null && ref != null) { forceUpdate(); } }, [containerRef]); const getterValue = getValue(); let actualSelectInputValue; actualSelectInputValue = selectInputValue; const menuIsOpen = getIsOpen() && !isDisabled; const showClearIndicator = Boolean((getterValue || selectInputValue) && !hideIcon); let clearIndicator = props => /*#__PURE__*/React.createElement(Box, { xcss: styles.dropdownIndicatorStyles }, /*#__PURE__*/React.createElement(Icon, props)); const dropDownIcon = appearance === 'subtle' || hideIcon || showClearIndicator ? null : clearIndicator; const valueId = useId(); const SingleValue = makeSingleValue({ id: valueId, lang: propLocale }); const selectComponents = { ...selectProps.components, DropdownIndicator: shouldShowCalendarButton ? EmptyComponent : dropDownIcon, // Only use this new container component if the calendar button is shown. // Otherwise, it throws errors downstream for some reason ...(shouldShowCalendarButton ? { IndicatorsContainer: props => /*#__PURE__*/React.createElement(IndicatorsContainer, _extends({}, props, { showClearIndicator: showClearIndicator })) } : {}), Menu: fg('platform-dst-top-layer') ? MenuTopLayer : Menu, SingleValue, ...(!showClearIndicator && { ClearIndicator: EmptyComponent }) }; const { styles: selectStyles = {} } = selectProps; const disabledStyle = isDisabled ? { pointerEvents: 'none', color: "var(--ds-icon-disabled, #080F214A)" } : {}; const calendarProps = { calendarContainerRef: containerRef.current, calendarDisabled: disabled, calendarDisabledDateFilter: disabledDateFilter, calendarMaxDate: maxDate, calendarMinDate: minDate, calendarRef: calendarRef, calendarValue: getterValue && getShortISOString(parseISO(getterValue)), calendarView: calendarValue, onCalendarChange, onCalendarSelect, calendarLocale: locale, calendarWeekStartDay: weekStartDay !== null && weekStartDay !== void 0 ? weekStartDay : fg('platform-dst-locale-week-start-day') ? l10n.getFirstDayOfWeek() : undefined, shouldSetFocusOnCurrentDay, /** * This overrides the inner wrapper the Calendar. * @private Please use this with extreme caution, this API may be changed in the future. */ menuInnerWrapper: props === null || props === void 0 ? void 0 : props.menuInnerWrapper }; const mergedStyles = mergeStyles(selectStyles, { control: base => ({ ...base, ...disabledStyle }), indicatorsContainer: base => ({ ...base, paddingLeft: "var(--ds-space-025, 2px)", // ICON_PADDING = 2 paddingRight: "var(--ds-space-075, 6px)" // 8 - ICON_PADDING = 6 }) }); const initialValue = getterValue ? { label: formatDate(getterValue, { formatDisplayLabel, dateFormat: rest === null || rest === void 0 ? void 0 : rest.dateFormat, l10n: l10n }), value: getterValue } : null; // `label` takes precedence of the `inputLabel` const fullopenCalendarLabel = label || inputLabel ? `${label || inputLabel}, ${openCalendarLabel}` : openCalendarLabel; const openCalendarLabelId = `open-calendar-label--${useId()}`; return /*#__PURE__*/React.createElement("div", _extends({}, innerProps, { "data-testid": testId && `${testId}--container`, onBlur: onContainerBlur // Reset before descendant click handlers run: click happens after native light-dismiss, so // any focus they move is safe to treat as non-pointer focus. Capture also guarantees cleanup // when a descendant stops the bubbling click. , onClickCapture: () => { isPointerInteractionRef.current = false; }, onFocus: onContainerFocus, onClick: onInputClick, onInput: onTextInput, onKeyDown: onInputKeyDown // A cancelled pointer produces no click, so clear the tracking state here instead. , onPointerCancelCapture: () => { isPointerInteractionRef.current = false; } // Capture pointerdown before it moves focus into a descendant Select control. , onPointerDownCapture: () => { isPointerInteractionRef.current = true; }, ref: getContainerRef // Since the onclick, onfocus are passed down, adding role="presentation" prevents typecheck errors. , role: "presentation", className: ax([styles.pickerContainerStyle]) }), /*#__PURE__*/React.createElement("input", { name: name, type: "hidden", value: getterValue, "data-testid": testId && `${testId}--input` }), /*#__PURE__*/React.createElement(Select, _extends({ appearance: appearance, "aria-describedby": ariaDescribedBy ? `${ariaDescribedBy} ${valueId}` : valueId, label: label || undefined, autoFocus: autoFocus, clearControlLabel: clearControlLabel, closeMenuOnSelect: true, enableAnimation: false, inputId: id, inputValue: actualSelectInputValue, isDisabled: isDisabled, isRequired: isRequired, menuIsOpen: menuIsOpen, onBlur: onSelectBlur, onChange: onSelectChange, onFocus: onSelectFocus, onInputChange: handleSelectInputChange, placeholder: getPlaceholder({ placeholder: placeholder, l10n: l10n }) // @ts-ignore -- Type 'OptionType' is not assignable to type '{ label: string; value: string; }' // eslint-disable-next-line @atlaskit/design-system/no-unsafe-style-overrides , styles: mergedStyles, value: initialValue }, selectProps, { // For some reason, this and the below `styles` type error _only_ show // up when you alter some of the properties in the `selectComponents` // object. These errors are still present, and I suspect have always // been present, without changing the unrelated code. Ignoring as the // component still works as expected despite this error. And also // because the select refresh team may solve it later. components: selectComponents // These are below the spread because I don't know what is in // selectProps or not and what wil be overwritten , isClearable: true, isInvalid: isInvalid, spacing: spacing, testId: testId // These aren't part of `Select`'s API, but we're using them here. , calendarContainerRef: calendarProps.calendarContainerRef, calendarDisabled: calendarProps.calendarDisabled, calendarDisabledDateFilter: calendarProps.calendarDisabledDateFilter, calendarLocale: calendarProps.calendarLocale, calendarMaxDate: calendarProps.calendarMaxDate, calendarMinDate: calendarProps.calendarMinDate, calendarRef: calendarProps.calendarRef, calendarValue: calendarProps.calendarValue, calendarView: calendarProps.calendarView, calendarWeekStartDay: calendarProps.calendarWeekStartDay, nextMonthLabel: nextMonthLabel, onCalendarChange: calendarProps.onCalendarChange, onCalendarSelect: calendarProps.onCalendarSelect, previousMonthLabel: previousMonthLabel, shouldSetFocusOnCurrentDay: calendarProps.shouldSetFocusOnCurrentDay, menuInnerWrapper: calendarProps.menuInnerWrapper })), shouldShowCalendarButton && !isDisabled ? /*#__PURE__*/React.createElement(Box, { xcss: cx(styles.iconContainerStyles, getterValue && !hideIcon ? styles.iconSpacingWithClearButtonStyles : styles.iconSpacingWithoutClearButtonStyles) }, /*#__PURE__*/React.createElement(IconButton, { appearance: "subtle", label: !inputLabelId ? fullopenCalendarLabel : openCalendarLabel, "aria-labelledby": inputLabelId ? `${inputLabelId} ${openCalendarLabelId}` : undefined, id: openCalendarLabelId, icon: iconProps => /*#__PURE__*/React.createElement(CalendarIcon, _extends({}, iconProps, { color: "var(--ds-icon, #292A2E)" })), onClick: onCalendarButtonClick, onKeyDown: onCalendarButtonKeyDown, ref: calendarButtonRef, testId: testId && `${testId}--open-calendar-button` })) : null); }); export default DatePicker;