@blueprintjs/datetime
Version:
Components for interacting with dates and times
155 lines • 8.76 kB
JavaScript
/*
* 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