feat: add calendar widget

This commit is contained in:
Sergey Krylov 2024-04-22 09:24:46 +03:00
parent 326d503f7f
commit 078e2c987f
43 changed files with 1281 additions and 0 deletions

View File

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

View File

@ -0,0 +1,4 @@
.wrapper {
border: 1px solid black;
width: 372px;
}

View File

@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import classes from './CalendarWidget.module.css';
import { CalendarWidgetProvider } from './CalendarWidgetContext';
import { DateWrapper } from './components/DateWrapper';
import { DayOfWeekRow } from './components/DayOfWeekRow';
import { HeaderRow } from './components/HeaderRow';
const CalendarWidget = (props) => (
<CalendarWidgetProvider {...props}>
<div className={classes.wrapper}>
<HeaderRow />
<DayOfWeekRow />
<DateWrapper />
</div>
</CalendarWidgetProvider>
);
CalendarWidget.propTypes = {
maxDate: PropTypes.instanceOf(Date),
minDate: PropTypes.instanceOf(Date),
value: PropTypes.instanceOf(Date),
};
export default CalendarWidget;

View File

@ -0,0 +1,41 @@
import PropTypes from 'prop-types';
import { createContext, useMemo, useState } from 'react';
export const CalendarWidgetContext = createContext({});
export const CalendarWidgetProvider = (props) => {
const {
children,
maxDate = new Date(2030, 0, 1),
minDate = new Date(1990, 0, 1),
locale = 'ru',
...restProps
} = props;
const [targetDate, setTargetDate] = useState(new Date());
const contextValue = useMemo(() => ({
targetYear: targetDate.getFullYear(),
targetMonthIndex: targetDate.getMonth(),
targetDate,
setTargetDate,
minDate,
maxDate,
locale,
...restProps,
}), [maxDate, minDate, targetDate, locale, restProps]);
return (
<CalendarWidgetContext.Provider value={contextValue}>
{children}
</CalendarWidgetContext.Provider>
);
};
CalendarWidgetProvider.propTypes = {
children: PropTypes.node.isRequired,
maxDate: PropTypes.instanceOf(Date),
minDate: PropTypes.instanceOf(Date),
locale: PropTypes.oneOf(['ru', 'en']),
};

View File

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

View File

@ -0,0 +1,37 @@
import { render, screen } from '@testing-library/react';
import DateCell from './DateCell.ui';
describe('Base Render', () => {
const setup = (date) => {
render(<DateCell date={date} />);
};
test('Check base render', () => {
setup(new Date(2024, 3, 15));
const dateCellElem = screen.getByTestId('date-cell');
expect(dateCellElem).toBeInTheDocument();
});
test('Check base class', () => {
setup(new Date(2024, 3, 15));
const dateCellElem = screen.getByTestId('date-cell');
expect(dateCellElem).toHaveClass('dateCell');
});
test('Is correct date', () => {
setup(new Date(2024, 3, 15));
const dateCellElem = screen.getByTestId('date-cell');
expect(dateCellElem).toHaveTextContent('15');
});
test('Have data-date attribute', () => {
setup(new Date(2024, 3, 15));
const dateCellElem = screen.getByTestId('date-cell');
expect(dateCellElem).toHaveAttribute('data-date', '4/15/2024');
});
});

View File

@ -0,0 +1,28 @@
.dateCell {
width: calc((100% / 7));
height: 40px;
background: aquamarine;
display: flex;
justify-content: center;
align-items: center;
&:nth-child(odd):not(:last-child) {
width: calc((100% / 7) - 1px);
border-right: 1px solid black;
}
&:nth-child(even) {
width: calc((100% / 7) - 1px);
border-right: 1px solid black;
}
&.today {
color: red;
font-size: 1.2em;
font-weight: bold;
}
&.outMothRange {
color: darkgrey;
}
}

View File

