UNPKG

baseui

Version:

A React Component library implementing the Base design language

537 lines (530 loc) 23.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.DEFAULT_DATE_FORMAT = void 0; var React = _interopRequireWildcard(require("react")); var _reactUid = require("react-uid"); var _input = require("../input"); var _popover = require("../popover"); var _calendar = _interopRequireDefault(require("./calendar")); var _overrides = require("../helpers/overrides"); var _locale = require("../locale"); var _styledComponents = require("./styled-components"); var _dateHelpers = _interopRequireDefault(require("./utils/date-helpers")); var _dateFnsAdapter = _interopRequireDefault(require("./utils/date-fns-adapter")); var _constants = require("./constants"); var _select = require("../select"); var _icon = require("../icon"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /* Copyright (c) Uber Technologies, Inc. This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree. */ const DEFAULT_DATE_FORMAT = exports.DEFAULT_DATE_FORMAT = 'yyyy/MM/dd'; const INPUT_DELIMITER = '–'; // @ts-ignore const combineSeparatedInputs = (newInputValue, prevCombinedInputValue = '', inputRole) => { let inputValue = newInputValue; const [prevStartDate = '', prevEndDate = ''] = prevCombinedInputValue.split(` ${INPUT_DELIMITER} `); if (inputRole === _constants.INPUT_ROLE.startDate && prevEndDate) { inputValue = `${inputValue} ${INPUT_DELIMITER} ${prevEndDate}`; } if (inputRole === _constants.INPUT_ROLE.endDate) { inputValue = `${prevStartDate} ${INPUT_DELIMITER} ${inputValue}`; } return inputValue; }; class Datepicker_DO_NOT_USE extends React.Component { constructor(props) { super(props); // @ts-ignore _defineProperty(this, "calendar", void 0); _defineProperty(this, "dateHelpers", void 0); _defineProperty(this, "calendarID", void 0); _defineProperty(this, "handleChange", date => { const onChange = this.props.onChange; const onRangeChange = this.props.onRangeChange; if (Array.isArray(date)) { if (onChange && date.every(Boolean)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any onChange({ date: date }); } if (onRangeChange) { onRangeChange({ date: [...date] }); } } else { if (onChange) { onChange({ date }); } if (onRangeChange) { onRangeChange({ date }); } } }); _defineProperty(this, "onCalendarSelect", data => { let isOpen = false; let isPseudoFocused = false; let calendarFocused = false; let nextDate = data.date; if (Array.isArray(nextDate) && this.props.range) { if (!nextDate[0] || !nextDate[1]) { isOpen = true; isPseudoFocused = true; // @ts-ignore calendarFocused = null; } else if (nextDate[0] && nextDate[1]) { const [start, end] = nextDate; if (this.dateHelpers.isAfter(start, end)) { if (this.hasLockedBehavior()) { nextDate = this.props.value; isOpen = true; } else { nextDate = [start, start]; } } else if (this.dateHelpers.dateRangeIncludesDates(nextDate, this.props.excludeDates)) { nextDate = this.props.value; isOpen = true; } if (this.state.lastActiveElm) { this.state.lastActiveElm.focus(); } } } else if (this.state.lastActiveElm) { this.state.lastActiveElm.focus(); } // Time selectors previously caused the calendar popover to close. // The check below refrains from closing the popover if only times changed. const onlyTimeChanged = (prev, next) => { if (!prev || !next) return false; const p = this.dateHelpers.format(prev, 'keyboardDate'); const n = this.dateHelpers.format(next, 'keyboardDate'); if (p === n) { return this.dateHelpers.getHours(prev) !== this.dateHelpers.getHours(next) || this.dateHelpers.getMinutes(prev) !== this.dateHelpers.getMinutes(next); } return false; }; const prevValue = this.props.value; if (Array.isArray(nextDate) && Array.isArray(prevValue)) { if (nextDate.some((d, i) => onlyTimeChanged(prevValue[i], d))) { isOpen = true; } } else if (!Array.isArray(nextDate) && !Array.isArray(prevValue)) { if (onlyTimeChanged(prevValue, nextDate)) { isOpen = true; } } // If nextDate is an array but the datepicker is not ranged, we assign // nextDate directly to the Date value to avoid formatting issues if (Array.isArray(nextDate) && !this.props.range) { nextDate = nextDate[0]; } this.setState({ isOpen, isPseudoFocused, ...(calendarFocused === null ? {} : { calendarFocused }), inputValue: this.formatDisplayValue(nextDate) }); this.handleChange(nextDate); }); _defineProperty(this, "formatDisplayValue", date => { const { displayValueAtRangeIndex, formatDisplayValue, range } = this.props; // @ts-ignore const formatString = this.normalizeDashes(this.props.formatString); if (typeof displayValueAtRangeIndex === 'number') { if (process.env.NODE_ENV !== "production") { if (!range) { console.error('displayValueAtRangeIndex only applies if range'); } if (range && displayValueAtRangeIndex > 1) { console.error('displayValueAtRangeIndex value must be 0 or 1'); } } if (date && Array.isArray(date)) { const value = date[displayValueAtRangeIndex]; if (formatDisplayValue) { return formatDisplayValue(value, formatString); } return this.formatDate(value, formatString); } } if (formatDisplayValue) { return formatDisplayValue(date, formatString); } return this.formatDate(date, formatString); }); _defineProperty(this, "open", inputRole => { this.setState({ isOpen: true, isPseudoFocused: true, calendarFocused: true, selectedInput: inputRole }, this.props.onOpen); }); _defineProperty(this, "close", () => { const isPseudoFocused = false; this.setState({ isOpen: false, selectedInput: null, isPseudoFocused, calendarFocused: false }, this.props.onClose); }); _defineProperty(this, "handleEsc", () => { if (this.state.lastActiveElm) { this.state.lastActiveElm.focus(); } this.close(); }); _defineProperty(this, "handleInputBlur", () => { if (!this.state.isPseudoFocused) { this.close(); } }); _defineProperty(this, "getMask", () => { const { formatString, mask, range } = this.props; if (mask === null || mask === undefined && formatString !== DEFAULT_DATE_FORMAT) { return null; } if (mask) { return this.normalizeDashes(mask); } if (range) { return `9999/99/99 ${INPUT_DELIMITER} 9999/99/99`; } return '9999/99/99'; }); _defineProperty(this, "handleInputChange", (event, inputRole) => { const inputValue = this.props.range ? combineSeparatedInputs(event.currentTarget.value, this.state.inputValue, inputRole) : event.currentTarget.value; const mask = this.getMask(); // @ts-ignore const formatString = this.normalizeDashes(this.props.formatString); if (typeof mask === 'string' && inputValue === mask.replace(/9/g, ' ') || inputValue.length === 0) { if (this.props.range) { this.handleChange([]); } else { this.handleChange(null); } } this.setState({ inputValue }); const parseDateString = dateString => { if (formatString === DEFAULT_DATE_FORMAT) { return this.dateHelpers.parse(dateString, 'slashDate', this.props.locale); } return this.dateHelpers.parseString(dateString, formatString, this.props.locale); }; if (this.props.range && typeof this.props.displayValueAtRangeIndex !== 'number') { const [left, right] = this.normalizeDashes(inputValue).split(` ${INPUT_DELIMITER} `); let startDate = this.dateHelpers.date(left); let endDate = this.dateHelpers.date(right); if (formatString) { startDate = parseDateString(left); endDate = parseDateString(right); } const datesValid = this.dateHelpers.isValid(startDate) && this.dateHelpers.isValid(endDate); // added equal case so that times within the same day can be expressed const rangeValid = this.dateHelpers.isAfter(endDate, startDate) || this.dateHelpers.isEqual(startDate, endDate); if (datesValid && rangeValid) { this.handleChange([startDate, endDate]); } } else { const dateString = this.normalizeDashes(inputValue); let date = this.dateHelpers.date(dateString); const formatString = this.props.formatString; // Prevent early parsing of value. // Eg 25.12.2 will be transformed to 25.12.0002 formatted from date to string // @ts-ignore if (dateString.replace(/(\s)*/g, '').length < formatString.replace(/(\s)*/g, '').length) { // @ts-ignore date = null; } else { date = parseDateString(dateString); } const { displayValueAtRangeIndex, range, value } = this.props; if (date && this.dateHelpers.isValid(date)) { if (range && Array.isArray(value) && typeof displayValueAtRangeIndex === 'number') { let [left, right] = value; if (displayValueAtRangeIndex === 0) { left = date; if (!right) { this.handleChange([left]); } else { if (this.dateHelpers.isAfter(right, left) || this.dateHelpers.isEqual(left, right)) { this.handleChange([left, right]); } else { // Is resetting back to previous value appropriate? Invalid range is not // communicated to the user, but if it was not reset the text value would // show one value and date value another. This seems a bit better but clearly // has a downside. this.handleChange([...value]); } } } else if (displayValueAtRangeIndex === 1) { right = date; if (!left) { // If start value is not defined, set start/end to the same day. this.handleChange([right, right]); } else { if (this.dateHelpers.isAfter(right, left) || this.dateHelpers.isEqual(left, right)) { this.handleChange([left, right]); } else { // See comment above about resetting dates on invalid range this.handleChange([...value]); } } } } else { this.handleChange(date); } } } }); _defineProperty(this, "handleKeyDown", (event, inputRole) => { const keyEvents = new Set(); keyEvents.add('ArrowDown'); keyEvents.add('Space'); keyEvents.add('Enter'); if (!this.state.isOpen && keyEvents.has(event.code)) { this.open(inputRole); } else if (this.state.isOpen && keyEvents.has(event.code)) { // next line prevents the page jump on the initial arrowDown event.preventDefault(); this.focusCalendar(); } else if (this.state.isOpen && event.code === 'Tab') { this.close(); } }); _defineProperty(this, "focusCalendar", () => { if (typeof document !== 'undefined') { const lastActiveElm = document.activeElement; this.setState({ calendarFocused: true, lastActiveElm }); } }); _defineProperty(this, "normalizeDashes", inputValue => { // replacing both hyphens and em-dashes with en-dashes return inputValue.replace(/-/g, INPUT_DELIMITER).replace(/—/g, INPUT_DELIMITER); }); _defineProperty(this, "generateAriaDescribedByIds", () => { let idList = this.state.ariaDescribedby; if (this.props['aria-describedby']) { idList = `${this.props['aria-describedby']} ${idList}`; } return idList; }); _defineProperty(this, "hasLockedBehavior", () => { return this.props.rangedCalendarBehavior === _constants.RANGED_CALENDAR_BEHAVIOR.locked && this.props.range; }); _defineProperty(this, "getPlaceholder", () => this.props.placeholder || this.props.placeholder === '' ? this.props.placeholder : this.props.range ? `YYYY/MM/DD ${INPUT_DELIMITER} YYYY/MM/DD` : 'YYYY/MM/DD'); this.dateHelpers = new _dateHelpers.default(props.adapter); this.state = { calendarFocused: false, isOpen: false, selectedInput: null, isPseudoFocused: false, lastActiveElm: null, inputValue: this.formatDisplayValue(props.value) || '', ariaDescribedby: null // we initialize this post-mount to prevent SSR hydration issue }; this.calendarID = `${(0, _reactUid.uid)(this)}-calendar`; } componentDidMount() { this.setState({ ariaDescribedby: (0, _reactUid.uid)(this) }); } getNullDatePlaceholder(formatString) { return (this.getMask() || formatString).split(INPUT_DELIMITER)[0].replace(/[0-9]|[a-z]/g, ' '); } formatDate(date, formatString) { const format = date => { if (formatString === DEFAULT_DATE_FORMAT) { return this.dateHelpers.format(date, 'slashDate', this.props.locale); } return this.dateHelpers.formatDate(date, formatString, this.props.locale); }; if (!date) { return ''; } else if (Array.isArray(date) && !date[0] && !date[1]) { return ''; } else if (Array.isArray(date) && !date[0] && date[1]) { const endDate = format(date[1]); const startDate = this.getNullDatePlaceholder(formatString); return [startDate, endDate].join(` ${INPUT_DELIMITER} `); } else if (Array.isArray(date)) { return date.map(day => day ? format(day) : '').join(` ${INPUT_DELIMITER} `); } else { return format(date); } } componentDidUpdate(prevProps) { if (prevProps.value !== this.props.value) { this.setState({ inputValue: this.formatDisplayValue(this.props.value) }); } } renderInputComponent(locale, inputRole) { const { overrides = {} } = this.props; const [InputContainer, inputContainerProps] = (0, _overrides.getOverrides)(overrides.InputContainer, _styledComponents.StyledInputContainer); const [InputComponent, inputProps] = (0, _overrides.getOverrides)(overrides.Input, _input.MaskedInput); const inputLabel = this.props['aria-label'] || `${this.props.range ? locale.datepicker.ariaLabelRange : locale.datepicker.ariaLabel}`; const [startDate = '', endDate = ''] = (this.state.inputValue || '').split(` ${INPUT_DELIMITER} `); const value = inputRole === _constants.INPUT_ROLE.startDate ? startDate : inputRole === _constants.INPUT_ROLE.endDate ? endDate : this.state.inputValue; return /*#__PURE__*/React.createElement(InputContainer, _extends({ role: "combobox", "aria-expanded": this.state.isOpen, "aria-haspopup": 'dialog', "aria-controls": this.calendarID }, inputContainerProps), /*#__PURE__*/React.createElement(InputComponent, _extends({ "aria-disabled": this.props.disabled, "aria-label": inputLabel, error: this.props.error, positive: this.props.positive, "aria-describedby": this.generateAriaDescribedByIds(), "aria-labelledby": this.props['aria-labelledby'], "aria-required": this.props.required || null, disabled: this.props.disabled, size: this.props.size, value: value, onBlur: this.handleInputBlur, onKeyDown: event => this.handleKeyDown(event, inputRole), onChange: event => this.handleInputChange(event, inputRole), placeholder: this.getPlaceholder(), mask: this.getMask(), required: this.props.required, clearable: this.props.clearable }, inputProps))); } renderCalendarSelect() { const { overrides = {} } = this.props; const [CalendarSelectComponent, calendarSelectProps] = (0, _overrides.getOverrides)(overrides.CalendarSelect, _select.Select); return /*#__PURE__*/React.createElement(CalendarSelectComponent, _extends({ "aria-describedby": this.props['aria-describedby'], "aria-labelledby": this.props['aria-labelledby'], onBlur: this.handleInputBlur, size: this.props.size, searchable: false, placeholder: /*#__PURE__*/React.createElement(_icon.Calendar, null), overrides: (0, _overrides.mergeOverrides)({ Root: { style: { minWidth: '75px' } }, ControlContainer: { props: { onKeyDown: this.handleKeyDown, onClick: this.open }, style: ({ $theme }) => { return { cursor: 'pointer', borderColor: this.state.calendarFocused || this.state.isPseudoFocused ? $theme.colors.borderSelected : $theme.colors.inputBorder }; } } }, calendarSelectProps.overrides) }, calendarSelectProps)); } render() { const { overrides = {}, startDateLabel = 'Start Date', endDateLabel = 'End Date' } = this.props; const [PopoverComponent, popoverProps] = (0, _overrides.getOverrides)(overrides.Popover, _popover.Popover); const [InputWrapper, inputWrapperProps] = (0, _overrides.getOverrides)(overrides.InputWrapper, _styledComponents.StyledInputWrapper); const [StartDate, startDateProps] = (0, _overrides.getOverrides)(overrides.StartDate, _styledComponents.StyledStartDate); const [EndDate, endDateProps] = (0, _overrides.getOverrides)(overrides.EndDate, _styledComponents.StyledEndDate); const [InputLabel, inputLabelProps] = (0, _overrides.getOverrides)(overrides.InputLabel, _styledComponents.StyledInputLabel); const singleDateFormatString = this.props.formatString || DEFAULT_DATE_FORMAT; const formatString = this.props.range ? `${singleDateFormatString} ${INPUT_DELIMITER} ${singleDateFormatString}` : singleDateFormatString; return /*#__PURE__*/React.createElement(_locale.LocaleContext.Consumer, null, locale => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(PopoverComponent, _extends({ accessibilityType: _popover.ACCESSIBILITY_TYPE.none, focusLock: false, autoFocus: false, mountNode: this.props.mountNode, placement: _popover.PLACEMENT.bottom, isOpen: this.state.isOpen, onClickOutside: this.close, onEsc: this.handleEsc, content: /*#__PURE__*/React.createElement(_calendar.default, _extends({ adapter: this.props.adapter, autoFocusCalendar: this.state.calendarFocused, trapTabbing: true, value: this.props.value, onChange: this.onCalendarSelect, selectedInput: this.state.selectedInput, hasLockedBehavior: this.hasLockedBehavior(), primaryButton: { label: 'Done', onClick: this.close }, secondaryButton: { label: 'Cancel', onClick: this.close }, id: this.calendarID }, this.props)) }, popoverProps), /*#__PURE__*/React.createElement("div", { style: { display: 'flex', flexDirection: 'row', gap: '8px' } }, /*#__PURE__*/React.createElement(InputWrapper, _extends({}, inputWrapperProps, { $separateRangeInputs: this.props.range }), this.props.range ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(StartDate, startDateProps, /*#__PURE__*/React.createElement(InputLabel, inputLabelProps, startDateLabel), this.renderInputComponent(locale, _constants.INPUT_ROLE.startDate)), /*#__PURE__*/React.createElement(EndDate, endDateProps, /*#__PURE__*/React.createElement(InputLabel, inputLabelProps, endDateLabel), this.renderInputComponent(locale, _constants.INPUT_ROLE.endDate))) : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(InputLabel, inputLabelProps, startDateLabel), this.renderInputComponent(locale))), /*#__PURE__*/React.createElement("div", { style: { marginTop: '28px' } }, this.renderCalendarSelect()))))); } } exports.default = Datepicker_DO_NOT_USE; _defineProperty(Datepicker_DO_NOT_USE, "defaultProps", { 'aria-describedby': null, // @ts-ignore value: null, formatString: DEFAULT_DATE_FORMAT, adapter: _dateFnsAdapter.default });