@atlaskit/datetime-picker
Version:
A date time picker allows the user to select an associated date and time.
575 lines (566 loc) • 24 kB
JavaScript
/* 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;