@ -0,0 +1,44 @@
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { CalendarWidgetContext } from '../../../CalendarWidgetContext';
import classes from './DateCell.style.module.css';
const DateCell = (props) => {
const {
date,
} = props;
const format = 'en-En';
const { targetMonthIndex } = useContext(CalendarWidgetContext);
const todayDate = new Date();
const dataDate = new Intl.DateTimeFormat(format).format(date);
const dataToday = new Intl.DateTimeFormat(format).format(todayDate);
const isOutMonthRange = date.getMonth() !== targetMonthIndex;
return (
<div
key={dataDate}
className={classnames(
{
[classes.dateCell]: true,
[classes.today]: dataDate === dataToday,
[classes.outMothRange]: isOutMonthRange,
},
)}
data-date={dataDate}
data-testid="date-cell"
>
{date.getDate()}
</div>
);
};
DateCell.propTypes = {
date: PropTypes.instanceOf(Date).isRequired,
};
export default DateCell;

View File

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

View File

@ -0,0 +1,65 @@
import { render, screen } from '@testing-library/react';
import DateRow from './DateRow.ui';
describe('Test Date Row', () => {
const setup = (cells) => {
render(<DateRow cells={cells} />);
};
const setupWeek = () => {
const cells = [
new Date(2024, 3, 15),
new Date(2024, 3, 16),
new Date(2024, 3, 17),
new Date(2024, 3, 18),
new Date(2024, 3, 19),
new Date(2024, 3, 20),
new Date(2024, 3, 21),
];
setup(cells);
};
test('base render', () => {
setupWeek();
const dateRowElem = screen.getByTestId('date-row');
expect(dateRowElem).toBeInTheDocument();
});
test('Correct data attribute', () => {
setupWeek();
const dateRowElem = screen.getByTestId('date-row');
expect(dateRowElem).toHaveAttribute('data-row', '4/15/2024-4/21/2024');
});
test('Correct style class', () => {
setupWeek();
const dateRowElem = screen.getByTestId('date-row');
expect(dateRowElem).toHaveClass('dateRow');
});
describe('Test cells', () => {
test('base render', () => {
setupWeek();
const dateCellElems = screen.getAllByTestId('date-cell');
expect(dateCellElems).toHaveLength(7);
});
test('is correct cells', () => {
setupWeek();
const dateCellElems = screen.getAllByTestId('date-cell');
expect(dateCellElems[0]).toHaveTextContent(15);
expect(dateCellElems[1]).toHaveTextContent(16);
expect(dateCellElems[2]).toHaveTextContent(17);
expect(dateCellElems[3]).toHaveTextContent(18);
expect(dateCellElems[4]).toHaveTextContent(19);
expect(dateCellElems[5]).toHaveTextContent(20);
expect(dateCellElems[6]).toHaveTextContent(21);
});
});
});

View File

@ -0,0 +1,10 @@
.dateRow {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
&:not(:last-child) {
border-bottom: 1px solid black;
}
}

View File

@ -0,0 +1,35 @@
import PropTypes from 'prop-types';
import { DateCell } from '../../DateCell';
import classes from './DateRow.style.module.css';
const DateRow = (props) => {
const {
cells,
} = props;
const format = 'en-En';
const firstDate = new Intl.DateTimeFormat(format).format(cells[0]);
const lastDate = new Intl.DateTimeFormat(format).format(cells[cells.length - 1]);
return (
<div
key={`${firstDate}-${lastDate}`}
className={classes.dateRow}
data-row={`${firstDate}-${lastDate}`}
data-testid="date-row"
>
{cells.map((item) => (
<DateCell key={item.toString()} date={item} />
))}
</div>
);
};
DateRow.propTypes = {
cells: PropTypes.arrayOf(PropTypes.instanceOf(Date)),
};
export default DateRow;

View File

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

View File

