diff --git a/src/components/DatePicker/Common.tsx b/src/components/DatePicker/Common.tsx
index cd48ac38..f93370ea 100644
--- a/src/components/DatePicker/Common.tsx
+++ b/src/components/DatePicker/Common.tsx
@@ -6,13 +6,7 @@ import { Container } from "../Container/Container";
import { useCalendar, UseCalendarOptions } from "@h6s/calendar";
import { IconButton } from "../IconButton/IconButton";
import { Text } from "../Typography/Text/Text";
-
-const locale = "en-US";
-const selectedDateFormatter = new Intl.DateTimeFormat(locale, {
- day: "2-digit",
- month: "short",
- year: "numeric",
-});
+import { headerDateFormatter, selectedDateFormatter, weekdayFormatter } from "./utils";
const explicitWidth = "250px";
@@ -137,12 +131,6 @@ export const DateRangePickerInput = ({
);
};
-const weekdayFormatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
-const headerDateFormatter = new Intl.DateTimeFormat(locale, {
- month: "short",
- year: "numeric",
-});
-
const DatePickerContainer = styled(Container)`
background: ${({ theme }) =>
theme.click.datePicker.dateOption.color.background.default};
@@ -277,6 +265,7 @@ export const CalendarRenderer = ({
orientation="horizontal"
>
{headerDateFormatter.format(headerDate)}
= {
+ component: DateRangePicker,
args: {
+ maxRangeLength: undefined,
onSelectDateRange: (startDate: Date, endDate: Date) => {
console.log("Date range selected: ", startDate, endDate);
},
},
- argTypes: {
- startDate: {
- control: "date",
- },
- endDate: {
- control: "date",
- },
- futureDatesDisabled: {
- control: "boolean",
- },
- placeholder: {
- control: "text",
- },
- onSelectDateRange: {
- control: "object",
- },
+ tags: ["autodocs"],
+ title: "Display/DateRangePicker",
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ predefinedDatesList: [],
},
- component: DateRangePicker,
render: (args: Args) => {
const endDate = args.endDate ? new Date(args.endDate) : undefined;
const startDate = args.startDate ? new Date(args.startDate) : undefined;
+
return (
);
},
- title: "Display/DateRangePicker",
- tags: ["autodocs"],
};
-export default defaultStory;
+export const DateRangeWithMaxRange: Story = {
+ args: {
+ maxRangeLength: 15,
+ predefinedDatesList: [],
+ },
+ render: (args: Args) => {
+ const endDate = args.endDate ? new Date(args.endDate) : undefined;
+ const startDate = args.startDate ? new Date(args.startDate) : undefined;
-export const Playground = {
- ...defaultStory,
+ return (
+
+ );
+ },
+};
+
+export const DateRangeFutureStartDatesDisabled: Story = {
+ args: {
+ futureStartDatesDisabled: true,
+ predefinedDatesList: [],
+ },
+};
+
+export const PredefinedDatesLastSixMonths: Story = {
+ render: (args: Args) => {
+ const endDate = args.endDate ? new Date(args.endDate) : undefined;
+ const startDate = args.startDate ? new Date(args.startDate) : undefined;
+ const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6);
+
+ return (
+
+ );
+ },
+};
+
+export const PredefinedDatesNextSixMonths: Story = {
+ render: (args: Args) => {
+ const endDate = args.endDate ? new Date(args.endDate) : undefined;
+ const startDate = args.startDate ? new Date(args.startDate) : undefined;
+ const predefinedDatesList = getPredefinedMonthsForDateRangePicker(6);
+
+ return (
+
+ );
+ },
+};
+
+export const PredefinedDatesArbitraryDates: Story = {
+ render: (args: Args) => {
+ const endDate = args.endDate ? new Date(args.endDate) : undefined;
+ const startDate = args.startDate ? new Date(args.startDate) : undefined;
+ const predefinedDatesList = [
+ { startDate: new Date("04/14/2025"), endDate: new Date("05/14/2025") },
+ { startDate: new Date("05/14/2025"), endDate: new Date("06/14/2025") },
+ { startDate: new Date("06/14/2025"), endDate: new Date("07/14/2025") },
+ ];
+
+ return (
+
+ );
+ },
+};
+
+export const PredefinedDatesScrollable: Story = {
+ render: (args: Args) => {
+ const endDate = args.endDate ? new Date(args.endDate) : undefined;
+ const startDate = args.startDate ? new Date(args.startDate) : undefined;
+ const predefinedDatesList = [
+ { startDate: new Date("09/14/2024"), endDate: new Date("10/14/2024") },
+ { startDate: new Date("10/14/2024"), endDate: new Date("11/14/2024") },
+ { startDate: new Date("11/14/2024"), endDate: new Date("12/14/2024") },
+ { startDate: new Date("12/14/2024"), endDate: new Date("01/14/2025") },
+ { startDate: new Date("01/14/2025"), endDate: new Date("02/14/2025") },
+ { startDate: new Date("02/14/2025"), endDate: new Date("03/14/2025") },
+ { startDate: new Date("03/14/2025"), endDate: new Date("04/14/2025") },
+ { startDate: new Date("04/14/2025"), endDate: new Date("05/14/2025") },
+ { startDate: new Date("05/14/2025"), endDate: new Date("06/14/2025") },
+ { startDate: new Date("06/14/2025"), endDate: new Date("07/14/2025") },
+ ];
+
+ return (
+
+ );
+ },
};
diff --git a/src/components/DatePicker/DateRangePicker.test.tsx b/src/components/DatePicker/DateRangePicker.test.tsx
index 32b921a8..e762ffbc 100644
--- a/src/components/DatePicker/DateRangePicker.test.tsx
+++ b/src/components/DatePicker/DateRangePicker.test.tsx
@@ -1,6 +1,8 @@
import { renderCUI } from "@/utils/test-utils";
import { DateRangePicker } from "./DateRangePicker";
import userEvent from "@testing-library/user-event";
+import { getPredefinedMonthsForDateRangePicker } from "./utils";
+import { fireEvent } from "@testing-library/dom";
describe("DateRangePicker", () => {
it("opens the calendar on click", async () => {
@@ -149,5 +151,235 @@ describe("DateRangePicker", () => {
expect(handleSelectDate).not.toHaveBeenCalled();
});
+
+ it("allows restricting the max range length", async () => {
+ const startDate = new Date("07-04-2020");
+ const handleSelectDate = vi.fn();
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+
+ const { getByTestId, findByText } = renderCUI(
+
+ );
+
+ user.click(getByTestId("daterangepicker-input"));
+ user.click(await findByText("25"));
+
+ expect(handleSelectDate).not.toHaveBeenCalled();
+
+ fireEvent.click(await findByText("15"));
+ expect(handleSelectDate).toHaveBeenCalled();
+ });
+ });
+
+ describe("predefined date ranges", () => {
+ beforeEach(() => {
+ vi.setSystemTime(new Date("07-04-2020"));
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("doesn't show any preselected dates if the predefined dates list isn't set", async () => {
+ const handleSelectDate = vi.fn();
+
+ const { getByTestId, queryByTestId } = renderCUI(
+
+ );
+
+ await userEvent.click(getByTestId("daterangepicker-input"));
+
+ expect(queryByTestId("predefined-dates-list")).not.toBeInTheDocument();
+ });
+
+ it("shows dates in the past if getPredefinedMonthsForDateRangePicker's value is negative", async () => {
+ const handleSelectDate = vi.fn();
+
+ const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6);
+ const { getByTestId, getByText } = renderCUI(
+
+ );
+
+ await userEvent.click(getByTestId("daterangepicker-input"));
+
+ expect(getByText("Jul 01, 2020 - Jul 04, 2020")).toBeInTheDocument();
+ expect(getByText("Jun 2020")).toBeInTheDocument();
+ expect(getByText("May 2020")).toBeInTheDocument();
+ expect(getByText("Apr 2020")).toBeInTheDocument();
+ expect(getByText("Mar 2020")).toBeInTheDocument();
+ expect(getByText("Feb 2020")).toBeInTheDocument();
+ });
+
+ it("doesn't show a range of a single day when using the past six months and its the first of the month", async () => {
+ vi.setSystemTime(new Date("07-01-2020"));
+
+ const handleSelectDate = vi.fn();
+
+ const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6);
+ const { getByTestId, getByText, queryByText } = renderCUI(
+
+ );
+
+ await userEvent.click(getByTestId("daterangepicker-input"));
+
+ expect(queryByText("Jul")).not.toBeInTheDocument();
+ expect(getByText("Jun 2020")).toBeInTheDocument();
+ expect(getByText("May 2020")).toBeInTheDocument();
+ expect(getByText("Apr 2020")).toBeInTheDocument();
+ expect(getByText("Mar 2020")).toBeInTheDocument();
+ expect(getByText("Feb 2020")).toBeInTheDocument();
+ });
+
+ it("shows dates in the future if getPredefinedMonthsForDateRangePicker's value is positive", async () => {
+ const handleSelectDate = vi.fn();
+
+ const predefinedDatesList = getPredefinedMonthsForDateRangePicker(6);
+ const { getByTestId, getByText } = renderCUI(
+
+ );
+
+ await userEvent.click(getByTestId("daterangepicker-input"));
+
+ expect(getByText("Jul 2020")).toBeInTheDocument();
+ expect(getByText("Aug 2020")).toBeInTheDocument();
+ expect(getByText("Sep 2020")).toBeInTheDocument();
+ expect(getByText("Oct 2020")).toBeInTheDocument();
+ expect(getByText("Nov 2020")).toBeInTheDocument();
+ expect(getByText("Dec 2020")).toBeInTheDocument();
+ });
+
+ it("shows the current current month if it's the first day of month if getPredefinedMonthsForDateRangePicker's value is positive", async () => {
+ vi.setSystemTime(new Date("07-01-2020"));
+ const handleSelectDate = vi.fn();
+
+ const predefinedDatesList = getPredefinedMonthsForDateRangePicker(6);
+ const { getByTestId, getByText } = renderCUI(
+
+ );
+
+ await userEvent.click(getByTestId("daterangepicker-input"));
+
+ expect(getByText("Jul 2020")).toBeInTheDocument();
+ expect(getByText("Aug 2020")).toBeInTheDocument();
+ expect(getByText("Sep 2020")).toBeInTheDocument();
+ expect(getByText("Oct 2020")).toBeInTheDocument();
+ expect(getByText("Nov 2020")).toBeInTheDocument();
+ expect(getByText("Dec 2020")).toBeInTheDocument();
+ });
+
+ it("allows showing the full calendar", async () => {
+ const handleSelectDate = vi.fn();
+
+ const predefinedDatesList = getPredefinedMonthsForDateRangePicker(6);
+ const { getByTestId, getByText, queryByTestId } = renderCUI(
+
+ );
+
+ expect(queryByTestId("datepicker-calendar-container")).not.toBeInTheDocument();
+
+ await userEvent.click(getByTestId("daterangepicker-input"));
+ await userEvent.click(getByText("Custom time period"));
+
+ expect(getByTestId("datepicker-calendar-container")).toBeInTheDocument();
+ });
+
+ it("selects up to the current date if the current month is selected", async () => {
+ const handleSelectDate = vi.fn();
+
+ const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6);
+ const { getByTestId, getByText } = renderCUI(
+
+ );
+
+ await userEvent.click(getByTestId("daterangepicker-input"));
+
+ await userEvent.click(getByTestId("Jul 01, 2020 - Jul 04, 2020"));
+ expect(getByText("Jul 01, 2020 – Jul 04, 2020")).toBeInTheDocument();
+ });
+
+ it("selects the full month if a date in the past or future is selected", async () => {
+ const handleSelectDate = vi.fn();
+
+ const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6);
+ const { getByTestId, getByText } = renderCUI(
+
+ );
+
+ await userEvent.click(getByTestId("daterangepicker-input"));
+
+ await userEvent.click(getByText("May 2020"));
+ expect(getByText("May 01, 2020 – May 31, 2020")).toBeInTheDocument();
+ });
+
+ it("shows the selected month if an entire month is manually selected", async () => {
+ const handleSelectDate = vi.fn();
+
+ const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6);
+ const { getByTestId, getAllByText, getByText } = renderCUI(
+
+ );
+
+ await userEvent.click(getByTestId("daterangepicker-input"));
+ await userEvent.click(getByText("Custom time period"));
+ await userEvent.click(getByTestId("calendar-previous-month"));
+
+ await userEvent.click(getAllByText("1")[0]);
+ await userEvent.click(getByText("30"));
+
+ expect(getByText("Jun 01, 2020 – Jun 30, 2020")).toBeInTheDocument();
+
+ await userEvent.click(getByTestId("daterangepicker-input"));
+ expect(getByText("Jun 2020").getAttribute("data-selected")).toBeTruthy();
+ });
+
+ it("shows months wrapping around to the next or previous year", async () => {
+ vi.setSystemTime(new Date("03-04-2020"));
+ const handleSelectDate = vi.fn();
+
+ const predefinedDatesList = getPredefinedMonthsForDateRangePicker(-6);
+ const { getByTestId, getByText } = renderCUI(
+
+ );
+
+ await userEvent.click(getByTestId("daterangepicker-input"));
+
+ expect(getByText("Mar 01, 2020 - Mar 04, 2020")).toBeInTheDocument();
+ expect(getByText("Feb 2020")).toBeInTheDocument();
+ expect(getByText("Jan 2020")).toBeInTheDocument();
+ expect(getByText("Dec 2019")).toBeInTheDocument();
+ expect(getByText("Nov 2019")).toBeInTheDocument();
+ expect(getByText("Oct 2019")).toBeInTheDocument();
+ });
});
});
diff --git a/src/components/DatePicker/DateRangePicker.tsx b/src/components/DatePicker/DateRangePicker.tsx
index 68119069..879eae78 100644
--- a/src/components/DatePicker/DateRangePicker.tsx
+++ b/src/components/DatePicker/DateRangePicker.tsx
@@ -1,8 +1,60 @@
-import { useCallback, useEffect, useState } from "react";
+import {
+ Dispatch,
+ MouseEvent,
+ SetStateAction,
+ useCallback,
+ useEffect,
+ useState,
+} from "react";
import { isSameDate, UseCalendarOptions } from "@h6s/calendar";
import { styled } from "styled-components";
-import Dropdown from "../Dropdown/Dropdown";
+import { Dropdown } from "../Dropdown/Dropdown";
import { Body, CalendarRenderer, DateRangePickerInput, DateTableCell } from "./Common";
+import { Container } from "../Container/Container";
+import { Panel } from "../Panel/Panel";
+import { Icon } from "../Icon/Icon";
+import {
+ DateRange,
+ datesAreWithinMaxRange,
+ isDateRangeTheWholeMonth,
+ selectedDateFormatter,
+} from "./utils";
+
+const PredefinedCalendarContainer = styled(Panel)`
+ align-items: start;
+ background: ${({ theme }) => theme.click.panel.color.background.muted};
+`;
+
+const PredefinedDatesContainer = styled(Container)`
+ width: 275px;
+`;
+
+// left value of 276px is the width of the PredefinedDatesContainer + 1 pixel for border
+const CalendarRendererContainer = styled.div`
+ border: ${({ theme }) =>
+ `${theme.click.datePicker.dateOption.stroke} solid ${theme.click.datePicker.dateOption.color.background.range}`};
+ border-radius: ${({ theme }) => theme.click.datePicker.dateOption.radii.default};
+ box-shadow: lch(6.77 0 0 / 0.15) 4px 4px 6px -1px, lch(6.77 0 0 / 0.15) 2px 2px 4px -1px;
+ left: 276px;
+ position: absolute;
+ top: 0;
+`;
+
+// Height of 221px is height the height the calendar needs to match the PredefinedDatesContainer
+const StyledCalendarRenderer = styled(CalendarRenderer)`
+ border-radius: ${({ theme }) => theme.click.datePicker.dateOption.radii.default};
+ min-height: 221px;
+`;
+
+const StyledDropdownItem = styled(Dropdown.Item)`
+ min-height: 24px;
+`;
+
+// max-height of 210px allows the scrollable container to be a reasonble height that matches the calendar
+const ScrollableContainer = styled(Container)`
+ max-height: 210px;
+ overflow-y: auto;
+`;
const DateRangeTableCell = styled(DateTableCell)<{
$shouldShowRangeIndicator?: boolean;
@@ -20,6 +72,8 @@ interface CalendarProps {
calendarBody: Body;
closeDatepicker: () => void;
futureDatesDisabled: boolean;
+ futureStartDatesDisabled: boolean;
+ maxRangeLength: number;
setSelectedDate: (selectedDate: Date) => void;
startDate?: Date;
endDate?: Date;
@@ -29,6 +83,8 @@ const Calendar = ({
calendarBody,
closeDatepicker,
futureDatesDisabled,
+ futureStartDatesDisabled,
+ maxRangeLength,
setSelectedDate,
startDate,
endDate,
@@ -50,11 +106,27 @@ const Calendar = ({
const today = new Date();
const isCurrentDate = isSameDate(today, fullDate);
- const isDisabled = futureDatesDisabled ? fullDate > today : false;
const isBetweenStartAndEndDates = Boolean(
startDate && endDate && fullDate > startDate && fullDate < endDate
);
+ let isDisabled = false;
+ if (futureDatesDisabled && fullDate > today) {
+ isDisabled = true;
+ }
+
+ if (futureStartDatesDisabled && !startDate && fullDate > today) {
+ isDisabled = true;
+ }
+
+ if (
+ maxRangeLength > 1 &&
+ startDate &&
+ !datesAreWithinMaxRange(startDate, fullDate, maxRangeLength)
+ ) {
+ isDisabled = true;
+ }
+
const shouldShowRangeIndicator =
!endDate &&
Boolean(
@@ -91,7 +163,7 @@ const Calendar = ({
return (
void;
+ predefinedDatesList: Array;
+ selectedEndDate: Date | undefined;
+ selectedStartDate: Date | undefined;
+ setEndDate: Dispatch>;
+ setStartDate: Dispatch>;
+ shouldShowCustomRange: boolean;
+ showCustomDateRange: Dispatch>;
+}
+
+const PredefinedDates = ({
+ onSelectDateRange,
+ predefinedDatesList,
+ selectedEndDate,
+ selectedStartDate,
+ setEndDate,
+ setStartDate,
+ shouldShowCustomRange,
+ showCustomDateRange,
+}: PredefinedDatesProps) => {
+ const handleCustomTimePeriodClick = (event: MouseEvent) => {
+ event.preventDefault();
+ showCustomDateRange(!shouldShowCustomRange);
+ };
+
+ return (
+
+
+ {predefinedDatesList.map(({ startDate, endDate }) => {
+ const handleItemClick = () => {
+ setStartDate(startDate);
+ setEndDate(endDate);
+ onSelectDateRange(startDate, endDate);
+ };
+
+ const rangeIsSelected =
+ selectedEndDate &&
+ isSameDate(selectedEndDate, endDate) &&
+ selectedStartDate &&
+ isSameDate(selectedStartDate, startDate);
+
+ const isWholeMonth = isDateRangeTheWholeMonth({ startDate, endDate });
+
+ const formattedText = isWholeMonth
+ ? monthFormatter.format(startDate)
+ : `${selectedDateFormatter.format(
+ startDate
+ )} - ${selectedDateFormatter.format(endDate)}`.trim();
+
+ return (
+
+
+ {formattedText}
+ {rangeIsSelected && }
+
+
+ );
+ })}
+
+
+
+ Custom time period
+
+
+
+ );
+};
+
+export interface DateRangePickerProps {
endDate?: Date;
disabled?: boolean;
futureDatesDisabled?: boolean;
+ futureStartDatesDisabled?: boolean;
onSelectDateRange: (selectedStartDate: Date, selectedEndDate: Date) => void;
placeholder?: string;
+ predefinedDatesList?: Array;
+ maxRangeLength?: number;
startDate?: Date;
}
@@ -125,12 +291,16 @@ export const DateRangePicker = ({
startDate,
disabled = false,
futureDatesDisabled = false,
+ futureStartDatesDisabled = false,
+ maxRangeLength = -1,
onSelectDateRange,
placeholder = "start date – end date",
-}: DatePickerProps) => {
+ predefinedDatesList,
+}: DateRangePickerProps) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedStartDate, setSelectedStartDate] = useState();
const [selectedEndDate, setSelectedEndDate] = useState();
+ const [shouldShowCustomRange, setShouldShowCustomRange] = useState(false);
const calendarOptions: UseCalendarOptions = {};
@@ -153,13 +323,27 @@ export const DateRangePicker = ({
const closeDatePicker = useCallback((): void => {
setIsOpen(false);
+ setShouldShowCustomRange(false);
}, []);
+ const handleOpenChange = (isOpen: boolean): void => {
+ setIsOpen(isOpen);
+
+ if (!isOpen) {
+ setShouldShowCustomRange(false);
+ }
+ };
+
const handleSelectDate = useCallback(
(selectedDate: Date): void => {
// Start date and end date are selected, user clicks any date.
// Set start date to the selected date, clear the end date.
if (selectedStartDate && selectedEndDate) {
+ // If futureStartDatesDisabled is true, only set the selected date to the date clicked if it's before today
+ if (futureStartDatesDisabled && selectedDate > new Date()) {
+ setSelectedEndDate(undefined);
+ return;
+ }
setSelectedStartDate(selectedDate);
setSelectedEndDate(undefined);
return;
@@ -183,17 +367,21 @@ export const DateRangePicker = ({
// Otherwise, set the end date to the date the user clicked.
setSelectedEndDate(selectedDate);
onSelectDateRange(selectedStartDate, selectedDate);
+ setShouldShowCustomRange(false);
return;
}
setSelectedStartDate(selectedDate);
},
- [onSelectDateRange, selectedEndDate, selectedStartDate]
+ [futureStartDatesDisabled, onSelectDateRange, selectedEndDate, selectedStartDate]
);
+ const shouldShowPredefinedDates =
+ predefinedDatesList !== undefined && predefinedDatesList.length > 0;
+
return (
@@ -207,18 +395,58 @@ export const DateRangePicker = ({
/>
-
- {body => (
-
+
- )}
-
+
+ {shouldShowCustomRange && (
+
+
+ {(body: Body) => (
+
+ )}
+
+
+ )}
+
+ ) : (
+
+ {(body: Body) => (
+
+ )}
+
+ )}
);
diff --git a/src/components/DatePicker/utils.test.ts b/src/components/DatePicker/utils.test.ts
new file mode 100644
index 00000000..41da3035
--- /dev/null
+++ b/src/components/DatePicker/utils.test.ts
@@ -0,0 +1,75 @@
+import { datesAreWithinMaxRange, isDateRangeTheWholeMonth } from "./utils";
+
+describe("DatePicker utils", () => {
+ describe("checking if two dates are fall within a range", () => {
+ it("returns true if the two dates are within the range", () => {
+ const startDate = new Date("07-01-2025");
+ const endDate = new Date("07-08-2025");
+
+ expect(datesAreWithinMaxRange(startDate, endDate, 15)).toBeTruthy();
+ });
+
+ it("returns false if the two dates are not within the range", () => {
+ const startDate = new Date("07-01-2025");
+ const endDate = new Date("07-31-2025");
+
+ expect(datesAreWithinMaxRange(startDate, endDate, 15)).toBeFalsy();
+ });
+
+ it("is inclusive with dates", () => {
+ const startDate = new Date("07-01-2025");
+ const endDate = new Date("07-16-2025");
+
+ expect(datesAreWithinMaxRange(startDate, endDate, 15)).toBeTruthy();
+ });
+ });
+
+ describe("checking if a date range occupies an entire month", () => {
+ it("returns false is the date range don't have the same month", () => {
+ const startDate = new Date("07-01-2025");
+ const endDate = new Date("08-16-2025");
+ expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeFalsy();
+ });
+
+ it("returns false is the date range starts before the first day in the same month", () => {
+ const startDate = new Date("07-02-2025");
+ const endDate = new Date("07-31-2025");
+ expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeFalsy();
+ });
+
+ it("returns false is the date range ends before the last day in the same month", () => {
+ const startDate = new Date("07-01-2025");
+ const endDate = new Date("07-30-2025");
+ expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeFalsy();
+ });
+
+ it("returns true is the date range occupies the whole month", () => {
+ let startDate = new Date("07-01-2025");
+ let endDate = new Date("07-31-2025");
+ expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeTruthy();
+
+ startDate = new Date("08-01-2025");
+ endDate = new Date("08-31-2025");
+ expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeTruthy();
+
+ startDate = new Date("09-01-2025");
+ endDate = new Date("09-30-2025");
+ expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeTruthy();
+
+ startDate = new Date("02-01-2025");
+ endDate = new Date("02-28-2025");
+ expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeTruthy();
+ });
+
+ it("handles leap years", () => {
+ // 2024 was a leap year
+ let startDate = new Date("02-01-2024");
+ let endDate = new Date("02-29-2024");
+ expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeTruthy();
+
+ startDate = new Date("02-01-2024");
+ endDate = new Date("02-28-2024");
+ expect(isDateRangeTheWholeMonth({ startDate, endDate })).toBeFalsy();
+ });
+ });
+});
diff --git a/src/components/DatePicker/utils.ts b/src/components/DatePicker/utils.ts
new file mode 100644
index 00000000..0696730f
--- /dev/null
+++ b/src/components/DatePicker/utils.ts
@@ -0,0 +1,76 @@
+import dayjs from "dayjs";
+
+export interface DateRange {
+ startDate: Date;
+ endDate: Date;
+}
+
+const locale = "en-US";
+
+export const selectedDateFormatter = new Intl.DateTimeFormat(locale, {
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+});
+
+export const weekdayFormatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
+export const headerDateFormatter = new Intl.DateTimeFormat(locale, {
+ month: "short",
+ year: "numeric",
+});
+
+export const getPredefinedMonthsForDateRangePicker = (
+ numberOfMonths: number
+): Array => {
+ const now = dayjs();
+
+ if (numberOfMonths < 0) {
+ const lastSixMonths: Array = [];
+ for (let i = 0; i < Math.abs(numberOfMonths); i++) {
+ const date = now.subtract(i, "month");
+ if (date.date() === 1 && date.month() === now.month()) {
+ continue;
+ }
+ lastSixMonths.push({
+ startDate: date.startOf("month").toDate(),
+ endDate: i === 0 ? now.toDate() : date.endOf("month").toDate(),
+ });
+ }
+
+ return lastSixMonths.reverse();
+ }
+
+ const nextSixMonths: Array = [];
+ for (let i = 0; i < numberOfMonths; i++) {
+ const date = now.add(i, "month");
+ nextSixMonths.push({
+ startDate: date.startOf("month").toDate(),
+ endDate: date.endOf("month").toDate(),
+ });
+ }
+
+ return nextSixMonths;
+};
+
+export const datesAreWithinMaxRange = (
+ startDate: Date,
+ endDate: Date,
+ maxRangeLength: number
+): boolean => {
+ const daysDifference = Math.abs(dayjs(startDate).diff(dayjs(endDate), "days"));
+
+ return daysDifference <= maxRangeLength;
+};
+
+export const isDateRangeTheWholeMonth = ({ startDate, endDate }: DateRange): boolean => {
+ if (startDate.getMonth() !== endDate.getMonth()) {
+ return false;
+ }
+
+ const start = dayjs(startDate);
+ const end = dayjs(endDate);
+ const startDateIsFirstDay = start.isSame(start.startOf("month"), "day");
+ const endDateIsLastDay = end.isSame(end.endOf("month"), "day");
+
+ return startDateIsFirstDay && endDateIsLastDay;
+};
diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx
index d2f396f2..85b26b0b 100644
--- a/src/components/Dropdown/Dropdown.tsx
+++ b/src/components/Dropdown/Dropdown.tsx
@@ -1,9 +1,10 @@
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { styled } from "styled-components";
-import { HorizontalDirection, Icon, IconName } from "@/components";
import { Arrow, GenericMenuItem, GenericMenuPanel } from "../GenericMenu";
import PopoverArrow from "../icons/PopoverArrow";
import IconWrapper from "../IconWrapper/IconWrapper";
+import { HorizontalDirection, IconName } from "../types";
+import { Icon } from "../Icon/Icon";
export const Dropdown = (props: DropdownMenu.DropdownMenuProps) => (
diff --git a/src/components/index.ts b/src/components/index.ts
index a2c24c20..f6bce05a 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -25,6 +25,8 @@ export { ContextMenu } from "./ContextMenu/ContextMenu";
export { Container } from "./Container/Container";
export { DateDetails } from "@/components/DateDetails/DateDetails";
export { DatePicker } from "./DatePicker/DatePicker";
+export { DateRangePicker } from "./DatePicker/DateRangePicker";
+export { getPredefinedMonthsForDateRangePicker } from "./DatePicker/utils";
export { Dialog } from "./Dialog/Dialog";
export { EllipsisContent } from "./EllipsisContent/EllipsisContent";
export { FileUpload } from "./FileUpload/FileUpload";