Skip to content

Commit

Permalink
Feat-630 - Redesign of Date Range Picker (#651)
Browse files Browse the repository at this point in the history
* ui: implementation of redesign date-range-picker

* ui: add test to redesigned date-range-picker component

* fix ui: adjust calendar to start on sunday

* fix ui: center calendar titles

* fix ui: adjust suggested relative dates

* fix ui: rename suggestion radio test
  • Loading branch information
gibsonliketheguitar authored Apr 21, 2024
1 parent ebc6aa4 commit 5e23e69
Show file tree
Hide file tree
Showing 10 changed files with 675 additions and 0 deletions.
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

0 comments on commit 5e23e69

Please sign in to comment.