@ -0,0 +1,24 @@
import { render, screen } from '@testing-library/react';
import { CalendarWidgetProvider } from 'widgets/CalendarWidget/ui/CalendarWidgetContext';
import { withProvider } from 'shared/hoc/withProvider';
import DateWrapper from './DateWrapper.ui';
describe('Base render DateWrapper', () => {
const setup = () => {
const DateWrapperWithProvider = withProvider(CalendarWidgetProvider)(DateWrapper);
render(<DateWrapperWithProvider />);
};
test('Base render', () => {
setup();
const dateWrapperElem = screen.getByTestId('date-wrapper');
expect(dateWrapperElem).toBeInTheDocument();
expect(dateWrapperElem).toHaveClass('dateWrapper');
});
});

View File

@ -0,0 +1,3 @@
.dateWrapper {
border-top: 1px solid black;
}

View File

@ -0,0 +1,61 @@
import { useCallback, useContext } from 'react';
import DateRow from 'widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.ui';
import { DAYS_IN_WEEK } from 'shared/constants';
// todo fix eslint - no empty line
import { CalendarWidgetContext } from '../../../CalendarWidgetContext';
import classes from './DateWrapper.styles.module.css';
const DateWrapper = () => {
const { targetYear, targetMonthIndex } = useContext(CalendarWidgetContext);
const renderDateWrapper = useCallback(() => {
const rowsCount = 6;
const days = [];
const lastDatePrevMonth = new Date(targetYear, targetMonthIndex, 0);
const startOffset = lastDatePrevMonth.getDay();
if (startOffset) {
for (let i = 0; i < startOffset; i++) {
const newDate = new Date(targetYear, targetMonthIndex, i - startOffset + 1);
days.push(newDate);
}
}
const daysPerMonth = new Date(targetYear, targetMonthIndex + 1, 0).getDate();
// eslint-disable-next-line @stylistic/padding-line-between-statements
for (let i = 1; i <= daysPerMonth; i++) {
const newDate = new Date(targetYear, targetMonthIndex, i);
days.push(newDate);
}
const endOffset = rowsCount * DAYS_IN_WEEK - days.length;
if (endOffset) {
for (let i = 1; i <= endOffset; i++) {
const newDate = new Date(targetYear, targetMonthIndex + 1, i);
days.push(newDate);
}
}
const rows = [];
// eslint-disable-next-line @stylistic/padding-line-between-statements
for (let i = 0; i < rowsCount; i++) {
rows.push(days.slice(i * 7, (i + 1) * 7));
}
return rows.map((item) => (
<DateRow key={item.toString()} cells={item} />
));
}, [targetMonthIndex, targetYear]);
return (
<div className={classes.dateWrapper} data-testid="date-wrapper">
{renderDateWrapper()}
</div>
);
};
export default DateWrapper;

View File

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

View File

