@atlaskit/datetime-picker
Version:
A date time picker allows the user to select an associated date and time.
486 lines (476 loc) • 19 kB
JavaScript
/* date-time-picker.tsx generated by @compiled/babel-plugin v3.0.2 */
import _extends from "@babel/runtime/helpers/extends";
import "./date-time-picker.compiled.css";
import { ax, ix } from "@compiled/react/runtime";
/// <reference types="node" />
// for typing `process`
import React, { forwardRef, useCallback, useEffect, useReducer, useRef, useState } from 'react';
// oxlint-disable-next-line @atlassian/no-restricted-imports
import { format, isValid, parseISO } from 'date-fns';
import { usePlatformLeafEventHandler } from '@atlaskit/analytics-next/usePlatformLeafEventHandler';
import IconButton from '@atlaskit/button/icon/button';
import SelectClearIcon from '@atlaskit/icon/core/cross-circle';
import { Box, Inline } from '@atlaskit/primitives/compiled';
import { mergeStyles } from '@atlaskit/react-select/styles';
import { DateTimePickerContainer } from '../internal/date-time-picker-container';
import { formatDateTimeZoneIntoIso } from '../internal/format-date-time-zone-into-iso';
import { convertTokens } from '../internal/parse-tokens';
import DatePicker from './date-picker';
import TimePicker from './time-picker';
const packageName = "@atlaskit/datetime-picker";
const packageVersion = "18.7.2";
const analyticsAttributes = {
componentName: 'dateTimePicker',
packageName,
packageVersion
};
const compiledStyles = {
datePickerContainerStyles: "_i0dl1ssb _16jlkb7n _1o9zidpf",
timePickerContainerStyles: "_i0dl1ssb _16jlkb7n",
iconContainerStyles: "_1e0c1txw _4cvr1h6o _i0dl1kw7"
};
// react-select overrides (via @atlaskit/select).
const styles = {
control: style => ({
...style,
backgroundColor: 'transparent',
border: 2,
borderRadius: 0,
paddingLeft: 0,
':hover': {
backgroundColor: 'transparent',
cursor: 'inherit'
}
})
};
/**
* Two action types keep the reducer focused:
*
* - APPLY: used by user-interaction handlers. The handler computes the full
* next state (including the new ISO value) and applies it atomically. This
* guarantees a single re-render with no cascades regardless of React version.
*
* - SET_VALUE: used by the `providedValue` prop effect. The reducer owns all
* parsing logic for external value changes, including the empty-string case
* that the previous useState approach missed.
*/
export const datePickerDefaultAriaLabel = 'Date';
export const timePickerDefaultAriaLabel = 'Time';
/**
* __Date time picker__
*
* A date time picker allows the user to select an associated date and time.
*
* - [Examples](https://atlassian.design/components/datetime-picker/examples)
* - [Code](https://atlassian.design/components/datetime-picker/code)
* - [Usage](https://atlassian.design/components/datetime-picker/usage)
*/
const DateTimePicker = /*#__PURE__*/forwardRef(({
'aria-describedby': ariaDescribedBy,
appearance = 'default',
autoFocus = false,
clearControlLabel = 'clear',
datePickerProps: datePickerPropsWithSelectProps = {},
defaultValue = '',
id = '',
innerProps = {},
isDisabled = false,
isInvalid = false,
isRequired = false,
name = '',
// These disables are here for proper typing when used as defaults. They
// should *not* use the `noop` function.
/* eslint-disable @repo/internal/react/use-noop */
onBlur = _event => {},
onChange: onChangeProp = _value => {},
onFocus = _event => {},
/* eslint-enable @repo/internal/react/use-noop */
parseValue: providedParseValue,
spacing = 'default',
locale = 'en-US',
testId,
timePickerProps: timePickerPropsWithSelectProps = {},
value: providedValue
}, ref) => {
const [isFocused, setIsFocused] = useState(false);
/**
* Defined inside the component so the reducer closes over `providedParseValue`
* without needing to smuggle it through every action payload. React always
* calls the reducer from the latest render, so stale-closure is not a concern.
*/
const reducer = (state, action) => {
switch (action.type) {
case 'APPLY':
return action.payload;
case 'SET_VALUE':
{
const newValue = action.payload;
// Explicit empty-string handling: clear all sub-fields so the date
// and time pickers visually reset when a controlled value is cleared.
if (!newValue) {
return {
value: '',
dateValue: '',
timeValue: '',
zoneValue: ''
};
}
if (providedParseValue) {
const parsed = providedParseValue(newValue, state.dateValue, state.timeValue, state.zoneValue);
return parsed ? {
value: newValue,
...parsed
} : {
value: newValue,
dateValue: state.dateValue,
timeValue: state.timeValue,
zoneValue: state.zoneValue
};
}
const parsed = parseISO(newValue);
return isValid(parsed) ? {
value: newValue,
dateValue: format(parsed, convertTokens('YYYY-MM-DD')),
timeValue: format(parsed, convertTokens('HH:mm')),
zoneValue: format(parsed, convertTokens('ZZ'))
} : {
value: newValue,
dateValue: '',
timeValue: '',
zoneValue: ''
};
}
default:
return state;
}
};
const [dtState, dispatch] = useReducer(reducer, null, () => {
var _ref;
const initialValue = (_ref = providedValue !== null && providedValue !== void 0 ? providedValue : defaultValue) !== null && _ref !== void 0 ? _ref : '';
const initialDate = (datePickerPropsWithSelectProps === null || datePickerPropsWithSelectProps === void 0 ? void 0 : datePickerPropsWithSelectProps.defaultValue) || '';
const initialTime = (timePickerPropsWithSelectProps === null || timePickerPropsWithSelectProps === void 0 ? void 0 : timePickerPropsWithSelectProps.defaultValue) || '';
if (!initialValue) {
return {
value: '',
dateValue: initialDate,
timeValue: initialTime,
zoneValue: ''
};
}
if (providedParseValue) {
const parsed = providedParseValue(initialValue, initialDate, initialTime, '');
return parsed ? {
value: initialValue,
...parsed
} : {
value: initialValue,
dateValue: initialDate,
timeValue: initialTime,
zoneValue: ''
};
}
const parsed = parseISO(initialValue);
return isValid(parsed) ? {
value: initialValue,
dateValue: format(parsed, convertTokens('YYYY-MM-DD')),
timeValue: format(parsed, convertTokens('HH:mm')),
zoneValue: format(parsed, convertTokens('ZZ'))
} : {
value: initialValue,
dateValue: initialDate,
timeValue: initialTime,
zoneValue: ''
};
});
// if a previously controlled value becomes undefined, it clears the reducer
// state so the hidden input no longer keeps the stale ISO timestamp.
// Without this, components that try to clear their components by providing
// `undefined` to the `value` prop will not clear the hidden input and will
// cause unexpected issues.
const hasReceivedProvidedValue = useRef(providedValue !== undefined);
// We don't want to set the state on the first render, as it would
// needlessly be a duplicate.
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
if (providedValue !== undefined) {
hasReceivedProvidedValue.current = true;
dispatch({
type: 'SET_VALUE',
payload: providedValue
});
} else if (hasReceivedProvidedValue.current) {
hasReceivedProvidedValue.current = false;
dispatch({
type: 'SET_VALUE',
payload: ''
});
}
}, [providedValue]);
const parseValue = useCallback((value, providedDateValue, providedTimeValue, providedZoneValue) => {
if (providedParseValue) {
const parsedFromFn = providedParseValue(value, providedDateValue, providedTimeValue, providedZoneValue);
// This handles cases found in Jira where the parse function actually does
// nothing and returns undefined. The previous `getSafeState` function
// just spread the values over the state, but if it returned `undefined`,
// it would just rely on the previous state values. Considering this is
// what is input to this function anyway, this is a safe way to handle
// this, colocate the behavior, and not rely on `getSafeState`.
return parsedFromFn || {
dateValue: providedDateValue,
timeValue: providedTimeValue,
zoneValue: providedZoneValue
};
}
const parsed = parseISO(value);
return isValid(parsed) ? {
dateValue: format(parsed, convertTokens('YYYY-MM-DD')),
timeValue: format(parsed, convertTokens('HH:mm')),
zoneValue: format(parsed, convertTokens('ZZ'))
} : {
dateValue: dtState.dateValue,
timeValue: dtState.timeValue,
zoneValue: dtState.zoneValue
};
}, [providedParseValue, dtState.dateValue, dtState.timeValue, dtState.zoneValue]);
const onDateBlur = event => {
setIsFocused(false);
onBlur(event);
if (datePickerPropsWithSelectProps !== null && datePickerPropsWithSelectProps !== void 0 && datePickerPropsWithSelectProps.onBlur) {
datePickerPropsWithSelectProps.onBlur(event);
}
};
const onTimeBlur = event => {
setIsFocused(false);
onBlur(event);
if (timePickerPropsWithSelectProps !== null && timePickerPropsWithSelectProps !== void 0 && timePickerPropsWithSelectProps.onBlur) {
timePickerPropsWithSelectProps.onBlur(event);
}
};
const onDateFocus = event => {
setIsFocused(false);
onFocus(event);
if (datePickerPropsWithSelectProps !== null && datePickerPropsWithSelectProps !== void 0 && datePickerPropsWithSelectProps.onFocus) {
datePickerPropsWithSelectProps.onFocus(event);
}
};
const onTimeFocus = event => {
setIsFocused(false);
onFocus(event);
if (timePickerPropsWithSelectProps !== null && timePickerPropsWithSelectProps !== void 0 && timePickerPropsWithSelectProps.onFocus) {
timePickerPropsWithSelectProps.onFocus(event);
}
};
const onDateChange = newDateValue => {
const parsedValues = parseValue(dtState.value, newDateValue, dtState.timeValue, dtState.zoneValue);
onValueChange({
providedDateValue: newDateValue,
providedTimeValue: parsedValues.timeValue,
providedZoneValue: parsedValues.zoneValue
});
if (datePickerPropsWithSelectProps !== null && datePickerPropsWithSelectProps !== void 0 && datePickerPropsWithSelectProps.onChange) {
datePickerPropsWithSelectProps.onChange(newDateValue);
}
};
const onTimeChange = newTimeValue => {
const parsedValues = parseValue(dtState.value, dtState.dateValue, newTimeValue, dtState.zoneValue);
onValueChange({
providedDateValue: parsedValues.dateValue,
providedTimeValue: newTimeValue,
providedZoneValue: parsedValues.zoneValue
});
if (timePickerPropsWithSelectProps !== null && timePickerPropsWithSelectProps !== void 0 && timePickerPropsWithSelectProps.onChange) {
timePickerPropsWithSelectProps.onChange(newTimeValue);
}
};
const onClear = () => {
const parsedValues = parseValue(dtState.value, dtState.dateValue, dtState.timeValue, dtState.zoneValue);
onValueChange({
providedDateValue: '',
providedTimeValue: '',
providedZoneValue: parsedValues.zoneValue
});
if (datePickerPropsWithSelectProps !== null && datePickerPropsWithSelectProps !== void 0 && datePickerPropsWithSelectProps.onChange) {
datePickerPropsWithSelectProps.onChange('');
}
if (timePickerPropsWithSelectProps !== null && timePickerPropsWithSelectProps !== void 0 && timePickerPropsWithSelectProps.onChange) {
timePickerPropsWithSelectProps.onChange('');
}
};
const onChangePropWithAnalytics = usePlatformLeafEventHandler({
fn: onChangeProp,
action: 'selectedDate',
actionSubject: 'dateTimePicker',
...analyticsAttributes
});
const onValueChange = ({
providedDateValue,
providedTimeValue,
providedZoneValue
}) => {
if (providedDateValue && providedTimeValue) {
const isoValue = formatDateTimeZoneIntoIso(providedDateValue, providedTimeValue, providedZoneValue);
const {
zoneValue: parsedZone
} = parseValue(isoValue, providedDateValue, providedTimeValue, providedZoneValue);
const valueWithValidZone = formatDateTimeZoneIntoIso(providedDateValue, providedTimeValue, parsedZone);
dispatch({
type: 'APPLY',
payload: {
value: valueWithValidZone,
dateValue: providedDateValue,
timeValue: providedTimeValue,
zoneValue: parsedZone
}
});
onChangePropWithAnalytics(valueWithValidZone);
// If the date or time value was cleared when there is an existing datetime value, then clear the value.
} else if (dtState.value) {
dispatch({
type: 'APPLY',
payload: {
value: '',
dateValue: providedDateValue,
timeValue: providedTimeValue,
zoneValue: providedZoneValue
}
});
onChangePropWithAnalytics('');
} else {
dispatch({
type: 'APPLY',
payload: {
value: '',
dateValue: providedDateValue,
timeValue: providedTimeValue,
zoneValue: providedZoneValue
}
});
}
};
const {
selectProps: datePickerSelectProps,
...datePickerProps
} = datePickerPropsWithSelectProps;
const datePickerAriaDescribedBy = datePickerProps['aria-describedby'] || ariaDescribedBy;
const datePickerLabel = datePickerProps.label || 'Date';
const mergedDatePickerSelectProps = {
...datePickerSelectProps,
styles: mergeStyles(styles, datePickerSelectProps === null || datePickerSelectProps === void 0 ? void 0 : datePickerSelectProps.styles)
};
const {
selectProps: timePickerSelectProps,
...timePickerProps
} = timePickerPropsWithSelectProps;
const timePickerAriaDescribedBy = timePickerProps['aria-describedby'] || ariaDescribedBy;
const timePickerLabel = timePickerProps.label || 'Time';
const mergedTimePickerSelectProps = {
...timePickerSelectProps,
styles: mergeStyles(styles, timePickerSelectProps === null || timePickerSelectProps === void 0 ? void 0 : timePickerSelectProps.styles)
};
// Render DateTimePicker's IconContainer when a value has been filled
// Don't use Date or TimePicker's because they can't be customised
const isClearable = Boolean(dtState.dateValue || dtState.timeValue);
return /*#__PURE__*/React.createElement(DateTimePickerContainer, {
appearance: appearance,
isDisabled: isDisabled,
isFocused: isFocused,
isInvalid: isInvalid,
testId: testId,
innerProps: innerProps,
ref: ref
}, /*#__PURE__*/React.createElement("input", {
name: name,
type: "hidden",
value: dtState.value,
"data-testid": testId && `${testId}--input`
}), /*#__PURE__*/React.createElement(Box, {
xcss: compiledStyles.datePickerContainerStyles
}, /*#__PURE__*/React.createElement(DatePicker, {
appearance: appearance,
"aria-describedby": datePickerAriaDescribedBy,
autoFocus: datePickerProps.autoFocus || autoFocus,
dateFormat: datePickerProps.dateFormat,
defaultIsOpen: datePickerProps.defaultIsOpen,
defaultValue: datePickerProps.defaultValue,
disabled: datePickerProps.disabled,
disabledDateFilter: datePickerProps.disabledDateFilter,
formatDisplayLabel: datePickerProps.formatDisplayLabel,
hideIcon: datePickerProps.hideIcon || true,
icon: datePickerProps.icon,
id: datePickerProps.id || id,
innerProps: datePickerProps.innerProps,
inputLabel: datePickerProps.inputLabel,
inputLabelId: datePickerProps.inputLabelId,
isDisabled: datePickerProps.isDisabled || isDisabled,
isInvalid: datePickerProps.isInvalid || isInvalid,
isOpen: datePickerProps.isOpen,
isRequired: datePickerProps.isRequired || isRequired,
label: datePickerLabel,
locale: datePickerProps.locale || locale,
maxDate: datePickerProps.maxDate,
minDate: datePickerProps.minDate,
name: datePickerProps.name,
nextMonthLabel: datePickerProps.nextMonthLabel,
onBlur: onDateBlur,
onChange: onDateChange,
onFocus: onDateFocus,
openCalendarLabel: datePickerProps.openCalendarLabel,
parseInputValue: datePickerProps.parseInputValue,
placeholder: datePickerProps.placeholder,
previousMonthLabel: datePickerProps.previousMonthLabel,
selectProps: mergedDatePickerSelectProps,
shouldShowCalendarButton: datePickerProps.shouldShowCalendarButton,
spacing: datePickerProps.spacing || spacing,
testId: testId && `${testId}--datepicker` || datePickerProps.testId,
value: dtState.dateValue,
weekStartDay: datePickerProps.weekStartDay
})), /*#__PURE__*/React.createElement(Box, {
xcss: compiledStyles.timePickerContainerStyles
}, /*#__PURE__*/React.createElement(TimePicker, {
appearance: timePickerProps.appearance || appearance,
"aria-describedby": timePickerAriaDescribedBy,
autoFocus: timePickerProps.autoFocus,
defaultIsOpen: timePickerProps.defaultIsOpen,
defaultValue: timePickerProps.defaultValue,
formatDisplayLabel: timePickerProps.formatDisplayLabel,
hideIcon: timePickerProps.hideIcon || true,
id: timePickerProps.id,
innerProps: timePickerProps.innerProps,
isDisabled: timePickerProps.isDisabled || isDisabled,
isInvalid: timePickerProps.isInvalid || isInvalid,
isOpen: timePickerProps.isOpen,
isRequired: timePickerProps.isRequired || isRequired,
label: timePickerLabel,
locale: timePickerProps.locale || locale,
name: timePickerProps.name,
onBlur: onTimeBlur,
onChange: onTimeChange,
onFocus: onTimeFocus,
parseInputValue: timePickerProps.parseInputValue,
placeholder: timePickerProps.placeholder,
selectProps: mergedTimePickerSelectProps,
spacing: timePickerProps.spacing || spacing,
testId: timePickerProps.testId || testId && `${testId}--timepicker`,
timeFormat: timePickerProps.timeFormat,
timeIsEditable: timePickerProps.timeIsEditable,
times: timePickerProps.times,
value: dtState.timeValue
})), isClearable && !isDisabled ? /*#__PURE__*/React.createElement(Inline, {
xcss: compiledStyles.iconContainerStyles
}, /*#__PURE__*/React.createElement(IconButton, {
appearance: "subtle",
label: clearControlLabel,
icon: iconProps => /*#__PURE__*/React.createElement(SelectClearIcon, _extends({}, iconProps, {
color: "var(--ds-text-subtlest, #6B6E76)",
size: "small"
})),
onClick: onClear,
testId: testId && `${testId}--icon--container`,
tabIndex: -1
})) : null);
});
export default DateTimePicker;