Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat-630 - Redesign of Date Range Picker #651

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/ui/src/components/calendar/option-data/months.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ export type MONTHS_OPTION = {
text: string;
};

export const MONTH_NAMES: string[] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];

export const MONTHS_RANGE: MONTHS_OPTION[] = [
{ value: 0, text: "January" },
{ value: 1, text: "February" },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import DatePickerCalendar from "./date-picker-calendar";
import { MONTH_NAMES } from "../calendar/option-data/months";
import userEvent from "@testing-library/user-event";

describe("Date Range 2 month Calendar", () => {
const todayDate = new Date();

beforeEach(async () => {
render(
<DatePickerCalendar
key="date-range-calendar-test"
initDate={todayDate}
startDate={null}
endDate={null}
setStartDate={() => {}}
setEndDate={() => {}}
handleReset={() => {}}
/>,
);
});

test("renders Back Arrow Icon", () => {
expect(screen.getByTestId("ArrowBackIosNewIcon")).toBeInTheDocument();
});

test("renders Forward Arrow Icon", () => {
expect(screen.getByTestId("ArrowForwardIosIcon")).toBeInTheDocument();
});

test("renders current month and next month", () => {
const currMonthIdx = todayDate.getMonth();
const nextMonthIdx = (currMonthIdx + 1) % 12;

expect(screen.getByText(MONTH_NAMES[currMonthIdx])).toBeInTheDocument();
expect(screen.getByText(MONTH_NAMES[nextMonthIdx])).toBeInTheDocument();
});

test("render current month's year and next month's year", () => {
const currMonthIdx = todayDate.getMonth();
const nextMonthIdx = (currMonthIdx + 1) % 12;

expect(screen.getByText(MONTH_NAMES[currMonthIdx])).toBeInTheDocument();
expect(screen.getByText(MONTH_NAMES[nextMonthIdx])).toBeInTheDocument();
});
});

describe("Date Range Call to Action ", () => {
const date = new Date("11-25-2024");
const user = userEvent.setup();
beforeEach(async () => {
render(
<DatePickerCalendar
key="date-range-calendar-test"
initDate={date}
startDate={null}
endDate={null}
setStartDate={() => {}}
setEndDate={() => {}}
handleReset={() => {}}
/>,
);
});

test("renders current month and next month", () => {
expect(screen.getByText("November")).toBeInTheDocument();
expect(screen.getByText("December")).toBeInTheDocument();
expect(screen.getByText((_, ele) => ele?.textContent === "2024")).toBeInTheDocument();
});

test("render December 2024 and January 2025 when Forward Arrow is pressed", async () => {
const nextMonthButton = screen.getByRole("button", { name: /next-months/i });
await user.click(nextMonthButton);

expect(screen.getByText("December")).toBeInTheDocument();
expect(screen.getByText("2024")).toBeInTheDocument();

expect(screen.getByText("January")).toBeInTheDocument();
expect(screen.getByText("2025")).toBeInTheDocument();
});

test("render October 2024 and November 2024 when Forward Arrow is pressed", async () => {
const nextMonthButton = screen.getByRole("button", { name: /prev-months/i });
await user.click(nextMonthButton);

expect(screen.getByText("October")).toBeInTheDocument();
expect(screen.getByText("November")).toBeInTheDocument();
expect(screen.getByText((_, ele) => ele?.textContent === "2024")).toBeInTheDocument();
});
});
200 changes: 200 additions & 0 deletions packages/ui/src/components/date-picker/date-picker-calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { useState } from "react";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
import { isEmpty } from "lodash";

import { Month, MONTH_NAMES } from "../calendar/option-data/months";
import { Year } from "../calendar/option-data/years";
import { CalendarDate } from "../calendar/utils/create-calendar";
import { twMerge } from "tailwind-merge";
import { createTwoMonthCalendar, CALENDAR_MONTH } from "./utils/create-two-month-calendar";
import { isEqual } from "../calendar/utils/is-equal";
import { addMonths } from "date-fns";

interface DatePickerCalendarProps {
initDate?: Date;
endDate: Date | null;
startDate: Date | null;
setEndDate: (arg0: Date | null) => void;
setStartDate: (arg0: Date | null) => void;
handleReset: () => void;
}

export default function DatePickerCalendar(props: DatePickerCalendarProps) {
const { initDate = new Date(), startDate, endDate, setStartDate, setEndDate } = props;

const [date, setDate] = useState(initDate);
const [month, setMonth] = useState(initDate.getMonth() as Month);
const [year, setYear] = useState(initDate.getFullYear());
const [firstMonth, secondMonth] = createTwoMonthCalendar(year as Year, month);

function handleUpdateMonth(type: "prev" | "next") {
const modify = type === "prev" ? -1 : 1;
const newDate = addMonths(new Date(date), modify);

setMonth(newDate.getMonth() as Month);
setYear(newDate.getFullYear() as Year);
setDate(newDate);
}

const handleSelected = ({ day, month, year }: CalendarDate) => {
const newDate = new Date(year, month, day);

if (!startDate && !endDate) {
setStartDate(newDate);
} else if (startDate && !endDate) {
setEndDate(newDate);
} else {
/**
* This handles the edge cases when new End date comes before start date
* Note: state is represent as date, but in system refer to CalendarDate interface
* EG:
* t-0 state:
* start : July 16, 1969
* end : July 24, 1969
*
* t-1 state:
* new End : July 15, 1969 selected
*
* t-final state:
* start : July 15, 1969
* end : null
*/

if (startDate && newDate < startDate) {
setStartDate(newDate);
setEndDate(null);
} else {
setEndDate(newDate);
}
}
};

return (
<div data-testid="date-range-calendar" className="bg-white-100 w-[512px]">
<div className="flex w-full items-center justify-center">
<div className="flex-col items-center justify-center">
{/*Left Calendar Nav*/}
<div className="flex h-[52px] flex-1 items-center justify-between space-x-4 px-4">
<button aria-label="prev-months" className="px-2" onClick={() => handleUpdateMonth("prev")}>
<ArrowBackIosNewIcon sx={{ fontSize: 8, color: "#7A7A7B" }} />
</button>
<div className="space-x-1">
<span aria-label="first-date-month"> {MONTH_NAMES[month]} </span>
<span aria-label="first-date-year">{year}</span>
</div>
<span className="invisible h-6 w-6" />
</div>

{/*Left Calendar*/}
<div className="flex justify-center px-4 pb-2">
<table className="border-collaspse">
<thead>
<tr>
{["S", "M", "T", "W", "T", "F", "S"].map((ele: string, index: number) => (
<td
key={ele + index}
className="text-black-400 h-8 w-8 p-px text-center text-xs font-normal leading-[16.8px]">
{ele}
</td>
))}
</tr>
</thead>
<tbody>
{firstMonth.map((week: CALENDAR_MONTH[], weekIdx: number) => (
<tr key={"first-month" + weekIdx}>
{week.map((ele: CALENDAR_MONTH, colIdx: number) => {
if (isEmpty(ele)) {
return <td key={"empty" + colIdx} className="h-8 w-8" />;
}
const { day, month, year } = ele as CalendarDate;
const key = `first-month-${month}/${day}/${year}`;
const isSelected =
isEqual(ele as CalendarDate, startDate) || isEqual(ele as CalendarDate, endDate);
const isCurrDate = isEqual(ele as CalendarDate, initDate);

return (
<td
key={key}
onClick={() => handleSelected(ele as CalendarDate)}
className={twMerge(
"h-8 w-8 text-center text-xs font-normal leading-[18.8px]",
isCurrDate && "inline-flex items-center justify-center rounded-full border-[1px]",
!isSelected && "rounded-full hover:bg-blue-200",
isSelected && "text-white-100 rounded-full bg-blue-500",
)}>
{day}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>

<div className="flex-col items-center justify-center">
{/**Right Calendar Nav */}
<div className="flex h-[52px] flex-1 items-center justify-between space-x-4 px-4">
<span className="invisible h-6 w-6" />
<div className="space-x-1">
<span aria-label="second-date-month"> {MONTH_NAMES[(month + 1) % 12]} </span>
<span aria-label="second-date-year"> {(month + 1) % 12 === 0 ? year + 1 : year} </span>
</div>
<button aria-label="next-months" className="px-2" onClick={() => handleUpdateMonth("next")}>
<ArrowForwardIosIcon sx={{ fontSize: 8, color: "#7A7A7B" }} />
</button>
</div>

{/**Right Calendar*/}
<div className="flex justify-center px-4 pb-2">
<table className="border-collaspse">
<thead>
<tr>
{["S", "M", "T", "W", "T", "F", "S"].map((ele: string, index: number) => (
<td
key={ele + index}
className="text-black-400 h-8 w-8 p-px text-center text-xs font-normal leading-[16.8px]">
{ele}
</td>
))}
</tr>
</thead>
<tbody>
{secondMonth.map((week: CALENDAR_MONTH[], weekIdx: number) => (
<tr key={"second-month" + weekIdx}>
{week.map((ele: CALENDAR_MONTH, colIdx: number) => {
if (isEmpty(ele)) {
return <td key={"empty" + colIdx} className="h-8 w-8" />;
}
const { day, month, year } = ele as CalendarDate;
const key = `second-month-${month}/${day}/${year}`;
const isSelected =
isEqual(ele as CalendarDate, startDate) || isEqual(ele as CalendarDate, endDate);
const isCurrDate = isEqual(ele as CalendarDate, initDate);

return (
<td
key={key}
onClick={() => handleSelected(ele as CalendarDate)}
className={twMerge(
"h-8 w-8 text-center text-xs font-normal leading-[18.8px]",
isCurrDate && "inline-flex items-center justify-center rounded-full border-[1px]",
!isSelected && "rounded-full hover:bg-blue-200",
isSelected && "text-white-100 rounded-full bg-blue-500",
)}>
{day}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import DatePickerSuggestions from "./date-picker-suggestions";

describe("Date Range Suggestions Radio Group", () => {
beforeEach(() => {
render(<DatePickerSuggestions onSuggestionChange={(value) => console.log(value)} />);
});

test("renders radio group and all it's options", () => {
expect(screen.getByLabelText("Past 1 Year"));
expect(screen.getByLabelText("Past 3 Months"));
expect(screen.getByLabelText("Past 1 Month"));
expect(screen.getByLabelText("Year to Date"));
expect(screen.queryByLabelText("It's over 9000")).not.toBeInTheDocument();
});
});
25 changes: 25 additions & 0 deletions packages/ui/src/components/date-picker/date-picker-suggestions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import _ from "lodash";
import RadioGroup from "../radio-group";

interface DatePickerSuggestion {
onSuggestionChange: (arg0: string) => void;
}

export enum RelativeDatePresets {
"YEARS_1" = "Past 1 Year",
"MONTHS_3" = "Past 3 Months",
"MONTHS_1" = "Past 1 Month",
"YTD" = "Year to Date",
}

const CITATION_DATE_PRESETS = _.map(RelativeDatePresets, (value) => value);

export default function DatePickerSuggestions(props: DatePickerSuggestion) {
const { onSuggestionChange } = props;

return (
<div className="my-4 flex flex-1 flex-col items-start justify-center">
<RadioGroup name="suggested date range preset" options={CITATION_DATE_PRESETS} onChange={onSuggestionChange} />
</div>
);
}
18 changes: 18 additions & 0 deletions packages/ui/src/components/date-picker/date-picker.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Meta, StoryObj } from "@storybook/react";
import DatePicker from "./date-picker";

const meta: Meta<typeof DatePicker> = {
title: "Components/Date Picker",
component: DatePicker,
};

type DatePickerStory = StoryObj<typeof DatePicker>;

export const Default: DatePickerStory = {
render: (args) => <DatePicker {...args} />,
args: {
//todo
},
};

export default meta;
Loading
Loading