@ -0,0 +1,136 @@
import { render, screen } from '@testing-library/react';
import { WEEK_DAYS } from 'shared/constants';
import { withProvider } from 'shared/hoc/withProvider';
import { CalendarWidgetProvider } from '../../../CalendarWidgetContext';
import DayOfWeekCell from './DayOfWeekCell.ui';
describe('Test DayOfWeekCell', () => {
const setup = (day, locale) => {
const DayOfWeekCellComponent = () => (
<DayOfWeekCell day={day} />
);
const Provider = (props) => (
<CalendarWidgetProvider {...props} locale={locale} />
);
const DayOfWeekCellWithProvider = withProvider(Provider)(DayOfWeekCellComponent);
render(<DayOfWeekCellWithProvider />);
};
describe('Basic render sunday', () => {
test('en locale', () => {
setup(WEEK_DAYS.SUNDAY, 'en');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Sun.');
});
test('ru locale', () => {
setup(WEEK_DAYS.SUNDAY, 'ru');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Вс.');
});
});
describe('Basic render monday', () => {
test('en locale', () => {
setup(WEEK_DAYS.MONDAY, 'en');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Mon.');
});
test('ru locale', () => {
setup(WEEK_DAYS.MONDAY, 'ru');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Пн.');
});
});
describe('Basic render tuesday', () => {
test('en locale', () => {
setup(WEEK_DAYS.TUESDAY, 'en');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Tue.');
});
test('ru locale', () => {
setup(WEEK_DAYS.TUESDAY, 'ru');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Вт.');
});
});
describe('Basic render wednesday', () => {
test('en locale', () => {
setup(WEEK_DAYS.WEDNESDAY, 'en');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Wed.');
});
test('ru locale', () => {
setup(WEEK_DAYS.WEDNESDAY, 'ru');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Ср.');
});
});
describe('Basic render thursday', () => {
test('en locale', () => {
setup(WEEK_DAYS.THURSDAY, 'en');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Thu.');
});
test('ru locale', () => {
setup(WEEK_DAYS.THURSDAY, 'ru');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Чт.');
});
});
describe('Basic render friday', () => {
test('en locale', () => {
setup(WEEK_DAYS.FRIDAY, 'en');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Fri.');
});
test('ru locale', () => {
setup(WEEK_DAYS.FRIDAY, 'ru');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Пт.');
});
});
describe('Basic render saturday', () => {
test('en locale', () => {
setup(WEEK_DAYS.SATURDAY, 'en');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Sat.');
});
test('ru locale', () => {
setup(WEEK_DAYS.SATURDAY, 'ru');
const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell');
expect(dayOfWeekCellElem).toHaveTextContent('Сб.');
});
});
});

View File

@ -0,0 +1,4 @@
.content {
max-width: 100%;
font-weight: 600;
}

View File

@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { WEEK_DAYS_LABEL, WEEK_DAYS } from 'shared/constants';
import { CalendarWidgetContext } from '../../../CalendarWidgetContext';
import classes from './DayOfWeekCell.style.module.css';
const DayOfWeekCell = (props) => {
const { day } = props;
const { locale } = useContext(CalendarWidgetContext);
const content = WEEK_DAYS_LABEL[day][locale]?.short;
return (
<span className={classes.content} data-testid="day-of-week-cell">
{content}
.
</span>
);
};
DayOfWeekCell.propTypes = {
day: PropTypes.oneOf(Object.values(WEEK_DAYS)).isRequired,
};
export default DayOfWeekCell;

View File

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

View File

@ -0,0 +1,29 @@
import { render, screen } from '@testing-library/react';
import { withProvider } from 'shared/hoc/withProvider';
import { CalendarWidgetProvider } from '../../../CalendarWidgetContext';
import DayOfWeekRow from './DayOfWeekRow.ui';
describe('Test DayOfWeekRow', () => {
const setup = () => {
const DayOfWeekRowWithProvider = withProvider(CalendarWidgetProvider)(DayOfWeekRow);
render(<DayOfWeekRowWithProvider />);
};
test('Basic render row', () => {
setup();
const dayOfWeekRowElem = screen.getByTestId('day-of-week-row');
expect(dayOfWeekRowElem).toBeInTheDocument();
});
test('Basic render cells', () => {
setup();
const dayOfWeekCellElems = screen.getAllByTestId('day-of-week-cell');
expect(dayOfWeekCellElems).toHaveLength(7);
});
});

View File

@ -0,0 +1,24 @@
.dayOfWeekRow {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid black;
>div {
width: calc(100% / 7 - 1px);
max-width: calc(100% / 7 - 1px);
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
&:last-child {
width: calc(100% / 7);
max-width: calc(100% / 7);
}
&:not(:last-child) {
border-right: 1px solid black;
}
}
}

View File

