UNPKG

baseui

Version:

A React Component library implementing the Base design language

580 lines (573 loc) • 23.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var React = _interopRequireWildcard(require("react")); var _chevronRight = _interopRequireDefault(require("../icon/chevron-right")); var _chevronLeft = _interopRequireDefault(require("../icon/chevron-left")); var _chevronDown = _interopRequireDefault(require("../icon/chevron-down")); var _dateFnsAdapter = _interopRequireDefault(require("./utils/date-fns-adapter")); var _dateHelpers = _interopRequireDefault(require("./utils/date-helpers")); var _calendarHeaderHelpers = require("./utils/calendar-header-helpers"); var _menu = require("../menu"); var _popover = require("../popover"); var _locale = require("../locale"); var _themeProvider = require("../styles/theme-provider"); var _styledComponents = require("./styled-components"); var _constants = require("./constants"); var _overrides = require("../helpers/overrides"); var _focusVisible = require("../utils/focusVisible"); 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. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars // @ts-ignore const navBtnStyle = ({ $theme }) => ({ cursor: 'pointer' }); const MIN_YEAR = 2000; const MAX_YEAR = 2030; const MIN_MONTH = 0; const MAX_MONTH = 11; const DIRECTION = { NEXT: 'next', PREVIOUS: 'previous' }; // When multiple calendar months are rendered, the month selection dropdown // must account for which of the rendered calendar months it corresponds with function adjustForCalendarOrder(monthId, year, order) { let adjustedMonth = Number(monthId) - order; let adjustedYear = year; if (adjustedMonth < 0) { adjustedMonth = 11; adjustedYear = adjustedYear - 1; } return { adjustedMonthId: adjustedMonth.toString(), adjustedYear: adjustedYear }; } // @ts-ignore function idToYearMonth(id) { return id.split('-').map(Number); } class CalendarHeader extends React.Component { constructor(props) { super(props); // @ts-ignore _defineProperty(this, "dateHelpers", void 0); _defineProperty(this, "monthItems", void 0); _defineProperty(this, "yearItems", void 0); _defineProperty(this, "state", { isMonthDropdownOpen: false, isYearDropdownOpen: false, isFocusVisible: false }); _defineProperty(this, "getDateProp", () => { return this.props.date || this.dateHelpers.date(); }); _defineProperty(this, "getYearItems", () => { const date = this.getDateProp(); const maxDate = this.props.maxDate; const minDate = this.props.minDate; const maxYear = maxDate ? this.dateHelpers.getYear(maxDate) : MAX_YEAR; const minYear = minDate ? this.dateHelpers.getYear(minDate) : MIN_YEAR; const selectedMonth = this.dateHelpers.getMonth(date); // TODO: this logic can be optimized to only run when minDate / maxDate change this.yearItems = Array.from({ length: maxYear - minYear + 1 }, (_, i) => minYear + i).map(year => ({ id: year.toString(), label: year.toString() })); const monthOfMaxDate = maxDate ? this.dateHelpers.getMonth(maxDate) : MAX_MONTH; const monthOfMinDate = minDate ? this.dateHelpers.getMonth(minDate) : MIN_MONTH; // Generates array like [0,1,.... monthOfMaxDate] const maxYearMonths = Array.from({ length: monthOfMaxDate + 1 }, (x, i) => i); // Generates array like [monthOfMinDate, ...., 10, 11] const minYearMonths = Array.from({ length: 12 - monthOfMinDate }, (x, i) => i + monthOfMinDate); if (selectedMonth > maxYearMonths[maxYearMonths.length - 1]) { const lastIdx = this.yearItems.length - 1; this.yearItems[lastIdx] = { ...this.yearItems[lastIdx], disabled: true }; } if (selectedMonth < minYearMonths[0]) { this.yearItems[0] = { ...this.yearItems[0], disabled: true }; } }); _defineProperty(this, "getMonthItems", () => { const date = this.getDateProp(); const year = this.dateHelpers.getYear(date); const maxDate = this.props.maxDate; const minDate = this.props.minDate; const maxYear = maxDate ? this.dateHelpers.getYear(maxDate) : MAX_YEAR; const minYear = minDate ? this.dateHelpers.getYear(minDate) : MIN_YEAR; const monthOfMaxDate = maxDate ? this.dateHelpers.getMonth(maxDate) : MAX_MONTH; // Generates array like [0,1,.... monthOfMaxDate] const maxYearMonths = Array.from({ length: monthOfMaxDate + 1 }, (x, i) => i); const monthOfMinDate = minDate ? this.dateHelpers.getMonth(minDate) : MIN_MONTH; // Generates array like [monthOfMinDate, ...., 10, 11] const minYearMonths = Array.from({ length: 12 - monthOfMinDate }, (x, i) => i + monthOfMinDate); const maxMinYearMonthsIntersection = maxYearMonths.filter(year => minYearMonths.includes(year)); const filterMonthsList = year === maxYear && year === minYear ? maxMinYearMonthsIntersection : year === maxYear ? maxYearMonths : year === minYear ? minYearMonths : null; // @ts-ignore const formatMonthLabel = month => this.dateHelpers.getMonthInLocale(month, this.props.locale); this.monthItems = (0, _calendarHeaderHelpers.getFilteredMonthItems)({ filterMonthsList, formatMonthLabel }); }); _defineProperty(this, "increaseMonth", () => { if (this.props.onMonthChange) { this.props.onMonthChange({ date: this.dateHelpers.addMonths(this.getDateProp(), // in a multi-month context, `order` is the number months ahead of // the root Calendar month that this CalendarHeader displays. We account // for this by incrementing the month by 1, less the value of `order`. 1 - this.props.order) }); } }); _defineProperty(this, "decreaseMonth", () => { if (this.props.onMonthChange) { this.props.onMonthChange({ date: this.dateHelpers.subMonths(this.getDateProp(), 1) }); } }); _defineProperty(this, "isMultiMonthHorizontal", () => { const { monthsShown, orientation } = this.props; if (!monthsShown) { return false; } return orientation === _constants.ORIENTATION.horizontal && monthsShown > 1; }); _defineProperty(this, "isHiddenPaginationButton", direction => { const { monthsShown, order } = this.props; if (!!monthsShown && this.isMultiMonthHorizontal()) { if (direction === DIRECTION.NEXT) { const isLastMonth = order === monthsShown - 1; return !isLastMonth; } else { const isFirstMonth = order === 0; return !isFirstMonth; } } return false; }); _defineProperty(this, "handleFocus", event => { if ((0, _focusVisible.isFocusVisible)(event)) { this.setState({ isFocusVisible: true }); } }); // eslint-disable-next-line @typescript-eslint/no-unused-vars _defineProperty(this, "handleBlur", event => { if (this.state.isFocusVisible !== false) { this.setState({ isFocusVisible: false }); } }); _defineProperty(this, "renderPreviousMonthButton", ({ locale, theme }) => { const date = this.getDateProp(); const { overrides = {}, density } = this.props; const allPrevDaysDisabled = this.dateHelpers.monthDisabledBefore(date, this.props); let isDisabled = false; if (allPrevDaysDisabled) { isDisabled = true; } const nextMonth = this.dateHelpers.subMonths(date, 1); const minYear = this.props.minDate ? this.dateHelpers.getYear(this.props.minDate) : MIN_YEAR; if (this.dateHelpers.getYear(nextMonth) < minYear) { isDisabled = true; } const isHidden = this.isHiddenPaginationButton(DIRECTION.PREVIOUS); if (isHidden) { isDisabled = true; } const [PrevButton, prevButtonProps] = (0, _overrides.getOverrides)(overrides.PrevButton, _styledComponents.StyledPrevButton); const [PrevButtonIcon, prevButtonIconProps] = (0, _overrides.getOverrides)(overrides.PrevButtonIcon, theme.direction === 'rtl' ? _chevronRight.default : _chevronLeft.default); let clickHandler = this.decreaseMonth; if (allPrevDaysDisabled) { // @ts-ignore clickHandler = null; } return /*#__PURE__*/React.createElement(PrevButton, _extends({ "aria-label": locale.datepicker.previousMonth, tabIndex: 0, onClick: clickHandler, disabled: isDisabled, $isFocusVisible: this.state.isFocusVisible, type: "button", $disabled: isDisabled, $order: this.props.order, $density: this.props.density }, prevButtonProps), isHidden ? null : /*#__PURE__*/React.createElement(PrevButtonIcon, _extends({ size: density === _constants.DENSITY.high ? 24 : 36, overrides: { Svg: { style: navBtnStyle } } }, prevButtonIconProps))); }); _defineProperty(this, "renderNextMonthButton", ({ locale, theme }) => { const date = this.getDateProp(); const { overrides = {}, density } = this.props; const allNextDaysDisabled = this.dateHelpers.monthDisabledAfter(date, this.props); let isDisabled = false; if (allNextDaysDisabled) { isDisabled = true; } const nextMonth = this.dateHelpers.addMonths(date, 1); const maxYear = this.props.maxDate ? this.dateHelpers.getYear(this.props.maxDate) : MAX_YEAR; if (this.dateHelpers.getYear(nextMonth) > maxYear) { isDisabled = true; } const isHidden = this.isHiddenPaginationButton(DIRECTION.NEXT); if (isHidden) { isDisabled = true; } const [NextButton, nextButtonProps] = (0, _overrides.getOverrides)(overrides.NextButton, _styledComponents.StyledNextButton); const [NextButtonIcon, nextButtonIconProps] = (0, _overrides.getOverrides)(overrides.NextButtonIcon, theme.direction === 'rtl' ? _chevronLeft.default : _chevronRight.default); let clickHandler = this.increaseMonth; // The other option is to always provide a click handler and let customers // override its functionality based on the `$allPrevDaysDisabled` prop // in a custom NextButton component override // Their options would be to render `null` or not apply the components handler // on click or do nothing if (allNextDaysDisabled) { // @ts-ignore clickHandler = null; } return /*#__PURE__*/React.createElement(NextButton, _extends({ "aria-label": locale.datepicker.nextMonth, tabIndex: 0, onClick: clickHandler, disabled: isDisabled, type: "button", $disabled: isDisabled, $isFocusVisible: this.state.isFocusVisible, $order: this.props.order, $density: this.props.density, $isTrailing: true }, nextButtonProps), isHidden ? null : /*#__PURE__*/React.createElement(NextButtonIcon, _extends({ size: density === _constants.DENSITY.high ? 24 : 36, overrides: { Svg: { style: navBtnStyle } } }, nextButtonIconProps))); }); _defineProperty(this, "canArrowsOpenDropdown", event => { if (!this.state.isMonthDropdownOpen && !this.state.isYearDropdownOpen) { if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { return true; } } return false; }); _defineProperty(this, "renderMonthYearDropdown", () => { const date = this.getDateProp(); const month = this.dateHelpers.getMonth(date); const year = this.dateHelpers.getYear(date); const order = this.props.order; const { locale, overrides = {}, density } = this.props; const [MonthYearSelectButton, monthYearSelectButtonProps] = (0, _overrides.getOverrides)(overrides.MonthYearSelectButton, _styledComponents.StyledMonthYearSelectButton); const [MonthYearSelectIconContainer, monthYearSelectIconContainerProps] = (0, _overrides.getOverrides)(overrides.MonthYearSelectIconContainer, _styledComponents.StyledMonthYearSelectIconContainer); const [OverriddenPopover, popoverProps] = (0, _overrides.getOverrides)(overrides.MonthYearSelectPopover, _popover.Popover); const [OverriddenStatefulMenu, menuProps] = (0, _overrides.getOverrides)(overrides.MonthYearSelectStatefulMenu, _menu.StatefulMenu); menuProps.overrides = (0, _overrides.mergeOverrides)({ List: { style: { height: 'auto', maxHeight: '257px' } } }, menuProps && menuProps.overrides); const initialMonthIndex = this.monthItems.findIndex(month => month.id === this.dateHelpers.getMonth(date).toString()); const initialYearIndex = this.yearItems.findIndex(year => year.id === this.dateHelpers.getYear(date).toString()); const monthTitle = `${this.dateHelpers.getMonthInLocale(this.dateHelpers.getMonth(date), locale)}`; const yearTitle = `${this.dateHelpers.getYear(date)}`; return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(OverriddenPopover, _extends({ placement: "bottom", autoFocus: true, focusLock: true, isOpen: this.state.isMonthDropdownOpen, onClick: () => { this.setState(prev => ({ isMonthDropdownOpen: !prev.isMonthDropdownOpen })); }, onClickOutside: () => this.setState({ isMonthDropdownOpen: false }), onEsc: () => this.setState({ isMonthDropdownOpen: false }), content: () => /*#__PURE__*/React.createElement(OverriddenStatefulMenu, _extends({ initialState: { highlightedIndex: initialMonthIndex, isFocused: true }, items: this.monthItems // @ts-ignore , onItemSelect: ({ item, event }) => { event.preventDefault(); const { adjustedMonthId, adjustedYear } = adjustForCalendarOrder(item.id, year, order); const updatedDate = this.dateHelpers.set(date, { year: adjustedYear, month: idToYearMonth(adjustedMonthId) }); this.props.onMonthChange && this.props.onMonthChange({ date: updatedDate }); this.setState({ isMonthDropdownOpen: false }); } }, menuProps)) }, popoverProps), /*#__PURE__*/React.createElement(MonthYearSelectButton, _extends({ "aria-live": "polite", type: "button", $isFocusVisible: this.state.isFocusVisible, $density: density // @ts-ignore , onKeyUp: event => { if (this.canArrowsOpenDropdown(event)) { this.setState({ isMonthDropdownOpen: true }); } } // @ts-ignore , onKeyDown: event => { if (this.canArrowsOpenDropdown(event)) { // disables page scroll event.preventDefault(); } if (event.key === 'Tab') { this.setState({ isMonthDropdownOpen: false }); } } }, monthYearSelectButtonProps, { "aria-label": `Month, ${monthTitle}` }), monthTitle, /*#__PURE__*/React.createElement(MonthYearSelectIconContainer, monthYearSelectIconContainerProps, /*#__PURE__*/React.createElement(_chevronDown.default, { title: "", overrides: { Svg: { props: { role: 'presentation' } } }, size: density === _constants.DENSITY.high ? 16 : 24 })))), /*#__PURE__*/React.createElement(OverriddenPopover, _extends({ placement: "bottom", focusLock: true, isOpen: this.state.isYearDropdownOpen, onClick: () => { this.setState(prev => ({ isYearDropdownOpen: !prev.isYearDropdownOpen })); }, onClickOutside: () => this.setState({ isYearDropdownOpen: false }), onEsc: () => this.setState({ isYearDropdownOpen: false }), content: () => /*#__PURE__*/React.createElement(OverriddenStatefulMenu, _extends({ initialState: { highlightedIndex: initialYearIndex, isFocused: true }, items: this.yearItems // @ts-ignore , onItemSelect: ({ item, event }) => { event.preventDefault(); const year = idToYearMonth(item.id); const updatedDate = this.dateHelpers.set(date, { year, month }); this.props.onYearChange && this.props.onYearChange({ date: updatedDate }); this.setState({ isYearDropdownOpen: false }); } }, menuProps)) }, popoverProps), /*#__PURE__*/React.createElement(MonthYearSelectButton, _extends({ "aria-live": "polite", type: "button", $isFocusVisible: this.state.isFocusVisible, $density: density // @ts-ignore , onKeyUp: event => { if (this.canArrowsOpenDropdown(event)) { this.setState({ isYearDropdownOpen: true }); } } // @ts-ignore , onKeyDown: event => { if (this.canArrowsOpenDropdown(event)) { // disables page scroll event.preventDefault(); } if (event.key === 'Tab') { this.setState({ isYearDropdownOpen: false }); } }, "aria-label": `Year, ${yearTitle}` }, monthYearSelectButtonProps), yearTitle, /*#__PURE__*/React.createElement(MonthYearSelectIconContainer, monthYearSelectIconContainerProps, /*#__PURE__*/React.createElement(_chevronDown.default, { title: "", overrides: { Svg: { props: { role: 'presentation' } } }, size: density === _constants.DENSITY.high ? 16 : 24 }))))); }); this.dateHelpers = new _dateHelpers.default(props.adapter); this.monthItems = []; this.yearItems = []; } componentDidMount() { this.getYearItems(); this.getMonthItems(); } componentDidUpdate(prevProps) { const selectedMonthDidChange = this.dateHelpers.getMonth(this.props.date) !== this.dateHelpers.getMonth(prevProps.date); const selectedYearDidChange = this.dateHelpers.getYear(this.props.date) !== this.dateHelpers.getYear(prevProps.date); if (selectedMonthDidChange) { // re-calculate yearItems this.getYearItems(); } if (selectedYearDidChange) { // re-calculate monthItems this.getMonthItems(); } } render() { const { overrides = {}, density } = this.props; const [CalendarHeader, calendarHeaderProps] = (0, _overrides.getOverrides)(overrides.CalendarHeader, _styledComponents.StyledCalendarHeader); const [MonthHeader, monthHeaderProps] = (0, _overrides.getOverrides)(overrides.MonthHeader, _styledComponents.StyledMonthHeader); const [WeekdayHeader, weekdayHeaderProps] = (0, _overrides.getOverrides)(overrides.WeekdayHeader, _styledComponents.StyledWeekdayHeader); const startOfWeek = this.dateHelpers.getStartOfWeek(this.getDateProp(), this.props.locale); return /*#__PURE__*/React.createElement(_themeProvider.ThemeContext.Consumer, null, theme => /*#__PURE__*/React.createElement(_locale.LocaleContext.Consumer, null, locale => /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(CalendarHeader, _extends({}, calendarHeaderProps, { $density: this.props.density, onFocus: (0, _focusVisible.forkFocus)(calendarHeaderProps, this.handleFocus), onBlur: (0, _focusVisible.forkBlur)(calendarHeaderProps, this.handleBlur) }), this.renderPreviousMonthButton({ locale, theme }), this.renderMonthYearDropdown(), this.renderNextMonthButton({ locale, theme })), /*#__PURE__*/React.createElement(MonthHeader, _extends({ role: "presentation" }, monthHeaderProps), _constants.WEEKDAYS.map(offset => { const day = this.dateHelpers.addDays(startOfWeek, offset); return /*#__PURE__*/React.createElement(WeekdayHeader, _extends({ key: offset }, weekdayHeaderProps, { $density: density }), /*#__PURE__*/React.createElement("abbr", { style: { textDecoration: 'none' }, title: this.dateHelpers.getWeekdayInLocale(day, this.props.locale) }, this.dateHelpers.getWeekdayMinInLocale(day, this.props.locale))); }))))); } } exports.default = CalendarHeader; _defineProperty(CalendarHeader, "defaultProps", { adapter: _dateFnsAdapter.default, // @ts-ignore locale: null, // @ts-ignore maxDate: null, // @ts-ignore minDate: null, onYearChange: () => {}, overrides: {} });