diff --git a/index.d.ts b/index.d.ts index 47f92744c..4b37039f1 100644 --- a/index.d.ts +++ b/index.d.ts @@ -37,6 +37,7 @@ export { CakeIcon, CalendarDayIcon, CalendarIcon, + CalendarListIcon, CalendarMonthIcon, CalendarWeekIcon, CapacityIcon, @@ -177,6 +178,7 @@ export { MoneyBackIcon, MoneyIcon, MountainIcon, + MonthPicker, MouseIcon, Number, numberFormat, diff --git a/src/components/DatePicker/DatePicker.jsx b/src/components/DatePicker/DatePicker.jsx index 0cb7aa3ee..bfd5b2532 100644 --- a/src/components/DatePicker/DatePicker.jsx +++ b/src/components/DatePicker/DatePicker.jsx @@ -12,6 +12,7 @@ import { Context } from "../Provider"; import { Day } from "./Day"; import { LocalizedDayPicker } from "./LocalizedDayPicker"; import { MonthYearSelector } from "./MonthYearSelector"; +import { MonthSelector } from "./MonthSelector"; import { NavbarElement } from "./NavbarElement"; import RangeDatePicker from "./RangeDatePicker"; import { RelativeDateRange } from "./RelativeDateRange"; @@ -30,8 +31,10 @@ export const DatePicker = ({ value, getDayContent, disabledDays = [], + selectedDays, loadingDays = [], shouldShowYearPicker = false, + shouldShowMonthSelector = false, onChange, onMonthChange, onSubmitDateRange, @@ -68,6 +71,7 @@ export const DatePicker = ({ const [rangeName, setRangeName] = useState(""); const isRangeVariant = variant === variants.range; const isValidValue = value && value.from && value.to; + const isSelectedDaysHasValidRange = selectedDays && selectedDays.from && selectedDays.to; useEffect(() => { if (timezoneName && !isValidTimeZoneName(timezoneName)) { @@ -190,19 +194,27 @@ export const DatePicker = ({ // TODO: Should be outside this component because this returns JSX const CaptionElement = useMemo(() => { - return shouldShowYearPicker && currentMonth - ? ({ date }) => ( - - ) + return (shouldShowYearPicker || shouldShowMonthSelector) && currentMonth + ? ({ date }) => + shouldShowMonthSelector ? ( + + ) : ( + + ) : undefined; // Adding `handleMonthChange` causes a lot of re-renders, and closes drop-down. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldShowYearPicker, currentMonth]); + }, [shouldShowYearPicker, shouldShowMonthSelector, currentMonth]); // TODO: Should be outside this component because this returns JSX const renderDay = (date) => { @@ -233,12 +245,20 @@ export const DatePicker = ({ ); }; - const rangeModifier = isRangeVariant && isValidValue ? { start: value.from, end: value.to } : null; + const rangeModifier = + isRangeVariant && isValidValue + ? { start: value.from, end: value.to } + : isSelectedDaysHasValidRange + ? { start: selectedDays.from, end: selectedDays.to } + : null; // Comparing `from` and `to` dates hides a weird CSS style when you select the same date twice in a date range. const useDateRangeStyle = isRangeVariant && isValidValue && value.from?.getTime() !== value.to?.getTime(); + const useDateSelectedRangeStyle = + isSelectedDaysHasValidRange && selectedDays.from?.getTime() !== selectedDays.to?.getTime(); // Return the same value if it is already dayjs object or has range variant otherwise format it to dayJs object - const selectedDays = value && (dayjs.isDayjs(value) || isRangeVariant ? value : now(value, timezoneName).toDate()); + const selectedDaysValues = + selectedDays ?? (value && (dayjs.isDayjs(value) || isRangeVariant ? value : now(value, timezoneName).toDate())); return ( <> @@ -267,7 +287,7 @@ export const DatePicker = ({ handleStartMonthChange={handleStartMonthChange} handleEndMonthChange={handleEndMonthChange} handleTodayClick={handleTodayClick} - selectedDays={selectedDays} + selectedDays={selectedDaysValues} locale={locale ?? contextLocale} timezoneName={timezoneName} {...rest} @@ -277,11 +297,12 @@ export const DatePicker = ({ className={clsx( "ui-date-picker rounded-lg pt-3", useDateRangeStyle ? "date-range-picker" : null, + useDateSelectedRangeStyle ? "date-range-picker max-w-[400px]" : null, getDayContent ? "has-custom-content" : null, modifiers.waitlist ? "has-custom-content" : null, )} todayButton="Today" - selectedDays={selectedDays} + selectedDays={selectedDaysValues} month={currentMonth} modifiers={{ ...modifiers, ...rangeModifier }} disabledDays={disabledDays} @@ -319,11 +340,13 @@ export const DatePicker = ({ DatePicker.propTypes = { variant: PropTypes.oneOf(Object.keys(variants)), value: PropTypes.objectOf(Date), + selectedDays: PropTypes.objectOf(Date), upcomingDates: PropTypes.arrayOf(Date), onChange: PropTypes.func.isRequired, onMonthChange: PropTypes.func, disabledDays: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.func]), shouldShowYearPicker: PropTypes.bool, + shouldShowMonthSelector: PropTypes.bool, isDateRangeStyle: PropTypes.bool, isRangeVariant: PropTypes.bool, getDayContent: PropTypes.func, diff --git a/src/components/DatePicker/DatePickerPopover.jsx b/src/components/DatePicker/DatePickerPopover.jsx index a5cb05a45..f42f5ada1 100644 --- a/src/components/DatePicker/DatePickerPopover.jsx +++ b/src/components/DatePicker/DatePickerPopover.jsx @@ -6,12 +6,14 @@ import { formatDate } from "../../helpers/date"; import { Input } from "../Forms/Input"; import { Popover } from "../Popover/Popover"; import { DatePicker } from "./DatePicker"; +import { MonthPicker } from "./MonthPicker"; export const DatePickerPopover = ({ value, variant = "single", dateFormat = "ddd, LL", placeholder = "Select Date", + pickerType = "day", onChange, children, classNames = {}, @@ -79,17 +81,20 @@ export const DatePickerPopover = ({ )} - {isVisible && ( - - )} + {isVisible && + (pickerType === "month" ? ( + + ) : ( + + ))} ); diff --git a/src/components/DatePicker/MonthGrid.jsx b/src/components/DatePicker/MonthGrid.jsx new file mode 100644 index 000000000..49951c18d --- /dev/null +++ b/src/components/DatePicker/MonthGrid.jsx @@ -0,0 +1,73 @@ +import React, { useContext } from "react"; +import dayjs from "dayjs"; +import PropTypes from "prop-types"; +import clsx from "clsx"; +import { Context } from "../Provider"; +import { ChevronLeftIcon, ChevronRightIcon } from "../../icons"; +import { Button } from "../Buttons/Button"; +import { ChevronButton } from "./NavbarElement"; + +const today = dayjs(); + +export const MonthGrid = ({ year, value, onChange, handleYearChange, locale, handleClear, handleToday }) => { + const { locale: contextLocale } = useContext(Context); + const months = [...Array.from({ length: 12 }).keys()].map((m) => + today + .locale(locale ?? contextLocale) + .month(m) + .format("MMM"), + ); + + const handleMonthSelect = (monthIndex) => { + const newDate = new Date(year, monthIndex); + onChange(newDate); + }; + + return ( + +
+ handleYearChange(-1)}> + + + {year} + handleYearChange(1)}> + + +
+ +
+ {months.map((month, index) => ( + + ))} +
+ +
+ + +
+
+ ); +}; + +MonthGrid.propTypes = { + year: PropTypes.number.isRequired, + value: PropTypes.objectOf(Date), + onChange: PropTypes.func.isRequired, + locale: PropTypes.string, +}; diff --git a/src/components/DatePicker/MonthPicker.jsx b/src/components/DatePicker/MonthPicker.jsx new file mode 100644 index 000000000..490324d88 --- /dev/null +++ b/src/components/DatePicker/MonthPicker.jsx @@ -0,0 +1,41 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { MonthGrid } from "./MonthGrid"; + +export const MonthPicker = ({ value, onChange, locale }) => { + const [year, setYear] = useState(new Date(value).getFullYear()); + + const handleYearChange = (offset) => { + setYear(year + offset); + }; + + const handleToday = () => { + const today = new Date(); + setYear(today.getFullYear()); + onChange(today); + }; + + const handleClear = () => { + onChange(value); + }; + + return ( +
+ +
+ ); +}; + +MonthPicker.propTypes = { + value: PropTypes.objectOf(Date), + onChange: PropTypes.func, + locale: PropTypes.string, +}; diff --git a/src/components/DatePicker/MonthSelector.jsx b/src/components/DatePicker/MonthSelector.jsx new file mode 100644 index 000000000..c146e2ea1 --- /dev/null +++ b/src/components/DatePicker/MonthSelector.jsx @@ -0,0 +1,65 @@ +import PropTypes from "prop-types"; +import React, { useState } from "react"; +import dayjs from "dayjs"; +import { ChevronDownIcon } from "../../icons"; +import { Popover } from "../Popover/Popover"; +import { MonthGrid } from "./MonthGrid"; + +export const MonthSelector = ({ date, locale, onChange, currentMonth }) => { + const [isVisible, setIsVisible] = useState(false); + const [year, setYear] = useState(new Date(currentMonth).getFullYear()); + + const toggleVisibility = () => { + setIsVisible(!isVisible); + }; + + const handleMonthSelect = (newDate) => { + onChange(newDate); + setIsVisible(false); + }; + + const handleYearChange = (offset) => { + setYear(year + offset); + }; + + const handleClear = () => { + setIsVisible(false); + }; + + const handleToday = () => { + onChange(new Date()); + setIsVisible(false); + }; + + return ( + + + +
+ {dayjs(date).locale(locale).format("MMMM YYYY")} + +
+ + + +
+
+
+ ); +}; + +MonthSelector.propTypes = { + date: PropTypes.objectOf(Date).isRequired, + onChange: PropTypes.func.isRequired, +}; diff --git a/src/components/DatePicker/NavbarElement.jsx b/src/components/DatePicker/NavbarElement.jsx index 1cd76f6d4..bc6598e55 100644 --- a/src/components/DatePicker/NavbarElement.jsx +++ b/src/components/DatePicker/NavbarElement.jsx @@ -30,7 +30,7 @@ NavbarElement.propTypes = { showPreviousButton: PropTypes.bool, }; -const ChevronButton = ({ isVisible = true, onClick, children }) => { +export const ChevronButton = ({ isVisible = true, onClick, children }) => { return (