@ -0,0 +1,39 @@
import { WEEK_DAYS } from 'shared/constants';
import { DayOfWeekCell } from '../../DayOfWeekCell';
import classes from './DayOfWeekRow.style.module.css';
const DayOfWeekRow = () => (
<div className={classes.dayOfWeekRow} data-testid="day-of-week-row">
<div>
<DayOfWeekCell day={WEEK_DAYS.MONDAY} />
</div>
<div>
<DayOfWeekCell day={WEEK_DAYS.TUESDAY} />
</div>
<div>
<DayOfWeekCell day={WEEK_DAYS.WEDNESDAY} />
</div>
<div>
<DayOfWeekCell day={WEEK_DAYS.THURSDAY} />
</div>
<div>
<DayOfWeekCell day={WEEK_DAYS.FRIDAY} />
</div>
<div>
<DayOfWeekCell day={WEEK_DAYS.SATURDAY} />
</div>
<div>
<DayOfWeekCell day={WEEK_DAYS.SUNDAY} />
</div>
</div>
);
export default DayOfWeekRow;

View File

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

View File

@ -0,0 +1,77 @@
import { render, screen } from '@testing-library/react';
import { withProvider } from 'shared/hoc/withProvider';
import { CalendarWidgetProvider } from '../../../CalendarWidgetContext';
import HeaderRow from './HeaderRow.ui';
describe('Test HeaderRow', () => {
const setup = () => {
const HeaderRowWithProvider = withProvider(CalendarWidgetProvider)(HeaderRow);
render(<HeaderRowWithProvider />);
};
test('Basic render', () => {
setup();
const headerRowElem = screen.getByTestId('header-row');
expect(headerRowElem).toBeInTheDocument();
});
test('Render month select', () => {
setup();
const headerRowElem = screen.getByTestId('header-row');
const monthSelectElem = screen.getByTestId('month-select');
expect(headerRowElem).toContainElement(monthSelectElem);
expect(monthSelectElem).toBeInTheDocument();
expect(monthSelectElem).toBeVisible();
});
test('Render prev month button', () => {
setup();
const headerRowElem = screen.getByTestId('header-row');
const prevMonthButtonElem = screen.getByTestId('prev-month-button');
expect(headerRowElem).toContainElement(prevMonthButtonElem);
expect(prevMonthButtonElem).toBeInTheDocument();
expect(prevMonthButtonElem).toBeVisible();
});
test('Render today button', () => {
setup();
const headerRowElem = screen.getByTestId('header-row');
const todayButtonElem = screen.getByTestId('today-button');
expect(headerRowElem).toContainElement(todayButtonElem);
expect(todayButtonElem).toBeInTheDocument();
expect(todayButtonElem).toBeVisible();
});
test('Render next month button', () => {
setup();
const headerRowElem = screen.getByTestId('header-row');
const nextMonthButtonElem = screen.getByTestId('next-month-button');
expect(headerRowElem).toContainElement(nextMonthButtonElem);
expect(nextMonthButtonElem).toBeInTheDocument();
expect(nextMonthButtonElem).toBeVisible();
});
test('Render year select', () => {
setup();
const headerRowElem = screen.getByTestId('header-row');
const yearSelectElem = screen.getByTestId('year-select');
expect(headerRowElem).toContainElement(yearSelectElem);
expect(yearSelectElem).toBeInTheDocument();
expect(yearSelectElem).toBeVisible();
});
});

View File

@ -0,0 +1,9 @@
.headerRow {
display: flex;
justify-content: space-between;
align-items: center;
>div {
background: dimgrey;
}
}

View File

@ -0,0 +1,35 @@
import { MonthSelect } from '../../MonthSelect';
import { NextMonthButton } from '../../NextMonthButton';
import { PrevMonthButton } from '../../PrevMonthButton';
import { TodayButton } from '../../TodayButton';
import { YearSelect } from '../../YearSelect';
import classes from './HeaderRow.styles.module.css';
const HeaderRow = () => (
<div className={classes.headerRow} data-testid="header-row">
<div>
<MonthSelect />
</div>
<div>
<PrevMonthButton>
&lt;
</PrevMonthButton>
<TodayButton>
Сегодня
</TodayButton>
<NextMonthButton>
&gt;
</NextMonthButton>
</div>
<div>
<YearSelect />
</div>
</div>
);
export default HeaderRow;

View File

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

View File

