feat: add DatePicker

This commit is contained in:
Sergey Krylov 2024-04-29 11:42:46 +03:00
parent c950bc6757
commit 422bef601f
6 changed files with 647 additions and 0 deletions

View File

@ -2,4 +2,5 @@
display: flex;
justify-content: center;
align-items: center;
gap: 50px
}

View File

@ -1,4 +1,5 @@
import { CalendarWidget } from 'widgets/CalendarWidget';
import { DatePicker } from 'widgets/DatePicker';
import classes from './CalendarPage.module.css';
@ -10,6 +11,12 @@ const CalendarPage = () => (
<div className={classes.calendarWrapper}>
<CalendarWidget />
<DatePicker
firstDate={new Date(2024, 9, 15)}
lastDate={new Date(2024, 9, 18)}
value={new Date(2024, 9, 15)}
/>
</div>
</div>
);

View File

@ -0,0 +1 @@
export { default as DatePicker } from './ui/DatePicker.ui';

View File

@ -0,0 +1,360 @@
import { render, screen } from '@testing-library/react';
import { getDateCellByDataDate, pickCellByDataDate } from 'shared/utils/spec-helpers/date';
import DatePicker from './DatePicker.ui';
import type { DatePickerProps } from './DatePicker.ui';
describe('Test DatePicker', () => {
const setup = (props?: DatePickerProps) => render(
<DatePicker {...props} />,
);
test('Basic render DatePicker', () => {
setup();
const datePickerElem = screen.queryByTestId('calendar-widget');
expect(datePickerElem).toBeVisible();
});
describe('Test base props', () => {
test('Default picked first date', () => {
setup({
firstDate: new Date(2024, 9, 11),
value: new Date(2024, 9, 15),
});
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).toBeVisible();
expect(firstPickedCellElem).toHaveAttribute('data-date', '10/11/2024');
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).not.toBeInTheDocument();
});
test('Default picked last date', () => {
setup({
lastDate: new Date(2024, 9, 18),
value: new Date(2024, 9, 15),
});
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).toBeVisible();
expect(lastPickedCellElem).toHaveAttribute('data-date', '10/18/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).not.toBeInTheDocument();
});
describe('onSelect handler', () => {
test('No default picked', () => {
const onSelect = jest.fn();
const { container } = setup({
onSelect,
value: new Date(2024, 9, 15),
});
pickCellByDataDate(container, '10/18/2024');
expect(onSelect).toHaveBeenCalled();
expect(onSelect).toHaveBeenCalledWith({
dataDate: '10/18/2024',
date: new Date('10/18/2024'),
type: 'firstDate',
});
});
test('Picked first date', () => {
const onSelect = jest.fn();
const { container } = setup({
onSelect,
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
});
pickCellByDataDate(container, '10/18/2024');
expect(onSelect).toHaveBeenCalled();
expect(onSelect).toHaveBeenCalledWith({
dataDate: '10/18/2024',
date: new Date('10/18/2024'),
type: 'lastDate',
});
});
test('Picked last date', () => {
const onSelect = jest.fn();
const { container } = setup({
onSelect,
value: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 15),
});
pickCellByDataDate(container, '10/11/2024');
expect(onSelect).toHaveBeenCalled();
expect(onSelect).toHaveBeenCalledWith({
dataDate: '10/11/2024',
date: new Date('10/11/2024'),
type: 'firstDate',
});
});
});
describe('onUnselect handler', () => {
test('Unpick first date', () => {
const onUnselect = jest.fn();
const { container } = setup({
onUnselect,
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 18),
});
pickCellByDataDate(container, '10/15/2024');
expect(onUnselect).toHaveBeenCalled();
expect(onUnselect).toHaveBeenCalledWith({
dataDate: '10/15/2024',
date: new Date('10/15/2024'),
type: 'firstDate',
});
});
test('Unpick last date', () => {
const onUnselect = jest.fn();
const { container } = setup({
onUnselect,
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 18),
});
pickCellByDataDate(container, '10/18/2024');
expect(onUnselect).toHaveBeenCalled();
expect(onUnselect).toHaveBeenCalledWith({
dataDate: '10/18/2024',
date: new Date('10/18/2024'),
type: 'lastDate',
});
});
});
});
describe('Test pick date', () => {
test('No picked date, select first', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
});
const cellElem = getDateCellByDataDate(container, '10/11/2024');
pickCellByDataDate(container, '10/11/2024');
const firstSelectedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstSelectedCellElem).toBe(cellElem);
});
test('Picked first date, pick before first date', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
});
pickCellByDataDate(container, '10/11/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).toHaveAttribute('data-date', '10/11/2024');
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
});
test('Picked first date, pick after first date', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
});
pickCellByDataDate(container, '10/18/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).toHaveAttribute('data-date', '10/18/2024');
});
test('Picked first date, pick this date', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
});
pickCellByDataDate(container, '10/15/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).not.toBeInTheDocument();
});
test('Picked last date, pick before last date', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 15),
});
pickCellByDataDate(container, '10/11/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).toHaveAttribute('data-date', '10/11/2024');
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
});
test('Picked last date, pick after last date', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 15),
});
pickCellByDataDate(container, '10/18/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).toHaveAttribute('data-date', '10/18/2024');
});
test('Picked last date, pick this date', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 15),
});
pickCellByDataDate(container, '10/15/2024');
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).not.toBeInTheDocument();
});
test('Picked first & last date, pick date before first', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 18),
});
pickCellByDataDate(container, '10/11/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).toHaveAttribute('data-date', '10/11/2024');
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).toHaveAttribute('data-date', '10/18/2024');
});
test('Picked first & last date, pick date after last', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 18),
});
pickCellByDataDate(container, '10/21/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).toHaveAttribute('data-date', '10/21/2024');
});
test('Picked first & last date, pick fist date', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 18),
});
pickCellByDataDate(container, '10/15/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).not.toBeInTheDocument();
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).toHaveAttribute('data-date', '10/18/2024');
});
test('Picked first & last date, pick last date', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 18),
});
pickCellByDataDate(container, '10/18/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).not.toBeInTheDocument();
});
test('Picked first & last date, pick the date closer to first date', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 18),
});
pickCellByDataDate(container, '10/16/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).toHaveAttribute('data-date', '10/16/2024');
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).toHaveAttribute('data-date', '10/18/2024');
});
test('Picked first & last date, pick the date closer to last date', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 18),
});
pickCellByDataDate(container, '10/17/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).toHaveAttribute('data-date', '10/17/2024');
});
test('Picked first & last date, pick the equidistant date', () => {
const { container } = setup({
value: new Date(2024, 9, 15),
firstDate: new Date(2024, 9, 15),
lastDate: new Date(2024, 9, 19),
});
pickCellByDataDate(container, '10/17/2024');
const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
expect(firstPickedCellElem).toHaveAttribute('data-date', '10/17/2024');
const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
expect(lastPickedCellElem).toHaveAttribute('data-date', '10/19/2024');
});
});
});

