@atlaskit/datetime-picker
Version: 
A date time picker allows the user to select an associated date and time.
545 lines (544 loc) • 22.9 kB
JavaScript
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
import _assertThisInitialized from "@babel/runtime/helpers/assertThisInitialized";
import _inherits from "@babel/runtime/helpers/inherits";
import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";
import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _extends from "@babel/runtime/helpers/extends";
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; }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
/** @jsx jsx */
import { Component } from 'react';
import { css, jsx } from '@emotion/react';
import { format, isValid, lastDayOfMonth, parseISO } from 'date-fns';
import pick from 'lodash/pick';
import { createAndFireEvent, withAnalyticsContext, withAnalyticsEvents } from '@atlaskit/analytics-next';
import Calendar from '@atlaskit/calendar';
import CalendarIcon from '@atlaskit/icon/glyph/calendar';
import { UNSAFE_LAYERING } from '@atlaskit/layering';
import { createLocalizationProvider } from '@atlaskit/locale';
import { getBooleanFF } from '@atlaskit/platform-feature-flags';
import Select, { mergeStyles } from '@atlaskit/select';
import { N0, N50A, N60A } from '@atlaskit/theme/colors';
import { layers } from '@atlaskit/theme/constants';
import { defaultDateFormat, EmptyComponent, padToTwo, placeholderDatetime } from '../internal';
import FixedLayer from '../internal/fixed-layer';
import { makeSingleValue } from '../internal/single-value';
import { convertTokens } from './utils';
var packageName = "@atlaskit/datetime-picker";
var packageVersion = "13.0.0";
function getValidDate(iso) {
  var date = parseISO(iso);
  return isValid(date) ? {
    day: date.getDate(),
    month: date.getMonth() + 1,
    year: date.getFullYear()
  } : {};
}
function getShortISOString(date) {
  return format(date, convertTokens('YYYY-MM-DD'));
}
var menuStyles = css({
  zIndex: layers.dialog(),
  backgroundColor: "var(--ds-surface-overlay, ".concat(N0, ")"),
  borderRadius: "var(--ds-border-radius, 3px)",
  boxShadow: "var(--ds-shadow-overlay, ".concat("0 4px 8px -2px ".concat(N50A, ", 0 0 1px ").concat(N60A), ")"),
  overflow: 'hidden'
});
var Menu = function Menu(_ref) {
  var selectProps = _ref.selectProps,
    innerProps = _ref.innerProps;
  return jsx(UNSAFE_LAYERING, {
    isDisabled: getBooleanFF('platform.design-system-team.layering_qmiw3') ? false : true
  }, jsx(FixedLayer, {
    inputValue: selectProps.inputValue,
    containerRef: selectProps.calendarContainerRef,
    content: jsx("div", _extends({
      css: menuStyles
    }, innerProps), jsx(Calendar, _extends({}, getValidDate(selectProps.calendarValue), getValidDate(selectProps.calendarView), {
      disabled: selectProps.calendarDisabled,
      disabledDateFilter: selectProps.calendarDisabledDateFilter,
      minDate: selectProps.calendarMinDate,
      maxDate: selectProps.calendarMaxDate,
      nextMonthLabel: selectProps.nextMonthLabel,
      onChange: selectProps.onCalendarChange,
      onSelect: selectProps.onCalendarSelect,
      previousMonthLabel: selectProps.previousMonthLabel,
      calendarRef: selectProps.calendarRef,
      selected: [selectProps.calendarValue],
      locale: selectProps.calendarLocale,
      testId: selectProps.testId && "".concat(selectProps.testId, "--calendar"),
      weekStartDay: selectProps.calendarWeekStartDay
    }))),
    testId: selectProps.testId
  }));
};
var datePickerDefaultProps = {
  appearance: 'default',
  autoFocus: false,
  defaultIsOpen: false,
  defaultValue: '',
  disabled: [],
  disabledDateFilter: function disabledDateFilter(_) {
    return false;
  },
  hideIcon: false,
  icon: CalendarIcon,
  id: '',
  innerProps: {},
  isDisabled: false,
  isInvalid: 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: function onBlur(_event) {},
  onChange: function onChange(_value) {},
  onFocus: function onFocus(_event) {},
  /* eslint-enable @repo/internal/react/use-noop */
  selectProps: {},
  spacing: 'default',
  locale: 'en-US'
  // Not including a default prop for value as it will
  // Make the component a controlled component
};
var DatePicker = /*#__PURE__*/function (_Component) {
  _inherits(DatePicker, _Component);
  var _super = _createSuper(DatePicker);
  function DatePicker(props) {
    var _this;
    _classCallCheck(this, DatePicker);
    _this = _super.call(this, props);
    _defineProperty(_assertThisInitialized(_this), "calendarRef", null);
    _defineProperty(_assertThisInitialized(_this), "containerRef", null);
    // All state needs to be accessed via this function so that the state is mapped from props
    // correctly to allow controlled/uncontrolled usage.
    _defineProperty(_assertThisInitialized(_this), "getSafeState", function () {
      return _objectSpread(_objectSpread(_objectSpread({}, _this.state), pick(_this.props, ['value', 'isOpen'])), pick(_this.props.selectProps, ['inputValue']));
    });
    _defineProperty(_assertThisInitialized(_this), "isDateDisabled", function (date) {
      return _this.props.disabled.indexOf(date) > -1;
    });
    _defineProperty(_assertThisInitialized(_this), "onCalendarChange", function (_ref2) {
      var iso = _ref2.iso;
      var _iso$split = iso.split('-'),
        _iso$split2 = _slicedToArray(_iso$split, 3),
        year = _iso$split2[0],
        month = _iso$split2[1],
        date = _iso$split2[2];
      var newIso = iso;
      var parsedDate = parseInt(date, 10);
      var parsedMonth = parseInt(month, 10);
      var parsedYear = parseInt(year, 10);
      var lastDayInMonth = lastDayOfMonth(new Date(parsedYear, parsedMonth - 1) // This needs to be -1, because the Date constructor expects an index of the given month
      ).getDate();
      if (lastDayInMonth < parsedDate) {
        newIso = "".concat(year, "-").concat(padToTwo(parsedMonth), "-").concat(padToTwo(lastDayInMonth));
      } else {
        newIso = "".concat(year, "-").concat(padToTwo(parsedMonth), "-").concat(padToTwo(parsedDate));
      }
      _this.setState({
        calendarValue: newIso
      });
    });
    _defineProperty(_assertThisInitialized(_this), "onCalendarSelect", function (_ref3) {
      var iso = _ref3.iso;
      _this.setState({
        selectInputValue: '',
        isOpen: false,
        calendarValue: iso,
        value: iso
      });
      _this.props.onChange(iso);
    });
    _defineProperty(_assertThisInitialized(_this), "onInputClick", function () {
      if (!_this.props.isDisabled && !_this.getSafeState().isOpen) {
        _this.setState({
          isOpen: true
        });
      }
    });
    _defineProperty(_assertThisInitialized(_this), "onContainerBlur", function (event) {
      var _this$containerRef;
      var newlyFocusedElement = event.relatedTarget;
      if (!((_this$containerRef = _this.containerRef) !== null && _this$containerRef !== void 0 && _this$containerRef.contains(newlyFocusedElement))) {
        _this.setState({
          isOpen: false
        });
      }
    });
    _defineProperty(_assertThisInitialized(_this), "onSelectBlur", function (event) {
      var _this$containerRef2;
      var newlyFocusedElement = event.relatedTarget;
      if (_this.getSafeState().clearingFromIcon) {
        // Don't close menu if blurring after the user has clicked clear
        _this.setState({
          clearingFromIcon: false
        });
      } else if (!((_this$containerRef2 = _this.containerRef) !== null && _this$containerRef2 !== void 0 && _this$containerRef2.contains(newlyFocusedElement))) {
        // Don't close menu if focus is staying within the date picker's
        // container. Makes keyboard accessibility of calendar possible
        _this.setState({
          isOpen: false,
          isFocused: false
        });
      }
      _this.props.onBlur(event);
    });
    _defineProperty(_assertThisInitialized(_this), "onSelectFocus", function (event) {
      var _this$getSafeState = _this.getSafeState(),
        clearingFromIcon = _this$getSafeState.clearingFromIcon,
        value = _this$getSafeState.value;
      if (clearingFromIcon) {
        // Don't open menu if focussing after the user has clicked clear
        _this.setState({
          clearingFromIcon: false
        });
      } else {
        _this.setState({
          isOpen: true,
          calendarValue: value,
          isFocused: true
        });
      }
      _this.props.onFocus(event);
    });
    _defineProperty(_assertThisInitialized(_this), "onTextInput", function (event) {
      var value = event.target.value;
      if (value) {
        var parsed = _this.parseDate(value);
        // 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
          _this.setState({
            calendarValue: getShortISOString(parsed)
          });
        }
      }
      _this.setState({
        isOpen: true
      });
    });
    _defineProperty(_assertThisInitialized(_this), "getSafeCalendarValue", function (calendarValue) {
      // If `calendarValue` has a year that is greater than 9999, default to
      // today's date
      var yearIsOverLimit = calendarValue.match(/^\d{5,}/);
      if (yearIsOverLimit) {
        return getShortISOString(new Date());
      }
      return calendarValue;
    });
    _defineProperty(_assertThisInitialized(_this), "onInputKeyDown", function (event) {
      var _this$getSafeState2 = _this.getSafeState(),
        value = _this$getSafeState2.value,
        calendarValue = _this$getSafeState2.calendarValue;
      var keyPressed = event.key.toLowerCase();
      switch (keyPressed) {
        case 'escape':
          _this.setState({
            isOpen: false
          });
          break;
        case 'backspace':
        case 'delete':
          {
            var inputCount = getBooleanFF('platform.design-system-team.date-picker-input-a11y-fix_cbbxs') ? 1 : 0;
            if (value && event.target instanceof HTMLInputElement && event.target.value.length <= inputCount) {
              // If being cleared from keyboard, don't change behaviour
              _this.setState({
                clearingFromIcon: false,
                value: ''
              });
            }
            break;
          }
        case 'enter':
          if (!_this.state.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 (!_this.isDateDisabled(calendarValue)) {
            var _this$getSafeState3 = _this.getSafeState(),
              _value2 = _this$getSafeState3.value;
            // Get a safe `calendarValue` in case the value exceeds the maximum
            // allowed by ISO 8601
            var safeCalendarValue = _this.getSafeCalendarValue(calendarValue);
            var valueChanged = safeCalendarValue !== _value2;
            _this.setState({
              selectInputValue: '',
              isOpen: false,
              value: safeCalendarValue,
              calendarValue: safeCalendarValue
            });
            if (valueChanged) {
              _this.props.onChange(safeCalendarValue);
            }
          }
          break;
        default:
          break;
      }
    });
    _defineProperty(_assertThisInitialized(_this), "onClear", function () {
      var changedState = {
        value: '',
        calendarValue: _this.props.defaultValue || getShortISOString(new Date())
      };
      if (!_this.props.hideIcon) {
        changedState = _objectSpread(_objectSpread({}, changedState), {}, {
          clearingFromIcon: true
        });
      }
      _this.setState(changedState);
      _this.props.onChange('');
    });
    _defineProperty(_assertThisInitialized(_this), "onSelectChange", function (_value, action) {
      // Used for native clear event in React Select
      // Triggered when clicking ClearIndicator or backspace with no value
      if (action.action === 'clear') {
        _this.onClear();
      }
    });
    _defineProperty(_assertThisInitialized(_this), "refCalendar", function (ref) {
      _this.calendarRef = ref;
    });
    _defineProperty(_assertThisInitialized(_this), "handleSelectInputChange", function (selectInputValue, actionMeta) {
      var onInputChange = _this.props.selectProps.onInputChange;
      if (onInputChange) {
        onInputChange(selectInputValue, actionMeta);
      }
      _this.setState({
        selectInputValue: selectInputValue
      });
    });
    _defineProperty(_assertThisInitialized(_this), "getContainerRef", function (ref) {
      var oldRef = _this.containerRef;
      _this.containerRef = 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) {
        _this.forceUpdate();
      }
    });
    /**
     * There are two props that can change how the date is parsed.
     * The priority of props used is:
     *   1. parseInputValue
     *   2. locale
     */
    _defineProperty(_assertThisInitialized(_this), "parseDate", function (date) {
      var _this$props = _this.props,
        parseInputValue = _this$props.parseInputValue,
        dateFormat = _this$props.dateFormat;
      if (parseInputValue) {
        return parseInputValue(date, dateFormat || defaultDateFormat);
      }
      var _this$getSafeState4 = _this.getSafeState(),
        l10n = _this$getSafeState4.l10n;
      return l10n.parseDate(date);
    });
    /**
     * There are multiple props that can change how the date is formatted.
     * The priority of props used is:
     *   1. formatDisplayLabel
     *   2. dateFormat
     *   3. locale
     */
    _defineProperty(_assertThisInitialized(_this), "formatDate", function (value) {
      var _this$props2 = _this.props,
        formatDisplayLabel = _this$props2.formatDisplayLabel,
        dateFormat = _this$props2.dateFormat;
      var _this$getSafeState5 = _this.getSafeState(),
        l10n = _this$getSafeState5.l10n;
      if (formatDisplayLabel) {
        return formatDisplayLabel(value, dateFormat || defaultDateFormat);
      }
      var date = parseISO(value);
      return dateFormat ? format(date, convertTokens(dateFormat)) : l10n.formatDate(date);
    });
    _defineProperty(_assertThisInitialized(_this), "getPlaceholder", function () {
      var placeholder = _this.props.placeholder;
      if (placeholder) {
        return placeholder;
      }
      var _this$getSafeState6 = _this.getSafeState(),
        l10n = _this$getSafeState6.l10n;
      return l10n.formatDate(placeholderDatetime);
    });
    _this.state = {
      isOpen: _this.props.defaultIsOpen,
      isFocused: false,
      clearingFromIcon: false,
      selectInputValue: _this.props.selectProps.inputValue || '',
      value: _this.props.value || _this.props.defaultValue,
      calendarValue: _this.props.value || _this.props.defaultValue || getShortISOString(new Date()),
      l10n: createLocalizationProvider(_this.props.locale),
      locale: _this.props.locale
    };
    return _this;
  }
  _createClass(DatePicker, [{
    key: "render",
    value: function render() {
      var _this$props3 = this.props,
        appearance = _this$props3.appearance,
        autoFocus = _this$props3.autoFocus,
        disabled = _this$props3.disabled,
        hideIcon = _this$props3.hideIcon,
        icon = _this$props3.icon,
        id = _this$props3.id,
        innerProps = _this$props3.innerProps,
        isDisabled = _this$props3.isDisabled,
        disabledDateFilter = _this$props3.disabledDateFilter,
        maxDate = _this$props3.maxDate,
        minDate = _this$props3.minDate,
        isInvalid = _this$props3.isInvalid,
        name = _this$props3.name,
        nextMonthLabel = _this$props3.nextMonthLabel,
        previousMonthLabel = _this$props3.previousMonthLabel,
        selectProps = _this$props3.selectProps,
        spacing = _this$props3.spacing,
        locale = _this$props3.locale,
        testId = _this$props3.testId,
        weekStartDay = _this$props3.weekStartDay;
      var _this$getSafeState7 = this.getSafeState(),
        value = _this$getSafeState7.value,
        calendarValue = _this$getSafeState7.calendarValue,
        isOpen = _this$getSafeState7.isOpen,
        selectInputValue = _this$getSafeState7.selectInputValue;
      var actualSelectInputValue;
      if (getBooleanFF('platform.design-system-team.date-picker-input-a11y-fix_cbbxs')) {
        actualSelectInputValue = selectInputValue || (value ? this.formatDate(value) : undefined);
      } else {
        actualSelectInputValue = selectInputValue;
      }
      var menuIsOpen = isOpen && !isDisabled;
      var showClearIndicator = Boolean((value || selectInputValue) && !hideIcon);
      var dropDownIcon = appearance === 'subtle' || hideIcon || showClearIndicator ? null : icon;
      var SingleValue = makeSingleValue({
        lang: this.props.locale
      });
      var selectComponents = _objectSpread({
        DropdownIndicator: dropDownIcon,
        Menu: Menu,
        SingleValue: SingleValue
      }, !showClearIndicator && {
        ClearIndicator: EmptyComponent
      });
      var _selectProps$styles = selectProps.styles,
        selectStyles = _selectProps$styles === void 0 ? {} : _selectProps$styles;
      var disabledStyle = isDisabled ? {
        pointerEvents: 'none',
        color: "var(--ds-icon-disabled, inherit)"
      } : {};
      var calendarProps = {
        calendarContainerRef: this.containerRef,
        calendarRef: this.refCalendar,
        calendarDisabled: disabled,
        calendarDisabledDateFilter: disabledDateFilter,
        calendarMaxDate: maxDate,
        calendarMinDate: minDate,
        calendarValue: value && getShortISOString(parseISO(value)),
        calendarView: calendarValue,
        onCalendarChange: this.onCalendarChange,
        onCalendarSelect: this.onCalendarSelect,
        calendarLocale: locale,
        calendarWeekStartDay: weekStartDay,
        nextMonthLabel: nextMonthLabel,
        previousMonthLabel: previousMonthLabel
      };
      return (
        // These event handlers must be on this element because the events come
        // from different child elements.
        jsx("div", _extends({}, innerProps, {
          role: "presentation",
          onBlur: this.onContainerBlur,
          onClick: this.onInputClick,
          onInput: this.onTextInput,
          onKeyDown: this.onInputKeyDown,
          ref: this.getContainerRef,
          "data-testid": testId && "".concat(testId, "--container")
        }), jsx("input", {
          name: name,
          type: "hidden",
          value: value,
          "data-testid": testId && "".concat(testId, "--input")
        }), jsx(Select, _extends({
          appearance: this.props.appearance,
          enableAnimation: false,
          menuIsOpen: menuIsOpen,
          closeMenuOnSelect: true,
          autoFocus: autoFocus,
          instanceId: id,
          isDisabled: isDisabled,
          onBlur: this.onSelectBlur,
          onFocus: this.onSelectFocus,
          inputValue: actualSelectInputValue,
          onInputChange: this.handleSelectInputChange,
          components: selectComponents,
          onChange: this.onSelectChange,
          styles: mergeStyles(selectStyles, {
            control: function control(base) {
              return _objectSpread(_objectSpread({}, base), disabledStyle);
            },
            indicatorsContainer: function indicatorsContainer(base) {
              return _objectSpread(_objectSpread({}, base), {}, {
                paddingLeft: "var(--ds-space-025, 2px)",
                // ICON_PADDING = 2
                paddingRight: "var(--ds-space-075, 6px)" // 8 - ICON_PADDING = 6
              });
            }
          }),
          placeholder: this.getPlaceholder(),
          value: value && {
            label: this.formatDate(value),
            value: value
          }
        }, selectProps, calendarProps, {
          isClearable: true,
          spacing: spacing,
          isInvalid: isInvalid,
          testId: testId
        })))
      );
    }
  }], [{
    key: "getDerivedStateFromProps",
    value: function getDerivedStateFromProps(nextProps, prevState) {
      if (nextProps.locale !== prevState.locale) {
        return {
          l10n: createLocalizationProvider(nextProps.locale),
          locale: nextProps.locale
        };
      } else {
        return null;
      }
    }
  }]);
  return DatePicker;
}(Component);
_defineProperty(DatePicker, "defaultProps", datePickerDefaultProps);
export { DatePicker as DatePickerWithoutAnalytics };
export default withAnalyticsContext({
  componentName: 'datePicker',
  packageName: packageName,
  packageVersion: packageVersion
})(withAnalyticsEvents({
  onChange: createAndFireEvent('atlaskit')({
    action: 'selectedDate',
    actionSubject: 'datePicker',
    attributes: {
      componentName: 'datePicker',
      packageName: packageName,
      packageVersion: packageVersion
    }
  })
})(DatePicker));