UNPKG

@atlaskit/datetime-picker

Version:

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

302 lines (297 loc) 9.92 kB
import _extends from "@babel/runtime/helpers/extends"; 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'; const packageName = "@atlaskit/datetime-picker"; const packageVersion = "18.7.2"; 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 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 = '', onBlur: providedOnBlur = __noop, onChange: providedOnChange = __noop, onFocus: providedOnFocus = __noop, parseInputValue = (time, _timeFormat) => parseTime(time), placeholder, selectProps = {}, 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 [value, setValue] = useState(defaultValue); // 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]); 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(inputValue, timeFormat || defaultTimeFormat); } 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 ICON_PADDING = 2; const GRID_SIZE = 8; const l10n = createLocalizationProvider(locale); const { styles: selectStyles = {}, ...otherSelectProps } = selectProps; const 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 */ 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 SingleValue = makeSingleValue({ lang: locale }); const selectComponents = { DropdownIndicator: EmptyComponent, Menu: fg('platform-dst-top-layer') ? FixedLayerMenuTopLayer : FixedLayerMenu, SingleValue, ...(hideIcon && { ClearIndicator: EmptyComponent }) }; const renderIconContainer = Boolean(!hideIcon && value); const mergedStyles = mergeStyles(selectStyles, { control: base => ({ ...base }), 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` }), /*#__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, "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;