Skip to content

Commit

Permalink
refactor(date-selector): Refactor DateSelector component
Browse files Browse the repository at this point in the history
  • Loading branch information
diogomateus committed May 15, 2024
1 parent ec7f381 commit ef5a559
Show file tree
Hide file tree
Showing 16 changed files with 474 additions and 388 deletions.
1 change: 0 additions & 1 deletion src/lib/components/button/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import classNames from 'classnames';
import React, { ReactElement, ReactNode } from 'react';
import styles from './style.module.scss';

type ButtonVariant =
| 'filledColor'
Expand Down
112 changes: 112 additions & 0 deletions src/lib/components/dateSelector/components/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useRef } from 'react';
import dayjs from 'dayjs';
import DayPicker from 'react-day-picker';

import { useOnClickOutside } from '../../../hooks/useOnClickOutside';
import { CalendarIcon } from '../../icon/icons';

import styles from './style.module.scss';
import './datepicker.scss';
import { Button } from '../../button';

export interface CalendarProps {
dateFormat: string;
value?: string;
onChange: (date: string) => void;
yearBoundaries: { min: number; max: number };
displayCalendar?: boolean;
dayjsLocale?: ILocale;
firstDayOfWeek?: number;
isOpen?: boolean;
setCalendarOpen: (isOpen: boolean) => void;
}

