diff --git a/src/widgets/CalendarWidget/index.js b/src/widgets/CalendarWidget/index.js
new file mode 100644
index 0000000..548da58
--- /dev/null
+++ b/src/widgets/CalendarWidget/index.js
@@ -0,0 +1 @@
+export { default as CalendarWidget } from './ui/CalendarWidget.ui';
diff --git a/src/widgets/CalendarWidget/ui/CalendarWidget.module.css b/src/widgets/CalendarWidget/ui/CalendarWidget.module.css
new file mode 100644
index 0000000..9d367e1
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/CalendarWidget.module.css
@@ -0,0 +1,4 @@
+.wrapper {
+ border: 1px solid black;
+ width: 372px;
+}
diff --git a/src/widgets/CalendarWidget/ui/CalendarWidget.ui.jsx b/src/widgets/CalendarWidget/ui/CalendarWidget.ui.jsx
new file mode 100644
index 0000000..92fa43a
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/CalendarWidget.ui.jsx
@@ -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) => (
+
+
+
+
+
+
+
+
+
+);
+
+CalendarWidget.propTypes = {
+ maxDate: PropTypes.instanceOf(Date),
+ minDate: PropTypes.instanceOf(Date),
+ value: PropTypes.instanceOf(Date),
+};
+
+export default CalendarWidget;
diff --git a/src/widgets/CalendarWidget/ui/CalendarWidgetContext.jsx b/src/widgets/CalendarWidget/ui/CalendarWidgetContext.jsx
new file mode 100644
index 0000000..92ae75c
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/CalendarWidgetContext.jsx
@@ -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 (
+
+ {children}
+
+ );
+};
+
+CalendarWidgetProvider.propTypes = {
+ children: PropTypes.node.isRequired,
+
+ maxDate: PropTypes.instanceOf(Date),
+ minDate: PropTypes.instanceOf(Date),
+ locale: PropTypes.oneOf(['ru', 'en']),
+};
diff --git a/src/widgets/CalendarWidget/ui/components/DateCell/index.js b/src/widgets/CalendarWidget/ui/components/DateCell/index.js
new file mode 100644
index 0000000..ab0a98b
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DateCell/index.js
@@ -0,0 +1 @@
+export { default as DateCell } from './ui/DateCell.ui';
diff --git a/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.spec.jsx b/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.spec.jsx
new file mode 100644
index 0000000..ac52c45
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.spec.jsx
@@ -0,0 +1,37 @@
+import { render, screen } from '@testing-library/react';
+
+import DateCell from './DateCell.ui';
+
+describe('Base Render', () => {
+ const setup = (date) => {
+ render();
+ };
+
+ 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');
+ });
+});
diff --git a/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.style.module.css b/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.style.module.css
new file mode 100644
index 0000000..33002eb
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.style.module.css
@@ -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;
+ }
+}
diff --git a/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.ui.jsx b/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.ui.jsx
new file mode 100644
index 0000000..26d5b2a
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.ui.jsx
@@ -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 (
+
+ {date.getDate()}
+
+ );
+};
+
+DateCell.propTypes = {
+ date: PropTypes.instanceOf(Date).isRequired,
+};
+
+export default DateCell;
diff --git a/src/widgets/CalendarWidget/ui/components/DateRow/index.js b/src/widgets/CalendarWidget/ui/components/DateRow/index.js
new file mode 100644
index 0000000..f2df3ce
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DateRow/index.js
@@ -0,0 +1 @@
+export { default as DateRow } from './ui/DateRow.ui';
diff --git a/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.spec.jsx b/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.spec.jsx
new file mode 100644
index 0000000..7371937
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.spec.jsx
@@ -0,0 +1,65 @@
+import { render, screen } from '@testing-library/react';
+
+import DateRow from './DateRow.ui';
+
+describe('Test Date Row', () => {
+ const setup = (cells) => {
+ render();
+ };
+
+ 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);
+ });
+ });
+});
diff --git a/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.style.module.css b/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.style.module.css
new file mode 100644
index 0000000..7e1bb92
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.style.module.css
@@ -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;
+ }
+}
diff --git a/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.ui.jsx b/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.ui.jsx
new file mode 100644
index 0000000..7c6acc2
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.ui.jsx
@@ -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 (
+
+ {cells.map((item) => (
+
+ ))}
+
+ );
+};
+
+DateRow.propTypes = {
+ cells: PropTypes.arrayOf(PropTypes.instanceOf(Date)),
+};
+
+export default DateRow;
diff --git a/src/widgets/CalendarWidget/ui/components/DateWrapper/index.js b/src/widgets/CalendarWidget/ui/components/DateWrapper/index.js
new file mode 100644
index 0000000..e38158d
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DateWrapper/index.js
@@ -0,0 +1 @@
+export { default as DateWrapper } from './ui/DateWrapper.ui';
diff --git a/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.spec.jsx b/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.spec.jsx
new file mode 100644
index 0000000..8dc3855
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.spec.jsx
@@ -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();
+ };
+
+ test('Base render', () => {
+ setup();
+
+ const dateWrapperElem = screen.getByTestId('date-wrapper');
+ expect(dateWrapperElem).toBeInTheDocument();
+
+ expect(dateWrapperElem).toHaveClass('dateWrapper');
+ });
+});
diff --git a/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.styles.module.css b/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.styles.module.css
new file mode 100644
index 0000000..a4b7e8f
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.styles.module.css
@@ -0,0 +1,3 @@
+.dateWrapper {
+ border-top: 1px solid black;
+}
diff --git a/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.ui.jsx b/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.ui.jsx
new file mode 100644
index 0000000..fcdbff0
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.ui.jsx
@@ -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) => (
+
+ ));
+ }, [targetMonthIndex, targetYear]);
+
+ return (
+
+ {renderDateWrapper()}
+
+ );
+};
+
+export default DateWrapper;
diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/index.js b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/index.js
new file mode 100644
index 0000000..5476336
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/index.js
@@ -0,0 +1 @@
+export { default as DayOfWeekCell } from './ui/DayOfWeekCell.ui';
diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.spec.jsx b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.spec.jsx
new file mode 100644
index 0000000..357d0df
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.spec.jsx
@@ -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 = () => (
+
+ );
+
+ const Provider = (props) => (
+
+ );
+
+ const DayOfWeekCellWithProvider = withProvider(Provider)(DayOfWeekCellComponent);
+
+ render();
+ };
+
+ 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('Сб.');
+ });
+ });
+});
diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.style.module.css b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.style.module.css
new file mode 100644
index 0000000..c10bbc9
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.style.module.css
@@ -0,0 +1,4 @@
+.content {
+ max-width: 100%;
+ font-weight: 600;
+}
diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.ui.jsx b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.ui.jsx
new file mode 100644
index 0000000..0dd4836
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.ui.jsx
@@ -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 (
+
+ {content}
+ .
+
+ );
+};
+
+DayOfWeekCell.propTypes = {
+ day: PropTypes.oneOf(Object.values(WEEK_DAYS)).isRequired,
+};
+
+export default DayOfWeekCell;
diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/index.js b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/index.js
new file mode 100644
index 0000000..238cd25
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/index.js
@@ -0,0 +1 @@
+export { default as DayOfWeekRow } from './ui/DayOfWeekRow.ui';
diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.spec.jsx b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.spec.jsx
new file mode 100644
index 0000000..7fd1783
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.spec.jsx
@@ -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();
+ };
+
+ 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);
+ });
+});
diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.style.module.css b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.style.module.css
new file mode 100644
index 0000000..20062f0
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.style.module.css
@@ -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;
+ }
+ }
+}
diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.ui.jsx b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.ui.jsx
new file mode 100644
index 0000000..c2110d1
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.ui.jsx
@@ -0,0 +1,39 @@
+import { WEEK_DAYS } from 'shared/constants';
+
+import { DayOfWeekCell } from '../../DayOfWeekCell';
+
+import classes from './DayOfWeekRow.style.module.css';
+
+const DayOfWeekRow = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export default DayOfWeekRow;
diff --git a/src/widgets/CalendarWidget/ui/components/HeaderRow/index.js b/src/widgets/CalendarWidget/ui/components/HeaderRow/index.js
new file mode 100644
index 0000000..257aa52
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/HeaderRow/index.js
@@ -0,0 +1 @@
+export { default as HeaderRow } from './ui/HeaderRow.ui';
diff --git a/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.spec.jsx b/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.spec.jsx
new file mode 100644
index 0000000..644feb2
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.spec.jsx
@@ -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();
+ };
+
+ 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();
+ });
+});
diff --git a/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.styles.module.css b/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.styles.module.css
new file mode 100644
index 0000000..9105e66
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.styles.module.css
@@ -0,0 +1,9 @@
+.headerRow {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ >div {
+ background: dimgrey;
+ }
+}
diff --git a/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.ui.jsx b/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.ui.jsx
new file mode 100644
index 0000000..3716020
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.ui.jsx
@@ -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 = () => (
+
+
+
+
+
+
+
+ <
+
+
+
+ Сегодня
+
+
+
+ >
+
+
+
+
+
+
+
+);
+
+export default HeaderRow;
diff --git a/src/widgets/CalendarWidget/ui/components/MonthSelect/index.js b/src/widgets/CalendarWidget/ui/components/MonthSelect/index.js
new file mode 100644
index 0000000..c9d19ac
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/MonthSelect/index.js
@@ -0,0 +1 @@
+export { default as MonthSelect } from './ui/MonthSelect.ui';
diff --git a/src/widgets/CalendarWidget/ui/components/MonthSelect/ui/MonthSelect.spec.jsx b/src/widgets/CalendarWidget/ui/components/MonthSelect/ui/MonthSelect.spec.jsx
new file mode 100644
index 0000000..238745c
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/MonthSelect/ui/MonthSelect.spec.jsx
@@ -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 = () => (
+
+ );
+ const MonthSelectWithProvider = withProvider(CalendarWidgetProvider)(MonthSelectComponent);
+
+ render();
+ };
+
+ 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);
+ });
+});
diff --git a/src/widgets/CalendarWidget/ui/components/MonthSelect/ui/MonthSelect.ui.jsx b/src/widgets/CalendarWidget/ui/components/MonthSelect/ui/MonthSelect.ui.jsx
new file mode 100644
index 0000000..1ed43fe
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/MonthSelect/ui/MonthSelect.ui.jsx
@@ -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 (
+
+ );
+ };
+
+ return (
+
+ );
+};
+
+MonthSelect.propTypes = {
+ onChange: PropTypes.func,
+};
+
+export default MonthSelect;
diff --git a/src/widgets/CalendarWidget/ui/components/NextMonthButton/index.js b/src/widgets/CalendarWidget/ui/components/NextMonthButton/index.js
new file mode 100644
index 0000000..be91df4
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/NextMonthButton/index.js
@@ -0,0 +1 @@
+export { default as NextMonthButton } from './ui/NextMonthButton.ui';
diff --git a/src/widgets/CalendarWidget/ui/components/NextMonthButton/ui/NextMonthButton.spec.jsx b/src/widgets/CalendarWidget/ui/components/NextMonthButton/ui/NextMonthButton.spec.jsx
new file mode 100644
index 0000000..e8266d6
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/NextMonthButton/ui/NextMonthButton.spec.jsx
@@ -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 = () => (
+
+ );
+ const NextMonthButtonWithProvider = withProvider(CalendarWidgetProvider)(NextMonthButtonComponent);
+
+ render();
+ };
+
+ 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();
+ });
+});
diff --git a/src/widgets/CalendarWidget/ui/components/NextMonthButton/ui/NextMonthButton.ui.jsx b/src/widgets/CalendarWidget/ui/components/NextMonthButton/ui/NextMonthButton.ui.jsx
new file mode 100644
index 0000000..b7001c7
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/NextMonthButton/ui/NextMonthButton.ui.jsx
@@ -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 (
+
+ );
+};
+
+NextMonthButton.propTypes = {
+ children: PropTypes.node,
+ onClick: PropTypes.func,
+ disabled: PropTypes.bool,
+};
+
+export default NextMonthButton;
diff --git a/src/widgets/CalendarWidget/ui/components/PrevMonthButton/index.js b/src/widgets/CalendarWidget/ui/components/PrevMonthButton/index.js
new file mode 100644
index 0000000..5a64373
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/PrevMonthButton/index.js
@@ -0,0 +1 @@
+export { default as PrevMonthButton } from './ui/PrevMonthButton.ui';
diff --git a/src/widgets/CalendarWidget/ui/components/PrevMonthButton/ui/PrevMonthButton.spec.jsx b/src/widgets/CalendarWidget/ui/components/PrevMonthButton/ui/PrevMonthButton.spec.jsx
new file mode 100644
index 0000000..3e1de67
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/PrevMonthButton/ui/PrevMonthButton.spec.jsx
@@ -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 = () => (
+
+ );
+ const PrevMonthButtonWithProvider = withProvider(CalendarWidgetProvider)(PrevMonthButtonComponent);
+
+ render();
+ };
+
+ 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();
+ });
+});
diff --git a/src/widgets/CalendarWidget/ui/components/PrevMonthButton/ui/PrevMonthButton.ui.jsx b/src/widgets/CalendarWidget/ui/components/PrevMonthButton/ui/PrevMonthButton.ui.jsx
new file mode 100644
index 0000000..45bca7e
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/PrevMonthButton/ui/PrevMonthButton.ui.jsx
@@ -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 (
+
+ );
+};
+
+PrevMonthButton.propTypes = {
+ children: PropTypes.node,
+ onClick: PropTypes.func,
+ disabled: PropTypes.bool,
+};
+
+export default PrevMonthButton;
diff --git a/src/widgets/CalendarWidget/ui/components/TodayButton/index.js b/src/widgets/CalendarWidget/ui/components/TodayButton/index.js
new file mode 100644
index 0000000..92dd317
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/TodayButton/index.js
@@ -0,0 +1 @@
+export { default as TodayButton } from './ui/TodayButton.ui';
diff --git a/src/widgets/CalendarWidget/ui/components/TodayButton/ui/TodayButton.spec.jsx b/src/widgets/CalendarWidget/ui/components/TodayButton/ui/TodayButton.spec.jsx
new file mode 100644
index 0000000..5e47dcf
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/TodayButton/ui/TodayButton.spec.jsx
@@ -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 = () => (
+
+ );
+ const TodayButtonWithProvider = withProvider(CalendarWidgetProvider)(TodayButtonComponent);
+
+ render();
+ };
+
+ 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);
+ });
+});
diff --git a/src/widgets/CalendarWidget/ui/components/TodayButton/ui/TodayButton.ui.jsx b/src/widgets/CalendarWidget/ui/components/TodayButton/ui/TodayButton.ui.jsx
new file mode 100644
index 0000000..a23d7e6
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/TodayButton/ui/TodayButton.ui.jsx
@@ -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 (
+
+ );
+};
+
+TodayButton.propTypes = {
+ children: PropTypes.node,
+ onClick: PropTypes.func,
+};
+
+export default TodayButton;
diff --git a/src/widgets/CalendarWidget/ui/components/YearSelect/index.js b/src/widgets/CalendarWidget/ui/components/YearSelect/index.js
new file mode 100644
index 0000000..7ac62c0
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/YearSelect/index.js
@@ -0,0 +1 @@
+export { default as YearSelect } from './ui/YearSelect.ui';
diff --git a/src/widgets/CalendarWidget/ui/components/YearSelect/ui/YearSelect.spec.jsx b/src/widgets/CalendarWidget/ui/components/YearSelect/ui/YearSelect.spec.jsx
new file mode 100644
index 0000000..80f3778
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/YearSelect/ui/YearSelect.spec.jsx
@@ -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 = () => (
+
+ );
+ const YearSelectWithProvider = withProvider(CalendarWidgetProvider)(YearSelectComponent);
+
+ render();
+ };
+
+ 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);
+ });
+});
diff --git a/src/widgets/CalendarWidget/ui/components/YearSelect/ui/YearSelect.ui.jsx b/src/widgets/CalendarWidget/ui/components/YearSelect/ui/YearSelect.ui.jsx
new file mode 100644
index 0000000..69a920c
--- /dev/null
+++ b/src/widgets/CalendarWidget/ui/components/YearSelect/ui/YearSelect.ui.jsx
@@ -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 (
+
+ );
+ });
+ };
+
+ return (
+
+ );
+};
+
+YearSelect.propTypes = {
+ onChange: PropTypes.func,
+};
+
+export default YearSelect;