@blueprintjs/datetime
Version:
Components for interacting with dates and times
118 lines • 7.62 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 classNames from "classnames";
import * as React from "react";
import { CaptionLabel, useDayPicker, useNavigation } from "react-day-picker";
import innerText from "react-innertext";
import { Button, DISPLAYNAME_PREFIX, HTMLSelect } from "@blueprintjs/core";
import { ChevronLeft, ChevronRight } from "@blueprintjs/icons";
import { DateUtils, Months } from "../../common";
import { DatePickerCaptionClasses as CaptionClasses, ReactDayPickerClasses } from "../../common/classes";
import { useMonthSelectRightOffset } from "../../common/useMonthSelectRightOffset";
import { DatePickerContext } from "../date-picker/datePickerContext";
/**
* Custom react-day-picker caption component used in non-contiguous two-month date range pickers.
*
* We need to override the whole caption instead of its lower-level components because react-day-picker
* does not have built-in support for non-contiguous range pickers.
*
* @see https://daypicker.dev/guides/custom-components
*/
export const DatePickerCaption = (props) => {
const { classNames: rdpClassNames, formatters, fromDate, toDate, labels } = useDayPicker();
const { locale, reverseMonthAndYearMenus } = React.useContext(DatePickerContext);
// non-null assertion because we define these values in defaultProps
const minYear = fromDate.getFullYear();
const maxYear = toDate.getFullYear();
const displayMonth = props.displayMonth.getMonth();
const displayYear = props.displayMonth.getFullYear();
const containerElement = React.useRef(null);
const monthSelectElement = React.useRef(null);
const { currentMonth, goToMonth, nextMonth, previousMonth } = useNavigation();
const handlePreviousClick = React.useCallback(() => previousMonth && goToMonth(previousMonth), [previousMonth, goToMonth]);
const handleNextClick = React.useCallback(() => nextMonth && goToMonth(nextMonth), [nextMonth, goToMonth]);
const prevButton = (React.createElement(Button, { "aria-label": labels.labelPrevious(previousMonth, { locale }), className: classNames(CaptionClasses.DATEPICKER3_NAV_BUTTON, CaptionClasses.DATEPICKER3_NAV_BUTTON_PREVIOUS), disabled: !previousMonth, icon: React.createElement(ChevronLeft, null), onClick: handlePreviousClick, variant: "minimal" }));
const nextButton = (React.createElement(Button, { "aria-label": labels.labelNext(nextMonth, { locale }), className: classNames(CaptionClasses.DATEPICKER3_NAV_BUTTON, CaptionClasses.DATEPICKER3_NAV_BUTTON_NEXT), disabled: !nextMonth, icon: React.createElement(ChevronRight, null), onClick: handleNextClick, variant: "minimal" }));
// build the list of available years, relying on react-day-picker's default date-fns formatter or a
// user-provided formatter to localize the year "names"
const { formatYearCaption } = formatters;
const allYearOptions = React.useMemo(() => {
const years = [];
for (let year = minYear; year <= maxYear; year++) {
const yearDate = new Date(year, 0);
const yearCaption = formatYearCaption(yearDate, { locale });
years.push({ label: innerText(yearCaption), value: year });
}
return years;
}, [formatYearCaption, maxYear, minYear, locale]);
// allow out-of-bounds years but disable the option.
// this handles the Dec 2016 case in https://github.com/palantir/blueprint/issues/391
if (displayYear > maxYear) {
const displayYearDate = new Date(displayYear, 0);
const displayYearCaption = formatYearCaption(displayYearDate, { locale });
allYearOptions.push({ disabled: true, label: innerText(displayYearCaption), value: displayYear });
}
const handleMonthSelectChange = React.useCallback((e) => {
const newMonth = parseInt(e.target.value, 10);
// ignore change events with invalid values to prevent crash on iOS Safari (#4178)
if (isNaN(newMonth)) {
return;
}
const newDate = DateUtils.clone(currentMonth);
newDate.setMonth(newMonth);
goToMonth(newDate);
}, [currentMonth, goToMonth]);
const startMonth = displayYear === minYear ? fromDate.getMonth() : 0;
const endMonth = displayYear === maxYear ? toDate.getMonth() + 1 : 12;
// build the list of available months, relying on react-day-picker's default date-fns formatter or a
// user-provided formatter to localize the month names
const { formatMonthCaption } = formatters;
const allMonths = React.useMemo(() => {
const months = [];
for (let i = Months.JANUARY; i <= Months.DECEMBER; i++) {
const monthDate = new Date(displayYear, i);
const formattedMonth = formatMonthCaption(monthDate, { locale });
months.push(innerText(formattedMonth));
}
return months;
}, [displayYear, formatMonthCaption, locale]);
const allMonthOptions = allMonths.map((month, i) => ({ label: month, value: i }));
const availableMonthOptions = allMonthOptions.slice(startMonth, endMonth);
const displayedMonthText = allMonths[displayMonth];
const monthSelectRightOffset = useMonthSelectRightOffset(monthSelectElement, containerElement, displayedMonthText);
const monthSelect = (React.createElement(HTMLSelect, { "aria-label": labels.labelMonthDropdown(), iconProps: { style: { right: monthSelectRightOffset } }, className: classNames(CaptionClasses.DATEPICKER3_DROPDOWN_MONTH, rdpClassNames.dropdown_month), key: "month", minimal: true, onChange: handleMonthSelectChange, ref: monthSelectElement, value: displayMonth, options: availableMonthOptions }));
const handleYearSelectChange = React.useCallback((e) => {
const newYear = parseInt(e.target.value, 10);
// ignore change events with invalid values to prevent crash on iOS Safari (#4178)
if (isNaN(newYear)) {
return;
}
const newDate = DateUtils.clone(currentMonth);
newDate.setFullYear(newYear);
goToMonth(newDate);
}, [currentMonth, goToMonth]);
const yearSelect = (React.createElement(HTMLSelect, { "aria-label": labels.labelYearDropdown(), className: classNames(CaptionClasses.DATEPICKER3_DROPDOWN_YEAR, rdpClassNames.dropdown_year), key: "year", minimal: true, onChange: handleYearSelectChange, value: displayYear, options: allYearOptions }));
const orderedSelects = reverseMonthAndYearMenus ? [yearSelect, monthSelect] : [monthSelect, yearSelect];
const hiddenCaptionLabel = (React.createElement("div", { className: ReactDayPickerClasses.RDP_VHIDDEN },
React.createElement(CaptionLabel, { displayMonth: props.displayMonth, id: props.id })));
return (React.createElement("div", { className: classNames(CaptionClasses.DATEPICKER3_CAPTION, rdpClassNames.caption), ref: containerElement },
hiddenCaptionLabel,
prevButton,
orderedSelects,
nextButton));
};
DatePickerCaption.displayName = `${DISPLAYNAME_PREFIX}.DatePickerCaption`;
//# sourceMappingURL=datePickerCaption.js.map