@ -0,0 +1,52 @@
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { MONTHS } from 'shared/constants';
import { withProvider } from 'shared/hoc/withProvider';
import { CalendarWidgetProvider } from '../../../CalendarWidgetContext';
import MonthSelect from './MonthSelect.ui';
describe('Test MonthSelect', () => {
let user;
beforeEach(() => {
user = userEvent.setup();
});
const setup = (props) => {
const MonthSelectComponent = () => (
<MonthSelect {...props} />
);
const MonthSelectWithProvider = withProvider(CalendarWidgetProvider)(MonthSelectComponent);
render(<MonthSelectWithProvider />);
};
test('Basic render', async () => {
setup();
const monthSelectElem = screen.getByTestId('month-select');
expect(monthSelectElem).toBeInTheDocument();
expect(monthSelectElem).toBeVisible();
await user.selectOptions(monthSelectElem, String(MONTHS.JULY));
expect(monthSelectElem).toHaveValue('6');
expect(monthSelectElem).toHaveDisplayValue(['Июль']);
});
test('onChange handler', async () => {
const onChange = jest.fn();
setup({ onChange });
const monthSelectElem = screen.getByTestId('month-select');
await user.selectOptions(monthSelectElem, String(MONTHS.JULY));
expect(onChange).toHaveBeenCalled();
const newDate = new Date(2024, 6);
expect(onChange).toHaveBeenCalledWith(newDate);
});
});

View File

@ -0,0 +1,61 @@
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { MONTHS, MONTHS_LABEL } from 'shared/constants';
import { lastDayOfMonthDate } from 'shared/utils/date';
import { CalendarWidgetContext } from '../../../CalendarWidgetContext';
const MonthSelect = (props) => {
const {
onChange,
...restProps
} = props;
// todo eslint sort
const {
targetYear,
targetMonthIndex,
setTargetDate,
minDate,
maxDate,
locale,
} = useContext(CalendarWidgetContext);
const onChangeHandler = (e) => {
const newDate = new Date(targetYear, Number(e.target.value));
setTargetDate(newDate);
onChange?.(newDate);
};
const createOption = (value) => {
const newDate = new Date(targetYear, value);
return (
<option
key={value}
disabled={lastDayOfMonthDate(newDate) < minDate || newDate > maxDate}
value={value}
>
{MONTHS_LABEL[value][locale]}
</option>
);
};
return (
<select
data-testid="month-select"
name="month"
value={targetMonthIndex}
onChange={onChangeHandler}
{...restProps}
>
{Object.values(MONTHS).map(createOption)}
</select>
);
};
MonthSelect.propTypes = {
onChange: PropTypes.func,
};
export default MonthSelect;

View File

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

View File

