UNPKG

@blueprintjs/datetime

Version:

Components for interacting with dates and times

155 lines 8.76 kB
/* * Copyright 2023 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as React from "react"; import { DayPicker } from "react-day-picker"; import { DISPLAYNAME_PREFIX } from "@blueprintjs/core"; import { DateRangeSelectionStrategy } from "../../common/dateRangeSelectionStrategy"; import { MonthAndYear } from "../../common/monthAndYear"; import { dateRangeToDayPickerRange } from "../../common/reactDayPickerUtils"; import { DatePickerDropdown } from "../react-day-picker/datePickerDropdown"; import { IconLeft, IconRight } from "../react-day-picker/datePickerNavIcons"; /** * Render a standard day range picker where props.contiguousCalendarMonths is expected to be `true`. */ export const ContiguousDayRangePicker = ({ allowSingleDayRange, boundaryToModify, dayPickerEventHandlers, dayPickerProps, initialMonthAndYear, locale, maxDate, minDate, onRangeSelect, singleMonthOnly = false, value, }) => { const { displayMonth, handleMonthChange } = useContiguousCalendarViews(initialMonthAndYear, singleMonthOnly, value, dayPickerProps === null || dayPickerProps === void 0 ? void 0 : dayPickerProps.onMonthChange); const handleRangeSelect = React.useCallback((range, selectedDay, activeModifiers, e) => { var _a; (_a = dayPickerProps === null || dayPickerProps === void 0 ? void 0 : dayPickerProps.onSelect) === null || _a === void 0 ? void 0 : _a.call(dayPickerProps, range, selectedDay, activeModifiers, e); if (activeModifiers.disabled) { return; } const { dateRange: nextValue, boundary } = DateRangeSelectionStrategy.getNextState(value, selectedDay, allowSingleDayRange, boundaryToModify); onRangeSelect(nextValue, selectedDay, boundary); }, [allowSingleDayRange, boundaryToModify, dayPickerProps, onRangeSelect, value]); return (React.createElement(DayPicker, { showOutsideDays: true, ...dayPickerEventHandlers, ...dayPickerProps, captionLayout: "dropdown-buttons", components: { Dropdown: DatePickerDropdown, IconLeft, IconRight, ...dayPickerProps === null || dayPickerProps === void 0 ? void 0 : dayPickerProps.components, }, fromDate: minDate, locale: locale, mode: "range", month: displayMonth.getFullDate(), numberOfMonths: singleMonthOnly ? 1 : 2, onMonthChange: handleMonthChange, onSelect: handleRangeSelect, selected: dateRangeToDayPickerRange(value), toDate: maxDate })); }; ContiguousDayRangePicker.displayName = `${DISPLAYNAME_PREFIX}.ContiguousDayRangePicker`; /** * State management and navigation event handlers for a single calendar or two contiguous calendar views. * * @param initialMonthAndYear initial month and year to display in the left calendar * @param singleMonthOnly whether we are only displaying a single month instead of two * @param selectedRange currently selected date range * @param userOnMonthChange custom `dayPickerProps.onMonthChange` handler supplied by users of `DateRangePicker` */ function useContiguousCalendarViews(initialMonthAndYear, singleMonthOnly, selectedRange, userOnMonthChange) { const [displayMonth, setDisplayMonth] = React.useState(initialMonthAndYear); const prevSelectedRange = React.useRef(selectedRange); const isInitialRender = React.useRef(true); // use an effect to react to external value updates (such as shortcut item selections) React.useEffect(() => { // upon first render, we shouldn't update the display month; instead just use the initially computed value. // this is important in cases where the user sets `initialMonth` and a controlled `value`. if (isInitialRender.current) { isInitialRender.current = false; return; } if (selectedRange == null) { return; } setDisplayMonth(prevDisplayMonth => { var _a, _b; let newDisplayMonth = prevDisplayMonth.clone(); if (selectedRange[0] == null || selectedRange[1] == null) { // special case: if only one boundary of the range is selected and it is already displayed in one of the // months, don't update the display month. this prevents the picker from shifting around when a user is in // the middle of a selection. if (isDateDisplayed(selectedRange[0], prevDisplayMonth, singleMonthOnly) || isDateDisplayed(selectedRange[1], prevDisplayMonth, singleMonthOnly)) { return newDisplayMonth; } } const nextRangeStart = MonthAndYear.fromDate(selectedRange[0]); const nextRangeEnd = MonthAndYear.fromDate(selectedRange[1]); const hasSelectionEndChanged = ((_a = prevSelectedRange.current[0]) === null || _a === void 0 ? void 0 : _a.valueOf()) === ((_b = selectedRange[0]) === null || _b === void 0 ? void 0 : _b.valueOf()); if (nextRangeStart == null && nextRangeEnd != null) { // Only end date selected. // If the newly selected end date isn't in either of the displayed months, then // - set the right DayPicker to the month of the selected end date // - ensure the left DayPicker is before the right, changing if needed if (!nextRangeEnd.isSame(newDisplayMonth.getNextMonth())) { newDisplayMonth = nextRangeEnd.getPreviousMonth(); } } else if (nextRangeStart != null && nextRangeEnd == null) { // Only start date selected. // If the newly selected start date isn't in either of the displayed months, then // - set the left DayPicker to the month of the selected start date // - ensure the right DayPicker is before the left, changing if needed if (!nextRangeStart.isSame(newDisplayMonth)) { newDisplayMonth = nextRangeStart; } } else if (nextRangeStart != null && nextRangeEnd != null) { if (nextRangeStart.isSame(nextRangeEnd)) { // Both start and end date months are identical if (newDisplayMonth.isSame(nextRangeStart) || (!singleMonthOnly && newDisplayMonth.getNextMonth().isSame(nextRangeEnd))) { // do nothing } else { newDisplayMonth = nextRangeStart; } } else if (hasSelectionEndChanged) { // If the selection end has changed, adjust the view to show the new end date newDisplayMonth = singleMonthOnly ? nextRangeEnd : nextRangeEnd.getPreviousMonth(); } else { // Otherwise, the selection start must have changed, show that newDisplayMonth = nextRangeStart; } } return newDisplayMonth; }); }, [setDisplayMonth, selectedRange, singleMonthOnly]); React.useEffect(() => { prevSelectedRange.current = selectedRange; }, [selectedRange]); const handleMonthChange = React.useCallback(newMonth => { const newDisplayMonth = MonthAndYear.fromDate(newMonth); if (newDisplayMonth) { setDisplayMonth(newDisplayMonth); } userOnMonthChange === null || userOnMonthChange === void 0 ? void 0 : userOnMonthChange(newMonth); }, [userOnMonthChange, setDisplayMonth]); return { displayMonth, handleMonthChange, }; } /** * Determines whether a given date is displayed in a contiguous single- or double-calendar view. */ function isDateDisplayed(date, displayMonth, singleMonthOnly) { if (date == null) { return false; } const month = MonthAndYear.fromDate(date); if (month == null) { return false; } return singleMonthOnly ? displayMonth.isSameMonth(month) : displayMonth.isSameMonth(month) || displayMonth.getNextMonth().isSameMonth(month); } //# sourceMappingURL=contiguousDayRangePicker.js.map