UNPKG

@atlaskit/datetime-picker

Version:

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

378 lines (372 loc) • 13.5 kB
/* time-picker.tsx generated by @compiled/babel-plugin v3.0.2 */ import _extends from "@babel/runtime/helpers/extends"; import "./time-picker.compiled.css"; import { ax, ix } from "@compiled/react/runtime"; import React, { forwardRef, useCallback, useEffect, useId, useMemo, 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 IconButton from '@atlaskit/button/icon/button'; import __noop from '@atlaskit/ds-lib/noop'; import ClockIcon from '@atlaskit/icon/core/clock'; import { getTimeFormat } from '@atlaskit/locale/get-time-format'; import { createLocalizationProvider } from '@atlaskit/locale/localization-provider'; import { fg } from '@atlaskit/platform-feature-flags/fg'; import { mergeStyles } from '@atlaskit/react-select/styles'; import CreatableSelect from '@atlaskit/select/creatable-select'; import Select from '@atlaskit/select/default'; import { defaultTimes } from '../internal/default-times'; import { EmptyComponent } from '../internal/empty-component'; import { FixedLayerMenu } from '../internal/fixed-layer-menu'; import parseTime from '../internal/parse-time'; import { convertTokens } from '../internal/parse-tokens'; import { PickerButtonContainer } from '../internal/picker-button-container'; import { placeholderDatetime } from '../internal/placeholder-date-time'; import { makeSingleValue } from '../internal/single-value'; const packageName = "@atlaskit/datetime-picker"; const packageVersion = "19.4.0"; const defaultTimeFormat = 'h:mma'; const 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 }; const styles = { pickerContainer: "_kqswh2mm" }; const analyticsAttributes = { componentName: 'timePicker', packageName, 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) */ const TimePicker = /*#__PURE__*/forwardRef(({ 'aria-describedby': ariaDescribedBy, appearance = 'default', autoFocus = false, clearControlLabel = 'clear timepicker', defaultIsOpen = false, defaultValue = '', formatDisplayLabel, hideIcon = false, id = '', innerProps = {}, isDisabled = false, isInvalid = false, isRequired = false, isOpen: providedIsOpen, label = '', locale = 'en-US', name = '', openTimeLabel = 'Open time picker', onBlur: providedOnBlur = __noop, onChange: providedOnChange = __noop, onFocus: providedOnFocus = __noop, parseInputValue, placeholder, selectProps = {}, shouldShowTimeButton = false, spacing = 'default', testId, timeFormat, timeIsEditable = false, times = defaultTimes, value: providedValue }, _ref) => { const [containerRef, setContainerRef] = useState(null); /** * When being cleared from the icon the TimePicker is blurred. * This variable defines whether the default onMenuOpen or onMenuClose * events should behave as normal */ const [clearingFromIcon, setClearingFromIcon] = useState(false); // TODO: Remove isFocused? Does it do anything? const [_, setIsFocused] = useState(false); const [isOpen, setIsOpen] = useState(defaultIsOpen); const [shouldFocusTimeInput, setShouldFocusTimeInput] = useState(false); const [value, setValue] = useState(defaultValue); const l10n = createLocalizationProvider(locale); const localeTimeFormat = useMemo(() => fg('platform-dst-locale-aware-time-parsing') ? getTimeFormat(locale) : undefined, [locale]); // Hack to force update: https://legacy.reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate const [, forceUpdate] = useReducer(x => x + 1, 0); const providedOnChangeWithAnalytics = usePlatformLeafEventHandler({ fn: providedOnChange, action: 'selectedTime', ...analyticsAttributes }); useEffect(() => { if (providedValue) { setValue(providedValue); } }, [providedValue]); useEffect(() => { if (providedIsOpen) { setIsOpen(providedIsOpen); } }, [providedIsOpen]); useEffect(() => { if (!isOpen || !shouldFocusTimeInput) { return; } const innerCombobox = containerRef === null || containerRef === void 0 ? void 0 : containerRef.querySelector('[role="combobox"]'); innerCombobox === null || innerCombobox === void 0 ? void 0 : innerCombobox.focus(); setShouldFocusTimeInput(false); }, [containerRef, isOpen, shouldFocusTimeInput]); const onChange = useCallback((newValue, action) => { const rawValue = newValue ? newValue.value || newValue : ''; const finalValue = rawValue.toString(); setValue(finalValue); if (action && action.action === 'clear') { setClearingFromIcon(true); } providedOnChangeWithAnalytics(finalValue); }, [providedOnChangeWithAnalytics]); /** * Only allow custom times if timeIsEditable prop is true */ const onCreateOption = inputValue => { if (timeIsEditable) { let sanitizedInput; try { sanitizedInput = parseInputValue ? parseInputValue(inputValue, timeFormat || defaultTimeFormat) : parseTime(inputValue, localeTimeFormat); } catch { return; // do nothing, the main validation should happen in the form } const includesSeconds = !!(timeFormat && /[:.]?(s|ss)/.test(timeFormat)); const formatFormat = includesSeconds ? 'HH:mm:ss' : 'HH:mm'; const formattedValue = format(sanitizedInput, formatFormat) || ''; setValue(formattedValue); providedOnChangeWithAnalytics(formattedValue); } else { providedOnChangeWithAnalytics(inputValue); } }; const onMenuOpen = () => { // Don't open menu after the user has clicked clear if (clearingFromIcon) { setClearingFromIcon(false); } else { setIsOpen(true); } }; const onMenuClose = () => { // Don't close menu after the user has clicked clear if (clearingFromIcon) { setClearingFromIcon(false); } else { setIsOpen(false); } }; const setInternalContainerRef = ref => { const 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(); } }; const onBlur = event => { setIsFocused(false); providedOnBlur(event); }; const onFocus = event => { setIsFocused(true); providedOnFocus(event); }; const onSelectKeyDown = event => { const { key } = event; const keyPressed = key.toLowerCase(); if (clearingFromIcon && (keyPressed === 'backspace' || keyPressed === 'delete')) { // If being cleared from keyboard, don't change behaviour setClearingFromIcon(false); } }; const onTimeButtonClick = event => { const nextIsOpen = !isOpen; setIsOpen(nextIsOpen); if (nextIsOpen) { var _otherSelectProps$onM; (_otherSelectProps$onM = otherSelectProps.onMenuOpen) === null || _otherSelectProps$onM === void 0 ? void 0 : _otherSelectProps$onM.call(otherSelectProps); } else { var _otherSelectProps$onM2; (_otherSelectProps$onM2 = otherSelectProps.onMenuClose) === null || _otherSelectProps$onM2 === void 0 ? void 0 : _otherSelectProps$onM2.call(otherSelectProps); } event.stopPropagation(); }; const onTimeButtonKeyDown = event => { if (!isOpen && (event.key === ' ' || event.key === 'Enter')) { setShouldFocusTimeInput(true); } }; const ICON_PADDING = 2; const GRID_SIZE = 8; const { styles: selectStyles = {}, ...otherSelectProps } = selectProps; const SelectComponent = timeIsEditable ? CreatableSelect : Select; const onInputChange = (inputValue, actionMeta) => { var _otherSelectProps$onI; (_otherSelectProps$onI = otherSelectProps.onInputChange) === null || _otherSelectProps$onI === void 0 ? void 0 : _otherSelectProps$onI.call(otherSelectProps, inputValue, actionMeta); // Keep the menu-opening behavior aligned with DatePicker: entering text // opens the available options, even when the optional time button disables // opening on focus. if (actionMeta.action === 'input-change') { setIsOpen(true); } }; /** * There are multiple props that can change how the time is formatted. * The priority of props used is: * 1. formatDisplayLabel * 2. timeFormat * 3. locale */ const formatTime = time => { if (formatDisplayLabel) { return formatDisplayLabel(time, timeFormat || defaultTimeFormat); } const date = parseTime(time); if (!(date instanceof Date)) { return ''; } if (!isValid(date)) { return time; } if (timeFormat) { return format(date, convertTokens(timeFormat)); } return l10n.formatTime(date); }; const options = times.map(time => { return { label: formatTime(time), value: time }; }); let 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; } const valueId = useId(); const SingleValue = makeSingleValue({ id: valueId, lang: locale }); const selectComponents = { DropdownIndicator: EmptyComponent, // Select owns top-layer positioning; only the legacy path needs a separate wrapper. ...(!fg('platform-dst-top-layer') && { Menu: FixedLayerMenu }), SingleValue, ...(hideIcon && { ClearIndicator: EmptyComponent }) }; const renderIconContainer = Boolean(!hideIcon && value); const fullOpenTimeLabel = label ? `${label}, ${openTimeLabel}` : openTimeLabel; const isInputMotionEnabled = fg('platform-dst-motion-uplift-input'); const mergedStyles = mergeStyles(selectStyles, { control: base => { var _base$transition; return { ...base, ...(isInputMotionEnabled && !isDisabled ? { transition: (_base$transition = base.transition) !== null && _base$transition !== void 0 ? _base$transition : "var(--ds-input, background-color border-color box-shadow 150ms cubic-bezier(0.4, 1, 0.6, 1))" } : {}) }; }, menu: base => ({ ...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: base => ({ ...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 && `${testId}--container`, className: ax([styles.pickerContainer]) }), /*#__PURE__*/React.createElement("input", { name: name, type: "hidden", value: value, "data-testid": testId && `${testId}--input`, onKeyDown: onSelectKeyDown }), /*#__PURE__*/React.createElement(SelectComponent, _extends({ "aria-describedby": ariaDescribedBy ? `${ariaDescribedBy} ${valueId}` : valueId, "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: !shouldShowTimeButton, 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, { onInputChange: onInputChange })), shouldShowTimeButton && !isDisabled ? /*#__PURE__*/React.createElement(PickerButtonContainer, { hasClearIndicator: renderIconContainer }, /*#__PURE__*/React.createElement(IconButton, { appearance: "subtle", label: fullOpenTimeLabel, icon: iconProps => /*#__PURE__*/React.createElement(ClockIcon, _extends({}, iconProps, { color: "var(--ds-icon, #292A2E)" })), onClick: onTimeButtonClick, onKeyDown: onTimeButtonKeyDown, testId: testId && `${testId}--open-time-button` })) : null); }); export default TimePicker;