View File

@ -0,0 +1,16 @@
.firstDate {
background-color: blue;
color: white;
border-top-left-radius: 10px
}
.inSelection {
background: gold;
color: darkgrey;
}
.lastDate {
background-color: blue;
color: white;
border-bottom-right-radius: 10px
}

View File

@ -0,0 +1,262 @@
import classnames from 'classnames';
import { useState } from 'react';
import type { DataDateString } from 'shared/types/date';
import { daysCountBetween, getDataDateString } from 'shared/utils/date';
import { exhaustiveCheck } from 'shared/utils/type';
import { CalendarWidget } from '../../CalendarWidget';
import classes from './DatePicker.style.module.css';
import type { CalendarWidgetProps } from '../../CalendarWidget';
import type { FC } from 'react';
export type DatePickerProps = CalendarWidgetProps & {
firstDate?: Date;
lastDate?: Date;
onSelect?: (params: { dataDate: DataDateString; date: Date; type: 'firstDate' | 'lastDate' }) => void;
onUnselect?: (params: { dataDate: DataDateString; date: Date; type: 'firstDate' | 'lastDate' }) => void;
};
const DatePicker: FC<DatePickerProps> = (props) => {
const {
firstDate,
lastDate,
onSelect,
onUnselect,
...restWidgetProps
} = props;
const [firstSelectedDate, setFirstSelectedDate] = useState<DataDateString | null>(
firstDate
? getDataDateString(firstDate)
: null,
);
const [lastSelectedDate, setLastSelectedDate] = useState<DataDateString | null>(
lastDate
? getDataDateString(lastDate)
: null,
);
const isSelectedFirstDate = firstSelectedDate !== null;
const isSelectedLastDate = lastSelectedDate !== null;
const isSelectedBothDate = isSelectedFirstDate && isSelectedLastDate;
const isCellInSelection = (dataDate: DataDateString) => {
if (!isSelectedFirstDate || !isSelectedLastDate) {
return false;
}
const dateTime = new Date(dataDate).getTime();
const firstDateTime = new Date(firstSelectedDate).getTime();
const lastDateTime = new Date(lastSelectedDate).getTime();
return dateTime >= firstDateTime && dateTime <= lastDateTime;
};
const toggleCellDate = (params: { dataDate: DataDateString | null; type: 'firstDate' | 'lastDate' }) => {
const { dataDate, type } = params;
if (dataDate === null) {
let typedDataDate: DataDateString;
switch (type) {
case 'firstDate':
if (!isSelectedFirstDate) {
return;
}
typedDataDate = firstSelectedDate;
setFirstSelectedDate(null);
break;
case 'lastDate':
if (!isSelectedLastDate) {
return;
}
typedDataDate = lastSelectedDate;
setLastSelectedDate(null);
break;
default:
exhaustiveCheck(type);
break;
}
onUnselect?.({
type,
// @ts-ignore
dataDate: typedDataDate,
// @ts-ignore
date: new Date(typedDataDate),
});
return;
}
switch (type) {
case 'firstDate':
setFirstSelectedDate(dataDate);
break;
case 'lastDate':
setLastSelectedDate(dataDate);
break;
default:
exhaustiveCheck(type);
break;
}
onSelect?.({
type,
dataDate,
date: new Date(dataDate),
});
};
const toggleCellSelection = (dataDate: DataDateString) => {
if (!isSelectedFirstDate && !isSelectedLastDate) {
toggleCellDate({ dataDate, type: 'firstDate' });
return;
}
if (isSelectedFirstDate && !isSelectedLastDate) {
if (dataDate < firstSelectedDate) {
toggleCellDate({ dataDate: firstSelectedDate, type: 'lastDate' });
toggleCellDate({ dataDate, type: 'firstDate' });
return;
}
if (dataDate > firstSelectedDate) {
toggleCellDate({ dataDate, type: 'lastDate' });
return;
}
toggleCellDate({ dataDate: null, type: 'firstDate' });
}
if (!isSelectedFirstDate && isSelectedLastDate) {
if (dataDate < lastSelectedDate) {
toggleCellDate({ dataDate, type: 'firstDate' });
return;
}
if (dataDate > lastSelectedDate) {
toggleCellDate({ dataDate: lastSelectedDate, type: 'firstDate' });
toggleCellDate({ dataDate, type: 'lastDate' });
return;
}
toggleCellDate({ dataDate: null, type: 'lastDate' });
}
if (isSelectedBothDate) {
if (dataDate < firstSelectedDate) {
toggleCellDate({ dataDate, type: 'firstDate' });
return;
}
if (dataDate > lastSelectedDate) {
toggleCellDate({ dataDate, type: 'lastDate' });
return;
}
if (dataDate === firstSelectedDate) {
toggleCellDate({ dataDate: null, type: 'firstDate' });
return;
}
if (dataDate === lastSelectedDate) {
toggleCellDate({ dataDate: null, type: 'lastDate' });
return;
}
const daysToFirstDate = daysCountBetween(new Date(firstSelectedDate), new Date(dataDate));
const daysToLastDate = daysCountBetween(new Date(lastSelectedDate), new Date(dataDate));
if (daysToFirstDate < daysToLastDate) {
toggleCellDate({ dataDate, type: 'firstDate' });
return;
}
if (daysToFirstDate > daysToLastDate) {
toggleCellDate({ dataDate, type: 'lastDate' });
return;
}
toggleCellDate({ dataDate, type: 'firstDate' });
}
};
const isFirstDate = (dataDate: DataDateString) => dataDate === firstSelectedDate;
const isLastDate = (dataDate: DataDateString) => dataDate === lastSelectedDate;
const getDataTestId = (dataDate: DataDateString) => {
if (isFirstDate(dataDate)) {
return {
'data-testid': 'first-selected-cell',
};
}
if (isLastDate(dataDate)) {
return {
'data-testid': 'last-selected-cell',
};
}
if (isCellInSelection(dataDate)) {
return {
'data-testid': 'cell-in-range',
};
}
return {};
};
return (
<div>
<button
type="button"
onClick={() => {
setLastSelectedDate(null);
setFirstSelectedDate(null);
}}
onKeyPress={() => {}}
>
reset
</button>
<CalendarWidget
DateCellProps={({ dataDate }) => ({
onClick: () => {
toggleCellSelection(dataDate);
},
className: classnames({
[classes.firstDate as string]: isFirstDate(dataDate),
[classes.inSelection as string]: isCellInSelection(dataDate)
&& !isFirstDate(dataDate)
&& !isLastDate(dataDate),
[classes.lastDate as string]: isLastDate(dataDate),
}),
...getDataTestId(dataDate),
})}
{...restWidgetProps}
/>
</div>
);
};
export default DatePicker;