generated from hackforla/.github-hackforla-base-repo-template
-
-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat-630 - Redesign of Date Range Picker (#651)
* 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
1 parent
ebc6aa4
commit 5e23e69
Showing
10 changed files
with
675 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
packages/ui/src/components/date-picker/date-picker-calendar.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
200
packages/ui/src/components/date-picker/date-picker-calendar.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
17 changes: 17 additions & 0 deletions
17
packages/ui/src/components/date-picker/date-picker-suggestions.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
25
packages/ui/src/components/date-picker/date-picker-suggestions.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
18
packages/ui/src/components/date-picker/date-picker.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.