export const Calendar = ({
dateFormat,
value,
onChange,
yearBoundaries,
displayCalendar,
dayjsLocale,
firstDayOfWeek,
setCalendarOpen,
isOpen
}: CalendarProps) => {
const localeDate = dayjsLocale
? dayjs().locale(dayjsLocale).localeData()
: dayjs().locale('en').localeData();

const localizedWeekdays = localeDate.weekdays();
const localizedWeekdaysShort = localeDate.weekdaysShort();
const localizedMonths = localeDate.months();

const calendarContainerRef = useRef<HTMLDivElement | null>(null);

const calendarDefaultDate =
dayjs().year() >= yearBoundaries.min && dayjs().year() <= yearBoundaries.max
? dayjs().toDate()
: dayjs().set('year', yearBoundaries.max).toDate();

const selectedDateInDateType = value
? dayjs(value).toDate()
: calendarDefaultDate;
const dateCalendarFromMonth = dayjs(String(yearBoundaries.min))
.startOf('year')
.toDate();
const dateCalendarToMonth = dayjs(String(yearBoundaries.max))
.endOf('year')
.toDate();

useOnClickOutside(calendarContainerRef, () => setCalendarOpen(false));

if (!displayCalendar) {
return null;
}

return (
<div
className={`${styles.container} ml8`}
ref={calendarContainerRef}
>
<Button
onClick={() => setCalendarOpen(!isOpen)}
data-testid="calendar-button"
hideLabel
variant='textColor'
leftIcon={<CalendarIcon />}
>
Select date
</Button>

{isOpen && (
<DayPicker
month={selectedDateInDateType}
showOutsideDays={true}
fromMonth={dateCalendarFromMonth}
toMonth={dateCalendarToMonth}
selectedDays={selectedDateInDateType}
onDayClick={(date: Date) => {
if (
dayjs(date).isAfter(dateCalendarFromMonth) ||
dayjs(date).isBefore(dateCalendarToMonth)
) {
const selectedDate = dayjs(date).format(dateFormat);
onChange(selectedDate);
setCalendarOpen(false);
}
}}
pagedNavigation={true}
disabledDays={{
before: dateCalendarFromMonth,
after: dateCalendarToMonth,
}}
firstDayOfWeek={firstDayOfWeek}
locale={dayjsLocale?.name || 'en'}
months={localizedMonths}
weekdaysLong={localizedWeekdays}
weekdaysShort={localizedWeekdaysShort}
/>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@use '../../scss/public/grid' as *;
@use '../../scss/public/colors' as *;
@use '../../../scss/public/grid' as *;
@use '../../../scss/public/colors' as *;

.DayPickerInput {
cursor: pointer;
Expand Down Expand Up @@ -281,12 +281,12 @@
}

.DayPicker-NavButton--prev {
background-image: url('./icons/chevron-left.svg');
background-image: url('../../icon/assets/chevron-left.svg');
margin-left: 16px;
}

.DayPicker-NavButton--next {
background-image: url('./icons/chevron-right.svg');
background-image: url('../../icon/assets/chevron-right.svg');
margin-right: 16px;
}

Expand Down
3 changes: 3 additions & 0 deletions src/lib/components/dateSelector/components/style.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.container {
position: relative;
}
3 changes: 0 additions & 3 deletions src/lib/components/dateSelector/icons/chevron-left.svg

This file was deleted.

3 changes: 0 additions & 3 deletions src/lib/components/dateSelector/icons/chevron-right.svg

This file was deleted.

26 changes: 18 additions & 8 deletions src/lib/components/dateSelector/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useState } from 'react';
import { DateSelector, DateSelectorProps } from '.';
import de from 'dayjs/locale/de';

Expand Down Expand Up @@ -71,14 +72,23 @@ export const DateSelectorStory = ({
onChange,
yearBoundaries,
value
}: DateSelectorProps) => (
<DateSelector
onChange={onChange}
displayCalendar={displayCalendar}
yearBoundaries={yearBoundaries}
value={value}
/>
);
}: DateSelectorProps) => {
const [newValue, setValue] = useState(value);

const handleOnChange = (value: string) => {
setValue(value)
onChange?.(value);
}

return (
<DateSelector
onChange={handleOnChange}
displayCalendar={displayCalendar}
yearBoundaries={yearBoundaries}
value={newValue}
/>
);
}

DateSelectorStory.storyName = "DateSelector";

Expand Down
33 changes: 0 additions & 33 deletions src/lib/components/dateSelector/index.test.ts

This file was deleted.

138 changes: 118 additions & 20 deletions src/lib/components/dateSelector/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { render } from '../../util/testUtils';

import { DateSelector } from '.';

const setup = (date: string, onChange: (date: string) => void = () => {}) => {
const setup = (date?: string, onChange: (date: string) => void = () => {}) => {
return render(
<DateSelector
value={date}
Expand All @@ -14,40 +14,138 @@ const setup = (date: string, onChange: (date: string) => void = () => {}) => {
};

describe('DateSelector component', () => {
it('should return the selected date when clicking on the calendar', async () => {
it('should show the value inside the inputs', () => {
const date = '2024-01-01';
const { getByTestId } = setup(date);

expect(getByTestId('date-selector-day')).toHaveValue('1');
expect(getByTestId('date-selector-month')).toHaveValue('1');
expect(getByTestId('date-selector-year')).toHaveValue('2024');
});

it('should call onChange with the value changed', async () => {
const callback = jest.fn();
const { getByTestId, user } = setup(undefined, callback);

await user.type(getByTestId('date-selector-day'), '5');
await user.type(getByTestId('date-selector-month'), '7');
await user.type(getByTestId('date-selector-year'), '2023');

expect(getByTestId('date-selector-day')).toHaveValue('5');
expect(getByTestId('date-selector-month')).toHaveValue('7');
expect(getByTestId('date-selector-year')).toHaveValue('2023');
expect(callback).toHaveBeenCalledWith('2023-07-05');
});

it('should call onChange with the value changed with initial value', async () => {
const callback = jest.fn();
const date = '2024-01-01';
const { getByTestId, user } = setup(date, callback);

await user.type(getByTestId('date-selector-day'), '{backspace}3');
await user.type(getByTestId('date-selector-month'), '{backspace}7');
await user.type(getByTestId('date-selector-year'), '{backspace}3');

expect(getByTestId('date-selector-day')).toHaveValue('3');
expect(getByTestId('date-selector-month')).toHaveValue('7');
expect(getByTestId('date-selector-year')).toHaveValue('2023');
expect(callback).toHaveBeenCalledWith('2023-07-03');
});

it('should call onChange empty when invalid date', async () => {
const callback = jest.fn();
const { getByTestId, user } = setup(undefined, callback);

await user.type(getByTestId('date-selector-day'), '5');

expect(getByTestId('date-selector-day')).toHaveValue('5');
expect(callback).toHaveBeenCalledWith('');
});

it('should call onChange empty when year out of boundaries', async () => {
const callback = jest.fn();
const date = '2024-01-01';
const expectedDate = '2024-01-17';
const { getByTestId, getByLabelText, user } = setup(date, callback);
const button = getByTestId('calendar-button');
const { getByTestId, user } = setup(date, callback);

await user.type(getByTestId('date-selector-year'), '{backspace}{backspace}30');

expect(getByTestId('date-selector-year')).toHaveValue('2030');
expect(callback).toHaveBeenCalledWith('');
});

it('should call onChange when year in boundaries after being out of boundaries', async () => {
const callback = jest.fn();
const date = '2030-01-01';
const { getByTestId, user } = setup(date, callback);

await user.click(button);
await user.type(getByTestId('date-selector-year'), '{backspace}{backspace}23');

const calendarCell = getByLabelText(/17 2024/);
expect(getByTestId('date-selector-year')).toHaveValue('2023');
expect(callback).toHaveBeenCalledWith('2023-01-01');
});

await user.click(calendarCell);
it('should call onChange with the value changed', async () => {
const callback = jest.fn();
const date = '2024-01-01';
const { getByTestId, user } = setup(date, callback);

expect(callback).toHaveBeenCalledWith(expectedDate);
await user.type(getByTestId('date-selector-day'), '{backspace}3');
await user.type(getByTestId('date-selector-month'), '{backspace}7');
await user.type(getByTestId('date-selector-year'), '{backspace}3');

expect(calendarCell).not.toBeVisible();
expect(getByTestId('date-selector-day')).toHaveValue('3');
expect(getByTestId('date-selector-month')).toHaveValue('7');
expect(callback).toHaveBeenCalledWith('2023-07-03');
});

it('should close the calendar when clicking outside', async () => {
it('should navigate inputs from day to month when day is over 3', async () => {
const callback = jest.fn();
const date = '2024-01-01';
const { getByTestId, getByLabelText, user } = setup(date, callback);
const button = getByTestId('calendar-button');
const { getByTestId, user } = setup(date, callback);

await user.type(getByTestId('date-selector-day'), '{backspace}45');

expect(getByTestId('date-selector-day')).toHaveValue('4');
expect(getByTestId('date-selector-month')).toHaveValue('5');
expect(callback).toHaveBeenCalledWith('2024-05-04');
});

describe('Calendar button', () => {
it('should return the selected date when clicking on the calendar', async () => {
const callback = jest.fn();
const date = '2024-01-01';
const expectedDate = '2024-01-17';
const { getByTestId, getByLabelText, user } = setup(date, callback);
const button = getByTestId('calendar-button');

await user.click(button);

const calendarCell = getByLabelText(/17 2024/);

await user.click(calendarCell);

expect(callback).toHaveBeenCalledWith(expectedDate);

expect(calendarCell).not.toBeVisible();
});

it('should close the calendar when clicking outside', async () => {
const callback = jest.fn();
const date = '2024-01-01';
const { getByTestId, getByLabelText, user } = setup(date, callback);
const button = getByTestId('calendar-button');

await user.click(button);
await user.click(button);

const calendarCell = getByLabelText(/17 2024/);
expect(calendarCell).toBeVisible();
const calendarCell = getByLabelText(/17 2024/);
expect(calendarCell).toBeVisible();

// click outside the calendar
await user.click(document.body);
// click outside the calendar
await user.click(document.body);

expect(callback).not.toHaveBeenCalled();
expect(callback).not.toHaveBeenCalled();

expect(calendarCell).not.toBeVisible();
expect(calendarCell).not.toBeVisible();
});
});
});
Loading

0 comments on commit ef5a559

Please sign in to comment.