@ -0,0 +1,43 @@
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { withProvider } from 'shared/hoc/withProvider';
import { CalendarWidgetProvider } from '../../../CalendarWidgetContext';
import NextMonthButton from './NextMonthButton.ui';
describe('Test NextMonthButton', () => {
let user;
beforeEach(() => {
user = userEvent.setup();
});
const setup = (props) => {
const NextMonthButtonComponent = () => (
<NextMonthButton {...props} />
);
const NextMonthButtonWithProvider = withProvider(CalendarWidgetProvider)(NextMonthButtonComponent);
render(<NextMonthButtonWithProvider />);
};
test('Basic render', () => {
setup();
const nextMontButtonElem = screen.getByTestId('next-month-button');
expect(nextMontButtonElem).toBeInTheDocument();
expect(nextMontButtonElem).toBeVisible();
});
test('onClick handler', async () => {
const onClick = jest.fn();
setup({ onClick });
const nextMontButtonElem = screen.getByTestId('next-month-button');
await user.click(nextMontButtonElem);
expect(onClick).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,46 @@
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { CalendarWidgetContext } from '../../../CalendarWidgetContext';
const NextMonthButton = (props) => {
const {
children = 'next',
disabled,
onClick,
...restProps
} = props;
const {
targetYear,
targetMonthIndex,
setTargetDate,
maxDate,
} = useContext(CalendarWidgetContext);
const nextDate = new Date(targetYear, targetMonthIndex + 1);
const onClickHandler = () => {
setTargetDate(nextDate);
onClick?.(nextDate);
};
return (
<button
data-testid="next-month-button"
disabled={disabled || maxDate < nextDate}
type="button"
onClick={onClickHandler}
{...restProps}
>
{children}
</button>
);
};
NextMonthButton.propTypes = {
children: PropTypes.node,
onClick: PropTypes.func,
disabled: PropTypes.bool,
};
export default NextMonthButton;

View File

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

View File

@ -0,0 +1,43 @@
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { withProvider } from 'shared/hoc/withProvider';
import { CalendarWidgetProvider } from '../../../CalendarWidgetContext';
import PrevMonthButton from './PrevMonthButton.ui';
describe('Test NextMonthButton', () => {
let user;
beforeEach(() => {
user = userEvent.setup();
});
const setup = (props) => {
const PrevMonthButtonComponent = () => (
<PrevMonthButton {...props} />
);
const PrevMonthButtonWithProvider = withProvider(CalendarWidgetProvider)(PrevMonthButtonComponent);
render(<PrevMonthButtonWithProvider />);
};
test('Basic render', () => {
setup();
const prevMontButtonElem = screen.getByTestId('prev-month-button');
expect(prevMontButtonElem).toBeInTheDocument();
expect(prevMontButtonElem).toBeVisible();
});
test('onClick handler', async () => {
const onClick = jest.fn();
setup({ onClick });
const prevMontButtonElem = screen.getByTestId('prev-month-button');
await user.click(prevMontButtonElem);
expect(onClick).toHaveBeenCalled();
});
});

View File

@ -0,0 +1,46 @@
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { CalendarWidgetContext } from '../../../CalendarWidgetContext';
const PrevMonthButton = (props) => {
const {
children = 'prev',
onClick,
disabled,
...restProps
} = props;
const {
targetYear,
targetMonthIndex,
setTargetDate,
minDate,
} = useContext(CalendarWidgetContext);
const prevDate = new Date(targetYear, targetMonthIndex, 0);
const onClickHandler = () => {
setTargetDate(prevDate);
onClick?.(prevDate);
};
return (
<button
data-testid="prev-month-button"
disabled={disabled || prevDate < minDate}
type="button"
onClick={onClickHandler}
{...restProps}
>
{children}
</button>
);
};
PrevMonthButton.propTypes = {
children: PropTypes.node,
onClick: PropTypes.func,
disabled: PropTypes.bool,
};
export default PrevMonthButton;

View File

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

View File

@ -0,0 +1,56 @@
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { withProvider } from 'shared/hoc/withProvider';
import { CalendarWidgetProvider } from '../../../CalendarWidgetContext';
import TodayButton from './TodayButton.ui';
describe('Test NextMonthButton', () => {
let user;
beforeEach(() => {
user = userEvent.setup();
});
const setup = (props) => {
const TodayButtonComponent = () => (
<TodayButton {...props} />
);
const TodayButtonWithProvider = withProvider(CalendarWidgetProvider)(TodayButtonComponent);
render(<TodayButtonWithProvider />);
};
test('Basic render', () => {
setup();
const todayButtonElem = screen.getByTestId('today-button');
expect(todayButtonElem).toBeInTheDocument();
expect(todayButtonElem).toBeVisible();
});
test('onClick handler', async () => {
const onClick = jest.fn();
setup({ onClick });
const todayButtonElem = screen.getByTestId('today-button');
await user.click(todayButtonElem);
expect(onClick).toHaveBeenCalled();
const onClickArgument = onClick.mock.calls[0][0];
const calledDate = new Date(onClickArgument);
const calledYear = calledDate.getFullYear();
const calledMonthIndex = calledDate.getMonth();
const calledDateNumber = calledDate.getDate();
const calledString = `${calledYear}-${calledMonthIndex}-${calledDateNumber}`;
const currentYear = new Date().getFullYear();
const currentMonthIndex = new Date().getMonth();
const currentDateNumber = new Date().getDate();
const expectedString = `${currentYear}-${currentMonthIndex}-${currentDateNumber}`;
expect(calledString).toBe(expectedString);
});
});

View File

@ -0,0 +1,38 @@
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { CalendarWidgetContext } from '../../../CalendarWidgetContext';
const TodayButton = (props) => {
const {
children = 'today',
onClick,
...restProps
} = props;
const { setTargetDate } = useContext(CalendarWidgetContext);
const onClickHandler = () => {
const newDate = new Date();
setTargetDate(newDate);
onClick?.(newDate);
};
return (
<button
data-testid="today-button"
type="button"
onClick={onClickHandler}
{...restProps}
>
{children}
</button>
);
};
TodayButton.propTypes = {
children: PropTypes.node,
onClick: PropTypes.func,
};
export default TodayButton;

View File

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

View File

@ -0,0 +1,53 @@
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { withProvider } from 'shared/hoc/withProvider';
import { CalendarWidgetProvider } from '../../../CalendarWidgetContext';
import YearSelect from './YearSelect.ui';
describe('Test YearSelect', () => {
let user;
beforeEach(() => {
user = userEvent.setup();
});
const setup = (props) => {
const YearSelectComponent = () => (
<YearSelect {...props} />
);
const YearSelectWithProvider = withProvider(CalendarWidgetProvider)(YearSelectComponent);
render(<YearSelectWithProvider />);
};
test('Basic render', async () => {
setup();
const yearSelectElem = screen.getByTestId('year-select');
expect(yearSelectElem).toBeInTheDocument();
expect(yearSelectElem).toBeVisible();
await user.selectOptions(yearSelectElem, '2020');
expect(yearSelectElem).toHaveValue('2020');
expect(yearSelectElem).toHaveDisplayValue(['2020']);
});
test('onChange handler', async () => {
const onChange = jest.fn();
setup({ onChange });
const yearSelectElem = screen.getByTestId('year-select');
await user.selectOptions(yearSelectElem, '2020');
expect(onChange).toHaveBeenCalled();
const currentDate = new Date();
const currentMonthIndex = currentDate.getMonth();
const newDate = new Date(2020, currentMonthIndex);
expect(onChange).toHaveBeenCalledWith(newDate);
});
});

View File

@ -0,0 +1,72 @@
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { CalendarWidgetContext } from '../../../CalendarWidgetContext';
const YearSelect = (props) => {
const {
onChange,
...restProps
} = props;
// todo eslint fix
const {
targetYear, targetMonthIndex, setTargetDate, minDate, maxDate,
} = useContext(CalendarWidgetContext);
const onChangeHandler = (event) => {
let newDate = new Date(event.target.value, targetMonthIndex);
if (newDate < minDate) {
newDate = minDate;
}
if (newDate > maxDate) {
newDate = maxDate;
}
setTargetDate(newDate);
onChange?.(newDate);
};
const renderOptions = () => {
const minYear = minDate.getFullYear();
const maxYear = maxDate.getFullYear();
const options = [];
for (let i = minYear; i <= maxYear; i++) {
options.push(i);
}
return options.map((item) => {
// todo refactor
const newDate = new Date(item, targetMonthIndex + 1, 0);
const newDate2 = new Date(item, targetMonthIndex);
const disabled = newDate2 > maxDate || newDate < minDate;
return (
<option key={item} disabled={disabled} value={item}>
{item}
</option>
);
});
};
return (
<select
data-testid="year-select"
id=""
name="year"
value={targetYear}
onChange={onChangeHandler}
{...restProps}
>
{renderOptions()}
</select>
);
};
YearSelect.propTypes = {
onChange: PropTypes.func,
};
export default YearSelect;