Skip to content

Commit

Permalink
feat: add new date picker
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelkeyzik committed Nov 1, 2023
1 parent 2bf07c7 commit d47e4de
Show file tree
Hide file tree
Showing 8 changed files with 441 additions and 4 deletions.
34 changes: 31 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"homepage": "https://github.com/expenseapp-io/design-system#readme",
"dependencies": {
"react": "18.2.0",
"react-day-picker": "8.9.1",
"react-dom": "18.2.0"
},
"devDependencies": {
Expand Down Expand Up @@ -67,6 +68,7 @@
"@typescript-eslint/parser": "5.59.6",
"@vitest/coverage-c8": "0.31.0",
"auto": "10.46.0",
"date-fns": "2.30.0",
"eslint": "8.40.0",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-storybook": "0.6.15",
Expand Down
164 changes: 164 additions & 0 deletions src/components/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import React, { ChangeEvent, SyntheticEvent, useRef } from "react";
import { CaptionProps, DayPicker, useNavigation } from "react-day-picker";
import { CalendarIcon, CaretLeftIcon, CaretRightIcon } from "../icons";

interface CustomCaptionProps extends CaptionProps {
formatCaption: (date: Date) => string;
}

function CustomCaption(props: CustomCaptionProps) {
const { goToMonth, nextMonth, previousMonth } = useNavigation();

return (
<header className="date-picker-caption">
<button
type="button"
disabled={!previousMonth}
className="icon-button"
onClick={() => previousMonth && goToMonth(previousMonth)}
>
<CaretLeftIcon />
</button>
<span className="date-picker-caption-month">
{props.formatCaption(props.displayMonth)}
</span>
<button
type="button"
disabled={!nextMonth}
className="icon-button"
onClick={() => nextMonth && goToMonth(nextMonth)}
>
<CaretRightIcon />
</button>
</header>
);
}

interface DatePickerProps {
selectValue: string;
datePickerValue: Date;
onChange: (newDate: Date) => void;
daysToSelect: {
value: string;
label: string;
}[];
withNativePicker?: boolean;
formatCaption: (date: Date) => string;
cancelCopy?: string;
}

function DatePicker(props: DatePickerProps) {
const { selectValue, datePickerValue, onChange, daysToSelect } = props;
const ref = useRef<HTMLDialogElement>(null);
const inputRef = useRef<HTMLInputElement>(null);

function openDatePicker() {
ref.current?.showModal();
}

function closeDatePicker() {
ref.current?.close();
}

function handleCloseDatePicker(
event: SyntheticEvent<HTMLDialogElement, Event>
) {
event.stopPropagation();
closeDatePicker();
}

function updateDate(newDate?: Date) {
if (newDate) {
onChange(newDate);
ref.current?.close();
}
}

function openNativeDatePicker() {
inputRef.current?.click();
}

function handleNativeDateChange(event: ChangeEvent<HTMLInputElement>) {
if (event.currentTarget.value) {
const newDate = new Date(event.currentTarget.value);
onChange(newDate);
}
}

const footer = (
<div className="date-picker-footer">
<button type="button" className="button" onClick={closeDatePicker}>
{props.cancelCopy || "Cancel"}
</button>
</div>
);

return (
<>
<div className="date-picker-container">
<select
className="select date-picker-select"
value={selectValue}
onChange={(event) => {
const newDate = new Date(event.currentTarget.value);
onChange(newDate);
}}
>
{daysToSelect.map((dayOption) => (
<option key={dayOption.value} value={dayOption.value}>
{dayOption.label}
</option>
))}
</select>
<div className="date-picker-button-container">
{/* TODO: this native input is not keyboard friendly on small screen sizes on desktop */}
{props.withNativePicker ? (
<button
type="button"
className="date-picker-button date-picker-mobile-button-container"
onClick={openNativeDatePicker}
>
<CalendarIcon />
<input
ref={inputRef}
className="date-picker-mobile-input"
type="date"
onChange={handleNativeDateChange}
/>
</button>
) : (
<button
type="button"
onClick={openDatePicker}
className="date-picker-button"
>
<CalendarIcon />
</button>
)}
</div>
</div>

<dialog ref={ref} className="date-picker" onClose={handleCloseDatePicker}>
<DayPicker
mode="single"
showOutsideDays
required
selected={datePickerValue}
onSelect={updateDate}
weekStartsOn={1}
components={{
Caption: (captionProps) => (
<CustomCaption
{...captionProps}
formatCaption={props.formatCaption}
/>
),
}}
footer={footer}
/>
</dialog>
</>
);
}

export { DatePicker };
2 changes: 2 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/**
* Export only component that could be used by consumer
*/

export { DatePicker } from "./DatePicker";
73 changes: 73 additions & 0 deletions src/stories/components/DatePicker.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useState } from "react";

import { Meta } from "@storybook/react";
import { DatePicker } from "../../components/DatePicker";
import { format, parse, sub } from "date-fns";

export default {
title: "Components/DatePicker",
component: DatePicker,
} as Meta;

const valueKeyFormat = "yyyy-LL-dd";
const valueFormat = "MMM d (eeee)";

const parseDate = (date: string): Date => {
return parse(date, valueKeyFormat, new Date());
};

const formatDate = (date: Date) => {
return format(date, valueKeyFormat);
};

function generateLastWeekDates(startFrom: Date, selectedDate: Date) {
const days = [];
let currentDate = startFrom;

for (let i = 0; i < 7; i++) {
const formattedDate = format(currentDate, valueFormat);
const customLabels = ["Today", "Yesterday"];

days.push({
value: formatDate(currentDate),
label: customLabels[i] ? `${customLabels[i]}` : formattedDate,
});

currentDate = sub(currentDate, {
days: 1,
});
}

const doesSelectedDateExists = days.find(
(day) => day.value === formatDate(selectedDate)
);

if (!doesSelectedDateExists) {
days.unshift({
value: formatDate(selectedDate),
label: format(selectedDate, valueFormat),
});
}

return days;
}

export const Default = () => {
const [value, setValue] = useState(formatDate(new Date()));

const daysToSelect = generateLastWeekDates(new Date(), parseDate(value));

return (
<div style={{ padding: 40 }}>
<DatePicker
selectValue={value}
datePickerValue={parseDate(value)}
daysToSelect={daysToSelect}
onChange={(newDate) => {
setValue(formatDate(newDate));
}}
formatCaption={(date) => format(date, "MMMM, yyy")}
/>
</div>
);
};
3 changes: 2 additions & 1 deletion src/styles/components/button.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@

display: inline-flex;
gap: var(--space-4);
align-items: center;
justify-content: center;

margin: 0;
padding: var(--space-5);
padding: var(--space-4) var(--space-5);

font: inherit;
font-size: calc(var(--font-scale) * var(--font-base-size));
Expand Down
Loading

0 comments on commit d47e4de

Please sign in to comment.