UNPKG

@atlaskit/datetime-picker

Version:

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

346 lines (341 loc) 14 kB
import _extends from "@babel/runtime/helpers/extends"; import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; var _excluded = ["styles"]; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } import React, { forwardRef, useCallback, useEffect, useReducer, useState } from 'react'; // oxlint-disable-next-line @atlassian/no-restricted-imports import { format, isValid } from 'date-fns'; import { usePlatformLeafEventHandler } from '@atlaskit/analytics-next/usePlatformLeafEventHandler'; import __noop from '@atlaskit/ds-lib/noop'; import { createLocalizationProvider } from '@atlaskit/locale/localization-provider'; import { fg } from '@atlaskit/platform-feature-flags/fg'; import Select from '@atlaskit/select/default'; import CreatableSelect from '@atlaskit/select/creatable-select'; import { mergeStyles } from '@atlaskit/react-select/styles'; import { defaultTimes } from '../internal/default-times'; import { EmptyComponent } from '../internal/empty-component'; import { FixedLayerMenu } from '../internal/fixed-layer-menu'; import { FixedLayerMenuTopLayer } from '../internal/fixed-layer-menu-top-layer'; import parseTime from '../internal/parse-time'; import { convertTokens } from '../internal/parse-tokens'; import { placeholderDatetime } from '../internal/placeholder-date-time'; import { makeSingleValue } from '../internal/single-value'; var packageName = "@atlaskit/datetime-picker"; var packageVersion = "18.7.2"; var defaultTimeFormat = 'h:mma'; var menuStyles = { /* Need to remove default absolute positioning as that causes issues with position fixed */ position: 'static', /* Need to add overflow to the element with max-height, otherwise causes overflow issues in IE11 */ overflowY: 'auto', /* React-Popper has already offset the menu so we need to reset the margin, otherwise the offset value is doubled */ margin: 0 }; var analyticsAttributes = { componentName: 'timePicker', packageName: packageName, packageVersion: packageVersion }; /** * __Time picker__ * * A time picker allows the user to select a specific time. * * - [Examples](https://atlassian.design/components/datetime-picker/time-picker/examples) * - [Code](https://atlassian.design/components/datetime-picker/time-picker/code) * - [Usage](https://atlassian.design/components/datetime-picker/time-picker/usage) */ var TimePicker = /*#__PURE__*/forwardRef(function (_ref2, _ref) { var ariaDescribedBy = _ref2['aria-describedby'], _ref2$appearance = _ref2.appearance, appearance = _ref2$appearance === void 0 ? 'default' : _ref2$appearance, _ref2$autoFocus = _ref2.autoFocus, autoFocus = _ref2$autoFocus === void 0 ? false : _ref2$autoFocus, _ref2$clearControlLab = _ref2.clearControlLabel, clearControlLabel = _ref2$clearControlLab === void 0 ? 'clear timepicker' : _ref2$clearControlLab, _ref2$defaultIsOpen = _ref2.defaultIsOpen, defaultIsOpen = _ref2$defaultIsOpen === void 0 ? false : _ref2$defaultIsOpen, _ref2$defaultValue = _ref2.defaultValue, defaultValue = _ref2$defaultValue === void 0 ? '' : _ref2$defaultValue, formatDisplayLabel = _ref2.formatDisplayLabel, _ref2$hideIcon = _ref2.hideIcon, hideIcon = _ref2$hideIcon === void 0 ? false : _ref2$hideIcon, _ref2$id = _ref2.id, id = _ref2$id === void 0 ? '' : _ref2$id, _ref2$innerProps = _ref2.innerProps, innerProps = _ref2$innerProps === void 0 ? {} : _ref2$innerProps, _ref2$isDisabled = _ref2.isDisabled, isDisabled = _ref2$isDisabled === void 0 ? false : _ref2$isDisabled, _ref2$isInvalid = _ref2.isInvalid, isInvalid = _ref2$isInvalid === void 0 ? false : _ref2$isInvalid, _ref2$isRequired = _ref2.isRequired, isRequired = _ref2$isRequired === void 0 ? false : _ref2$isRequired, providedIsOpen = _ref2.isOpen, _ref2$label = _ref2.label, label = _ref2$label === void 0 ? '' : _ref2$label, _ref2$locale = _ref2.locale, locale = _ref2$locale === void 0 ? 'en-US' : _ref2$locale, _ref2$name = _ref2.name, name = _ref2$name === void 0 ? '' : _ref2$name, _ref2$onBlur = _ref2.onBlur, providedOnBlur = _ref2$onBlur === void 0 ? __noop : _ref2$onBlur, _ref2$onChange = _ref2.onChange, providedOnChange = _ref2$onChange === void 0 ? __noop : _ref2$onChange, _ref2$onFocus = _ref2.onFocus, providedOnFocus = _ref2$onFocus === void 0 ? __noop : _ref2$onFocus, _ref2$parseInputValue = _ref2.parseInputValue, parseInputValue = _ref2$parseInputValue === void 0 ? function (time, _timeFormat) { return parseTime(time); } : _ref2$parseInputValue, placeholder = _ref2.placeholder, _ref2$selectProps = _ref2.selectProps, selectProps = _ref2$selectProps === void 0 ? {} : _ref2$selectProps, _ref2$spacing = _ref2.spacing, spacing = _ref2$spacing === void 0 ? 'default' : _ref2$spacing, testId = _ref2.testId, timeFormat = _ref2.timeFormat, _ref2$timeIsEditable = _ref2.timeIsEditable, timeIsEditable = _ref2$timeIsEditable === void 0 ? false : _ref2$timeIsEditable, _ref2$times = _ref2.times, times = _ref2$times === void 0 ? defaultTimes : _ref2$times, providedValue = _ref2.value; var _useState = useState(null), _useState2 = _slicedToArray(_useState, 2), containerRef = _useState2[0], setContainerRef = _useState2[1]; /** * When being cleared from the icon the TimePicker is blurred. * This variable defines whether the default onMenuOpen or onMenuClose * events should behave as normal */ var _useState3 = useState(false), _useState4 = _slicedToArray(_useState3, 2), clearingFromIcon = _useState4[0], setClearingFromIcon = _useState4[1]; // TODO: Remove isFocused? Does it do anything? var _useState5 = useState(false), _useState6 = _slicedToArray(_useState5, 2), _ = _useState6[0], setIsFocused = _useState6[1]; var _useState7 = useState(defaultIsOpen), _useState8 = _slicedToArray(_useState7, 2), isOpen = _useState8[0], setIsOpen = _useState8[1]; var _useState9 = useState(defaultValue), _useState0 = _slicedToArray(_useState9, 2), value = _useState0[0], setValue = _useState0[1]; // Hack to force update: https://legacy.reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate var _useReducer = useReducer(function (x) { return x + 1; }, 0), _useReducer2 = _slicedToArray(_useReducer, 2), forceUpdate = _useReducer2[1]; var providedOnChangeWithAnalytics = usePlatformLeafEventHandler(_objectSpread({ fn: providedOnChange, action: 'selectedTime' }, analyticsAttributes)); useEffect(function () { if (providedValue) { setValue(providedValue); } }, [providedValue]); useEffect(function () { if (providedIsOpen) { setIsOpen(providedIsOpen); } }, [providedIsOpen]); var onChange = useCallback(function (newValue, action) { var rawValue = newValue ? newValue.value || newValue : ''; var finalValue = rawValue.toString(); setValue(finalValue); if (action && action.action === 'clear') { setClearingFromIcon(true); } providedOnChangeWithAnalytics(finalValue); }, [providedOnChangeWithAnalytics]); /** * Only allow custom times if timeIsEditable prop is true */ var onCreateOption = function onCreateOption(inputValue) { if (timeIsEditable) { var sanitizedInput; try { sanitizedInput = parseInputValue(inputValue, timeFormat || defaultTimeFormat); } catch (_unused) { return; // do nothing, the main validation should happen in the form } var includesSeconds = !!(timeFormat && /[:.]?(s|ss)/.test(timeFormat)); var formatFormat = includesSeconds ? 'HH:mm:ss' : 'HH:mm'; var formattedValue = format(sanitizedInput, formatFormat) || ''; setValue(formattedValue); providedOnChangeWithAnalytics(formattedValue); } else { providedOnChangeWithAnalytics(inputValue); } }; var onMenuOpen = function onMenuOpen() { // Don't open menu after the user has clicked clear if (clearingFromIcon) { setClearingFromIcon(false); } else { setIsOpen(true); } }; var onMenuClose = function onMenuClose() { // Don't close menu after the user has clicked clear if (clearingFromIcon) { setClearingFromIcon(false); } else { setIsOpen(false); } }; var setInternalContainerRef = function setInternalContainerRef(ref) { var oldRef = containerRef; setContainerRef(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(); } }; var onBlur = function onBlur(event) { setIsFocused(false); providedOnBlur(event); }; var onFocus = function onFocus(event) { setIsFocused(true); providedOnFocus(event); }; var onSelectKeyDown = function onSelectKeyDown(event) { var key = event.key; var keyPressed = key.toLowerCase(); if (clearingFromIcon && (keyPressed === 'backspace' || keyPressed === 'delete')) { // If being cleared from keyboard, don't change behaviour setClearingFromIcon(false); } }; var ICON_PADDING = 2; var GRID_SIZE = 8; var l10n = createLocalizationProvider(locale); var _selectProps$styles = selectProps.styles, selectStyles = _selectProps$styles === void 0 ? {} : _selectProps$styles, otherSelectProps = _objectWithoutProperties(selectProps, _excluded); var SelectComponent = timeIsEditable ? CreatableSelect : Select; /** * There are multiple props that can change how the time is formatted. * The priority of props used is: * 1. formatDisplayLabel * 2. timeFormat * 3. locale */ var formatTime = function formatTime(time) { if (formatDisplayLabel) { return formatDisplayLabel(time, timeFormat || defaultTimeFormat); } var date = parseTime(time); if (!(date instanceof Date)) { return ''; } if (!isValid(date)) { return time; } if (timeFormat) { return format(date, convertTokens(timeFormat)); } return l10n.formatTime(date); }; var options = times.map(function (time) { return { label: formatTime(time), value: time }; }); var initialValue; if (providedValue !== null && providedValue !== undefined && providedValue !== '') { initialValue = { label: formatTime(providedValue), value: providedValue }; } else if (providedValue !== '' && value) { initialValue = { label: formatTime(value), value: value }; } else { initialValue = null; } var SingleValue = makeSingleValue({ lang: locale }); var selectComponents = _objectSpread({ DropdownIndicator: EmptyComponent, Menu: fg('platform-dst-top-layer') ? FixedLayerMenuTopLayer : FixedLayerMenu, SingleValue: SingleValue }, hideIcon && { ClearIndicator: EmptyComponent }); var renderIconContainer = Boolean(!hideIcon && value); var mergedStyles = mergeStyles(selectStyles, { control: function control(base) { return _objectSpread({}, base); }, menu: function menu(base) { return _objectSpread(_objectSpread(_objectSpread({}, base), menuStyles), {}, { // Fixed positioned elements no longer inherit width from their parent, so we must explicitly set the // menu width to the width of our container width: containerRef ? containerRef.getBoundingClientRect().width : 'auto' }); }, indicatorsContainer: function indicatorsContainer(base) { return _objectSpread(_objectSpread({}, base), {}, { paddingLeft: renderIconContainer ? ICON_PADDING : 0, paddingRight: renderIconContainer ? GRID_SIZE - ICON_PADDING : 0 }); } }); return /*#__PURE__*/React.createElement("div", _extends({}, innerProps, { ref: setInternalContainerRef, "data-testid": testId && "".concat(testId, "--container") }), /*#__PURE__*/React.createElement("input", { name: name, type: "hidden", value: value, "data-testid": testId && "".concat(testId, "--input"), onKeyDown: onSelectKeyDown }), /*#__PURE__*/React.createElement(SelectComponent, _extends({ "aria-describedby": ariaDescribedBy, "aria-label": label || undefined, appearance: appearance, autoFocus: autoFocus, clearControlLabel: clearControlLabel, components: selectComponents, inputId: id, isClearable: true, isDisabled: isDisabled, isRequired: isRequired, menuIsOpen: isOpen && !isDisabled, menuPlacement: "auto", openMenuOnFocus: true, onBlur: onBlur, onCreateOption: onCreateOption, onChange: onChange, options: options, onFocus: onFocus, onMenuOpen: onMenuOpen, onMenuClose: onMenuClose, placeholder: placeholder || l10n.formatTime(placeholderDatetime), styles: mergedStyles, value: initialValue, spacing: spacing // We need this to get things to work, even though it's not supported. , fixedLayerRef: containerRef, isInvalid: isInvalid, testId: testId }, otherSelectProps))); }); export default TimePicker;