@daryasidorovich/modsen-datepicker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/.babelrc.json +7 -0
  2. package/.github/workflows/chromatic.yml +25 -0
  3. package/.github/workflows/ci.yml +56 -0
  4. package/.husky/commit-msg +1 -0
  5. package/.husky/pre-commit +1 -0
  6. package/.prettierignore +16 -0
  7. package/.prettierrc +6 -0
  8. package/.storybook/main.js +93 -0
  9. package/.storybook/preview-head.html +6 -0
  10. package/.storybook/preview.js +26 -0
  11. package/.storybook/preview.tsx +18 -0
  12. package/README.md +302 -0
  13. package/commitlint.config.js +23 -0
  14. package/dist/components/BaseCalendar/index.d.ts +2 -0
  15. package/dist/components/CalendarGrid/CalendarGridWithTasks/Tooltip/index.d.ts +8 -0
  16. package/dist/components/CalendarGrid/CalendarGridWithTasks/index.d.ts +2 -0
  17. package/dist/components/CalendarGrid/CalendarGridWithoutTasks/CalendarGridWithoutTasks.test.d.ts +1 -0
  18. package/dist/components/CalendarGrid/CalendarGridWithoutTasks/index.d.ts +4 -0
  19. package/dist/components/CalendarGrid/index.d.ts +2 -0
  20. package/dist/components/CalendarGrid/types.d.ts +21 -0
  21. package/dist/components/Clear/index.d.ts +5 -0
  22. package/dist/components/DatePicker/index.d.ts +5 -0
  23. package/dist/components/Day/index.d.ts +16 -0
  24. package/dist/components/Input/index.d.ts +10 -0
  25. package/dist/components/Modal/CrudModal/index.d.ts +2 -0
  26. package/dist/components/Modal/index.d.ts +12 -0
  27. package/dist/components/Modal/types.d.ts +12 -0
  28. package/dist/components/Month/index.d.ts +7 -0
  29. package/dist/components/RangePicker/index.d.ts +6 -0
  30. package/dist/components/TasksManager/AddModal/index.d.ts +6 -0
  31. package/dist/components/TasksManager/DeleteModal/index.d.ts +6 -0
  32. package/dist/components/TasksManager/EditModal/index.d.ts +7 -0
  33. package/dist/components/TasksManager/index.d.ts +9 -0
  34. package/dist/components/Week/index.d.ts +17 -0
  35. package/dist/components/WeekDay/index.d.ts +7 -0
  36. package/dist/constants/dateMask.d.ts +21 -0
  37. package/dist/constants/firstDayOfWeek.d.ts +5 -0
  38. package/dist/constants/holidays.d.ts +1 -0
  39. package/dist/constants/modes.d.ts +5 -0
  40. package/dist/constants/monthsNames.d.ts +1 -0
  41. package/dist/constants/weekdaysNames.d.ts +1 -0
  42. package/dist/hooks/useCalendar.d.ts +7 -0
  43. package/dist/hooks/useLockBodyScroll.d.ts +2 -0
  44. package/dist/hooks/useModal.d.ts +13 -0
  45. package/dist/hooks/useTooltip.d.ts +9 -0
  46. package/dist/hooks/useValidate.d.ts +11 -0
  47. package/dist/index.cjs +17250 -0
  48. package/dist/index.cjs.map +1 -0
  49. package/dist/index.d.ts +11 -0
  50. package/dist/index.js +17233 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/services/calendarService.d.ts +19 -0
  53. package/dist/services/decorators/withHolidays.d.ts +2 -0
  54. package/dist/services/decorators/withHolidays.test.d.ts +1 -0
  55. package/dist/services/decorators/withMinMax.d.ts +2 -0
  56. package/dist/services/decorators/withMinMax.test.d.ts +1 -0
  57. package/dist/services/holidayService.d.ts +5 -0
  58. package/dist/setupTests.d.ts +1 -0
  59. package/dist/store/localStorage.d.ts +3 -0
  60. package/dist/store/tasksStore.d.ts +8 -0
  61. package/dist/store/tasksStore.test.d.ts +1 -0
  62. package/dist/store/types.d.ts +18 -0
  63. package/dist/utils/calendar.d.ts +7 -0
  64. package/dist/utils/calendar.test.d.ts +1 -0
  65. package/dist/utils/controlDate.d.ts +1 -0
  66. package/dist/utils/dateChecks.d.ts +10 -0
  67. package/dist/utils/dateChecks.test.d.ts +1 -0
  68. package/dist/utils/formatDate.d.ts +3 -0
  69. package/dist/utils/formatDate.test.d.ts +1 -0
  70. package/dist/utils/schema.d.ts +6 -0
  71. package/dist/utils/schema.test.d.ts +1 -0
  72. package/dist/utils/weekdayOrder.d.ts +1 -0
  73. package/dist/utils/weekdayOrder.test.d.ts +1 -0
  74. package/docs/base-calendar.png +0 -0
  75. package/docs/dark-theme.png +0 -0
  76. package/docs/datepicker.png +0 -0
  77. package/docs/rangepicker-with-error.png +0 -0
  78. package/docs/rangepicker.png +0 -0
  79. package/eslint.config.ts +80 -0
  80. package/jest.config.json +21 -0
  81. package/package.json +106 -0
  82. package/rollup.config.js +65 -0
  83. package/src/assets/actions/Delete.svg +4 -0
  84. package/src/assets/actions/Edit.svg +3 -0
  85. package/src/assets/arrows/Next.svg +4 -0
  86. package/src/assets/arrows/Prev.svg +4 -0
  87. package/src/assets/input/Calendar.svg +3 -0
  88. package/src/assets/input/Clear.svg +3 -0
  89. package/src/components/BaseCalendar/BaseCalendar.stories.tsx +77 -0
  90. package/src/components/BaseCalendar/index.tsx +63 -0
  91. package/src/components/BaseCalendar/styles.module.scss +15 -0
  92. package/src/components/CalendarGrid/CalendarGrid.stories.tsx +136 -0
  93. package/src/components/CalendarGrid/CalendarGridWithTasks/Tooltip/index.tsx +27 -0
  94. package/src/components/CalendarGrid/CalendarGridWithTasks/Tooltip/styles.module.scss +9 -0
  95. package/src/components/CalendarGrid/CalendarGridWithTasks/index.tsx +51 -0
  96. package/src/components/CalendarGrid/CalendarGridWithoutTasks/CalendarGridWithoutTasks.test.tsx +223 -0
  97. package/src/components/CalendarGrid/CalendarGridWithoutTasks/index.tsx +36 -0
  98. package/src/components/CalendarGrid/CalendarGridWithoutTasks/styles.module.scss +11 -0
  99. package/src/components/CalendarGrid/index.tsx +19 -0
  100. package/src/components/CalendarGrid/types.ts +26 -0
  101. package/src/components/Clear/Clear.stories.tsx +16 -0
  102. package/src/components/Clear/index.tsx +12 -0
  103. package/src/components/Clear/styles.module.scss +34 -0
  104. package/src/components/DatePicker/DatePicker.stories.tsx +127 -0
  105. package/src/components/DatePicker/index.tsx +72 -0
  106. package/src/components/DatePicker/styles.module.scss +10 -0
  107. package/src/components/Day/Day.stories.tsx +75 -0
  108. package/src/components/Day/index.tsx +95 -0
  109. package/src/components/Day/styles.module.scss +97 -0
  110. package/src/components/Input/Input.stories.tsx +37 -0
  111. package/src/components/Input/index.tsx +64 -0
  112. package/src/components/Input/styles.module.scss +79 -0
  113. package/src/components/Modal/CrudModal/index.tsx +43 -0
  114. package/src/components/Modal/CrudModal/styles.module.scss +44 -0
  115. package/src/components/Modal/index.tsx +66 -0
  116. package/src/components/Modal/styles.module.scss +40 -0
  117. package/src/components/Modal/types.ts +13 -0
  118. package/src/components/Month/Month.stories.tsx +19 -0
  119. package/src/components/Month/index.tsx +31 -0
  120. package/src/components/Month/styles.module.scss +30 -0
  121. package/src/components/RangePicker/RangePicker.stories.tsx +134 -0
  122. package/src/components/RangePicker/index.tsx +144 -0
  123. package/src/components/RangePicker/styles.module.scss +19 -0
  124. package/src/components/TasksManager/AddModal/index.tsx +62 -0
  125. package/src/components/TasksManager/DeleteModal/index.tsx +22 -0
  126. package/src/components/TasksManager/EditModal/index.tsx +96 -0
  127. package/src/components/TasksManager/index.tsx +112 -0
  128. package/src/components/TasksManager/styles.module.scss +62 -0
  129. package/src/components/Week/Week.stories.tsx +49 -0
  130. package/src/components/Week/index.tsx +68 -0
  131. package/src/components/Week/styles.module.scss +7 -0
  132. package/src/components/WeekDay/WeekDay.stories.tsx +38 -0
  133. package/src/components/WeekDay/index.tsx +35 -0
  134. package/src/components/WeekDay/styles.module.scss +19 -0
  135. package/src/constants/dateMask.ts +23 -0
  136. package/src/constants/firstDayOfWeek.ts +7 -0
  137. package/src/constants/holidays.ts +32 -0
  138. package/src/constants/modes.ts +6 -0
  139. package/src/constants/monthsNames.ts +14 -0
  140. package/src/constants/weekdaysNames.ts +1 -0
  141. package/src/hooks/useCalendar.ts +62 -0
  142. package/src/hooks/useLockBodyScroll.ts +19 -0
  143. package/src/hooks/useModal.ts +45 -0
  144. package/src/hooks/useTooltip.ts +41 -0
  145. package/src/hooks/useValidate.ts +80 -0
  146. package/src/index.ts +15 -0
  147. package/src/services/calendarService.ts +62 -0
  148. package/src/services/decorators/withHolidays.test.ts +64 -0
  149. package/src/services/decorators/withHolidays.ts +12 -0
  150. package/src/services/decorators/withMinMax.test.ts +77 -0
  151. package/src/services/decorators/withMinMax.ts +19 -0
  152. package/src/services/holidayService.ts +56 -0
  153. package/src/setupTests.ts +1 -0
  154. package/src/store/localStorage.ts +33 -0
  155. package/src/store/tasksStore.test.tsx +208 -0
  156. package/src/store/tasksStore.tsx +146 -0
  157. package/src/store/types.ts +22 -0
  158. package/src/styles/_border.scss +9 -0
  159. package/src/styles/_colors.scss +53 -0
  160. package/src/styles/_gap.scss +4 -0
  161. package/src/styles/_globals.scss +61 -0
  162. package/src/styles/_grid.scss +9 -0
  163. package/src/styles/_mixins.scss +6 -0
  164. package/src/styles/_reset.scss +98 -0
  165. package/src/styles/_theme-mixins.scss +51 -0
  166. package/src/styles/_typography.scss +10 -0
  167. package/src/styles/_variables.scss +8 -0
  168. package/src/styles/main.scss +3 -0
  169. package/src/types/types.d.ts +15 -0
  170. package/src/utils/calendar.test.ts +154 -0
  171. package/src/utils/calendar.ts +82 -0
  172. package/src/utils/controlDate.ts +3 -0
  173. package/src/utils/dateChecks.test.ts +216 -0
  174. package/src/utils/dateChecks.ts +75 -0
  175. package/src/utils/formatDate.test.ts +57 -0
  176. package/src/utils/formatDate.ts +27 -0
  177. package/src/utils/schema.test.ts +40 -0
  178. package/src/utils/schema.ts +8 -0
  179. package/src/utils/weekdayOrder.test.ts +11 -0
  180. package/src/utils/weekdayOrder.ts +7 -0
  181. package/tsconfig.json +31 -0
  182. package/vitest.config.ts +38 -0
  183. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,136 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { CalendarGrid } from '.';
3
+
4
+ const meta: Meta<typeof CalendarGrid> = {
5
+ component: CalendarGrid,
6
+ parameters: {
7
+ layout: 'centered',
8
+ },
9
+ argTypes: {
10
+ firstDayOfWeek: {
11
+ control: 'select',
12
+ options: [0, 1],
13
+ },
14
+ },
15
+ };
16
+
17
+ export default meta;
18
+
19
+ type Story = StoryObj<typeof CalendarGrid>;
20
+
21
+ export const Default: Story = {
22
+ args: {
23
+ monthDate: new Date(2026, 7, 1),
24
+ firstDayOfWeek: 1,
25
+ selectedDate: new Date(2026, 7, 15),
26
+ },
27
+ };
28
+
29
+ export const SundayFirst: Story = {
30
+ args: {
31
+ monthDate: new Date(2026, 7, 1),
32
+ firstDayOfWeek: 0,
33
+ selectedDate: new Date(2026, 7, 15),
34
+ },
35
+ };
36
+
37
+ export const WithSelectedDate: Story = {
38
+ args: {
39
+ monthDate: new Date(2026, 7, 1),
40
+ firstDayOfWeek: 1,
41
+ selectedDate: new Date(2026, 7, 27),
42
+ },
43
+ };
44
+
45
+ export const WithMinMax: Story = {
46
+ args: {
47
+ monthDate: new Date(2026, 7, 1),
48
+ firstDayOfWeek: 1,
49
+ selectedDate: new Date(2026, 7, 15),
50
+ minDate: new Date(2026, 7, 10),
51
+ maxDate: new Date(2026, 7, 20),
52
+ },
53
+ };
54
+
55
+ export const WeekMode: Story = {
56
+ args: {
57
+ monthDate: new Date(2026, 7, 1),
58
+ firstDayOfWeek: 1,
59
+ selectedDate: new Date(2026, 7, 15),
60
+ mode: 'week',
61
+ },
62
+ };
63
+
64
+ export const FullFeatured: Story = {
65
+ args: {
66
+ monthDate: new Date(2026, 7, 1),
67
+ firstDayOfWeek: 1,
68
+ selectedDate: new Date(2026, 7, 15),
69
+ minDate: new Date(2026, 7, 1),
70
+ maxDate: new Date(2026, 7, 15),
71
+ },
72
+ };
73
+
74
+ export const September: Story = {
75
+ args: {
76
+ monthDate: new Date(2026, 8, 1),
77
+ firstDayOfWeek: 1,
78
+ selectedDate: new Date(2026, 8, 15),
79
+ },
80
+ };
81
+
82
+ export const CurrentMonth: Story = {
83
+ args: {
84
+ monthDate: new Date(),
85
+ firstDayOfWeek: 1,
86
+ },
87
+ };
88
+
89
+ export const WithRange: Story = {
90
+ args: {
91
+ monthDate: new Date(),
92
+ firstDayOfWeek: 1,
93
+ rangeStart: new Date(2026, 7, 10),
94
+ rangeEnd: new Date(2026, 7, 19),
95
+ },
96
+ };
97
+
98
+ export const HideWeekends: Story = {
99
+ args: {
100
+ monthDate: new Date(),
101
+ firstDayOfWeek: 1,
102
+ hideWeekends: true,
103
+ },
104
+ };
105
+
106
+ export const ShowHolidays: Story = {
107
+ args: {
108
+ monthDate: new Date(2025, 0, 3),
109
+ firstDayOfWeek: 1,
110
+ showHolidays: true,
111
+ },
112
+ };
113
+
114
+ export const WithError: Story = {
115
+ args: {
116
+ monthDate: new Date(2026, 0, 3),
117
+ firstDayOfWeek: 1,
118
+ showHolidays: true,
119
+ },
120
+ };
121
+
122
+ export const WithTasks: Story = {
123
+ args: {
124
+ monthDate: new Date(2026, 7, 25),
125
+ firstDayOfWeek: 1,
126
+ enableTasks: true,
127
+ },
128
+ };
129
+
130
+ export const WithoutTasks: Story = {
131
+ args: {
132
+ monthDate: new Date(2026, 7, 25),
133
+ firstDayOfWeek: 1,
134
+ enableTasks: false,
135
+ },
136
+ };
@@ -0,0 +1,27 @@
1
+ import { createPortal } from 'react-dom';
2
+
3
+ import styles from './styles.module.scss';
4
+
5
+ interface TooltipProps {
6
+ isVisible: boolean;
7
+ text: string;
8
+ x: number;
9
+ y: number;
10
+ }
11
+
12
+ export const Tooltip = ({ isVisible, text, x, y }: TooltipProps) => {
13
+ if (!isVisible) return;
14
+
15
+ const Tooltip = (
16
+ <div
17
+ className={styles.tooltip}
18
+ style={{
19
+ top: y,
20
+ left: x,
21
+ }}
22
+ >
23
+ <p>{text}</p>
24
+ </div>
25
+ );
26
+ return createPortal(Tooltip, document.body);
27
+ };
@@ -0,0 +1,9 @@
1
+ .tooltip {
2
+ @include themify-modules($themes) {
3
+ background-color: themed('border');
4
+ }
5
+
6
+ border-radius: $radius-8;
7
+ padding: 2px $gap-8;
8
+ position: absolute;
9
+ }
@@ -0,0 +1,51 @@
1
+ import { useState } from 'react';
2
+
3
+ import { TasksManager } from '@/components/TasksManager';
4
+ import { useModal } from '@/hooks/useModal';
5
+ import { useTooltip } from '@/hooks/useTooltip';
6
+ import { useTasks } from '@/store/tasksStore';
7
+
8
+ import { CalendarGridWithoutTasks } from '../CalendarGridWithoutTasks';
9
+ import { CalendarProps } from '../types';
10
+ import { Tooltip } from './Tooltip';
11
+
12
+ export const CalendarGridWithTasks = (props: CalendarProps) => {
13
+ const { getTasksByDate, hasTasks } = useTasks();
14
+ const [selectedDate, setSelectedDate] = useState<Date>();
15
+ const tasksManagerModal = useModal();
16
+ const { tooltip, handleDayMouseEnter, handleDayMouseLeave } = useTooltip();
17
+
18
+ const handleDateDoubleClick = (date: Date) => {
19
+ setSelectedDate(date);
20
+ tasksManagerModal.open();
21
+ };
22
+
23
+ const currentTasks = selectedDate ? getTasksByDate(selectedDate) : [];
24
+
25
+ return (
26
+ <>
27
+ <CalendarGridWithoutTasks
28
+ {...props}
29
+ hasTasks={hasTasks}
30
+ onDateDoubleClick={handleDateDoubleClick}
31
+ onMouseEnter={handleDayMouseEnter}
32
+ onMouseLeave={handleDayMouseLeave}
33
+ >
34
+ {selectedDate && (
35
+ <TasksManager
36
+ tasks={currentTasks}
37
+ isOpen={tasksManagerModal.isOpen}
38
+ onClose={tasksManagerModal.close}
39
+ selectedDate={selectedDate}
40
+ />
41
+ )}
42
+ </CalendarGridWithoutTasks>
43
+ <Tooltip
44
+ isVisible={tooltip.isVisible}
45
+ text="Double-click to open the task manager"
46
+ x={tooltip.x}
47
+ y={tooltip.y}
48
+ />
49
+ </>
50
+ );
51
+ };
@@ -0,0 +1,223 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { getHolidays } from '@/services/holidayService';
4
+ import { getDefaultHolidays } from '@/constants/holidays';
5
+ import { CalendarGridWithoutTasks } from '.';
6
+
7
+ jest.mock('@/services/holidayService');
8
+ jest.mock('@/constants/holidays');
9
+
10
+ describe('CalendarWithoutTasks:', () => {
11
+ const august2026 = new Date(2026, 7, 1);
12
+ const mockHolidays = [new Date(2026, 7, 10)];
13
+
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+
18
+ test('renders calendar with default props:', () => {
19
+ render(
20
+ <CalendarGridWithoutTasks monthDate={august2026} firstDayOfWeek={1} />
21
+ );
22
+
23
+ const days = screen.getAllByText('1');
24
+ expect(days.length).toBeGreaterThan(0);
25
+ const days31 = screen.getAllByText('31');
26
+ expect(days31.length).toBeGreaterThan(0);
27
+ });
28
+
29
+ test('displays selected date correctly:', () => {
30
+ render(
31
+ <CalendarGridWithoutTasks
32
+ monthDate={august2026}
33
+ selectedDate={new Date(2026, 7, 15)}
34
+ firstDayOfWeek={1}
35
+ />
36
+ );
37
+
38
+ const day15 = screen.getByText('15');
39
+ expect(day15).toBeInTheDocument();
40
+ expect(day15.closest('button')).toHaveClass('selected');
41
+ });
42
+
43
+ test('handles hideWeekends option:', () => {
44
+ render(
45
+ <CalendarGridWithoutTasks
46
+ monthDate={august2026}
47
+ firstDayOfWeek={1}
48
+ hideWeekends={true}
49
+ />
50
+ );
51
+
52
+ expect(screen.queryByText('15')).not.toBeInTheDocument();
53
+ expect(screen.queryByText('16')).not.toBeInTheDocument();
54
+ expect(screen.getByText('7')).toBeInTheDocument();
55
+ });
56
+
57
+ test('displays holidays when showHolidays is true:', async () => {
58
+ (getHolidays as jest.Mock).mockResolvedValue(mockHolidays);
59
+
60
+ render(
61
+ <CalendarGridWithoutTasks
62
+ monthDate={august2026}
63
+ firstDayOfWeek={1}
64
+ showHolidays={true}
65
+ />
66
+ );
67
+ await waitFor(() => {
68
+ const day10 = screen.getByText('10');
69
+ expect(day10).toBeInTheDocument();
70
+ expect(day10.closest('button')).toHaveClass('holiday');
71
+ });
72
+ });
73
+
74
+ test('shows loading state while fetching holidays:', async () => {
75
+ (getHolidays as jest.Mock).mockResolvedValue(mockHolidays);
76
+
77
+ render(
78
+ <CalendarGridWithoutTasks
79
+ monthDate={august2026}
80
+ firstDayOfWeek={1}
81
+ showHolidays={true}
82
+ />
83
+ );
84
+
85
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
86
+
87
+ await waitFor(() => {
88
+ expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
89
+ });
90
+ });
91
+
92
+ test('handles holiday loading error:', async () => {
93
+ const errorMessage = 'Failed to load holidays';
94
+ (getHolidays as jest.Mock).mockRejectedValue(new Error(errorMessage));
95
+ (getDefaultHolidays as jest.Mock).mockReturnValue(mockHolidays);
96
+
97
+ render(
98
+ <CalendarGridWithoutTasks
99
+ monthDate={august2026}
100
+ firstDayOfWeek={1}
101
+ showHolidays={true}
102
+ />
103
+ );
104
+
105
+ await waitFor(() => {
106
+ expect(screen.getByText(errorMessage)).toBeInTheDocument();
107
+ });
108
+
109
+ const day10 = screen.getByText('10');
110
+ expect(day10.closest('button')).toHaveClass('holiday');
111
+ });
112
+
113
+ test('applies min/max dates correctly:', () => {
114
+ render(
115
+ <CalendarGridWithoutTasks
116
+ monthDate={august2026}
117
+ firstDayOfWeek={1}
118
+ minDate={new Date(2026, 7, 10)}
119
+ maxDate={new Date(2026, 7, 20)}
120
+ />
121
+ );
122
+
123
+ const day1 = screen.getByText('8');
124
+ expect(day1.closest('button')).toBeDisabled();
125
+
126
+ const day15 = screen.getByText('18');
127
+ expect(day15.closest('button')).not.toBeDisabled();
128
+
129
+ const day31 = screen.getByText('25');
130
+ expect(day31.closest('button')).toBeDisabled();
131
+ });
132
+
133
+ test('handles week mode:', () => {
134
+ render(
135
+ <CalendarGridWithoutTasks
136
+ monthDate={august2026}
137
+ firstDayOfWeek={1}
138
+ mode="week"
139
+ />
140
+ );
141
+
142
+ const days = document.querySelectorAll('.day');
143
+ expect(days.length).toBe(7);
144
+ });
145
+
146
+ test('handles day click', async () => {
147
+ const handleDoubleClick = jest.fn();
148
+ const handleClick = jest.fn();
149
+
150
+ render(
151
+ <CalendarGridWithoutTasks
152
+ monthDate={august2026}
153
+ firstDayOfWeek={1}
154
+ onDateDoubleClick={handleDoubleClick}
155
+ onDateClick={handleClick}
156
+ enableTasks
157
+ />
158
+ );
159
+
160
+ const day15 = screen.getByRole('button', { name: '15' });
161
+ expect(day15).not.toBeDisabled();
162
+
163
+ await userEvent.dblClick(day15);
164
+
165
+ expect(handleClick).not.toHaveBeenCalled();
166
+ expect(handleDoubleClick).toHaveBeenCalledTimes(1);
167
+ });
168
+
169
+ test('applies all decorators together:', async () => {
170
+ (getHolidays as jest.Mock).mockResolvedValue(mockHolidays);
171
+
172
+ render(
173
+ <CalendarGridWithoutTasks
174
+ monthDate={august2026}
175
+ firstDayOfWeek={1}
176
+ hideWeekends={true}
177
+ showHolidays={true}
178
+ minDate={new Date(2026, 7, 9)}
179
+ maxDate={new Date(2026, 7, 25)}
180
+ mode="month"
181
+ />
182
+ );
183
+
184
+ expect(screen.queryByText('15')).not.toBeInTheDocument();
185
+ expect(screen.queryByText('16')).not.toBeInTheDocument();
186
+ await waitFor(() => {
187
+ const day10 = screen.getByText('10');
188
+ expect(day10.closest('button')).toHaveClass('holiday');
189
+ });
190
+ expect(screen.getByText('6').closest('button')).toHaveClass('disabled');
191
+
192
+ const day18 = screen.getByText('18');
193
+ expect(day18.closest('button')).not.toBeDisabled();
194
+ });
195
+
196
+ test('does not fetch holidays when showHolidays is false:', () => {
197
+ render(
198
+ <CalendarGridWithoutTasks
199
+ monthDate={august2026}
200
+ firstDayOfWeek={1}
201
+ showHolidays={false}
202
+ />
203
+ );
204
+
205
+ expect(getHolidays).not.toHaveBeenCalled();
206
+ });
207
+
208
+ test('handles different first day of week:', () => {
209
+ const { rerender } = render(
210
+ <CalendarGridWithoutTasks monthDate={august2026} firstDayOfWeek={0} />
211
+ );
212
+
213
+ const week = screen.getAllByRole('button');
214
+ expect(week[0]).toHaveTextContent('26');
215
+
216
+ rerender(
217
+ <CalendarGridWithoutTasks monthDate={august2026} firstDayOfWeek={1} />
218
+ );
219
+
220
+ const week2 = screen.getAllByRole('button');
221
+ expect(week2[0]).toHaveTextContent('27');
222
+ });
223
+ });
@@ -0,0 +1,36 @@
1
+ import { Week } from '@/components/Week';
2
+ import { useCalendar } from '@/hooks/useCalendar';
3
+
4
+ import { CalendarProps } from '../types';
5
+ import styles from './styles.module.scss';
6
+
7
+ export const CalendarGridWithoutTasks = ({
8
+ children,
9
+ ...props
10
+ }: CalendarProps & { children?: React.ReactNode }) => {
11
+ const { calendar, loading, error } = useCalendar(props);
12
+
13
+ const weeks = calendar.getWeeks();
14
+
15
+ return (
16
+ <div className={styles.calendar}>
17
+ {weeks.map((weekDays) => (
18
+ <Week
19
+ key={weekDays[0].toISOString()}
20
+ weekDays={weekDays}
21
+ selectedDate={calendar.selectedDate}
22
+ getIsDisabled={calendar.getIsDisabled}
23
+ getIsCurrentMonth={calendar.getIsCurrentMonth}
24
+ getIsHoliday={calendar.getIsHoliday}
25
+ rangeStart={calendar.rangeStart}
26
+ rangeEnd={calendar.rangeEnd}
27
+ {...props}
28
+ />
29
+ ))}
30
+
31
+ {error && <p className={styles.errorMessage}>{error}</p>}
32
+ {loading && <div>Loading...</div>}
33
+ {children}
34
+ </div>
35
+ );
36
+ };
@@ -0,0 +1,11 @@
1
+ .calendar {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: $gap-8;
5
+ }
6
+
7
+ .errorMessage {
8
+ @include themify-modules($themes) {
9
+ color: themed('accent');
10
+ }
11
+ }
@@ -0,0 +1,19 @@
1
+ import { TasksProvider } from '@/store/tasksStore';
2
+
3
+ import { CalendarGridWithoutTasks } from './CalendarGridWithoutTasks';
4
+ import { CalendarGridWithTasks } from './CalendarGridWithTasks';
5
+ import { CalendarProps } from './types';
6
+
7
+ export const CalendarGrid = (props: CalendarProps) => {
8
+ const { enableTasks } = props;
9
+
10
+ if (enableTasks) {
11
+ return (
12
+ <TasksProvider>
13
+ <CalendarGridWithTasks {...props} />
14
+ </TasksProvider>
15
+ );
16
+ }
17
+
18
+ return <CalendarGridWithoutTasks {...props} />;
19
+ };
@@ -0,0 +1,26 @@
1
+ import { FIRST_DAY_OF_WEEK_TYPES } from '@/constants/firstDayOfWeek';
2
+ import { MODES_TYPES } from '@/constants/modes';
3
+
4
+ export interface CalendarProps {
5
+ minDate?: Date;
6
+ maxDate?: Date;
7
+ monthDate: Date;
8
+ firstDayOfWeek?: FIRST_DAY_OF_WEEK_TYPES;
9
+ selectedDate?: Date;
10
+ mode?: MODES_TYPES;
11
+ rangeStart?: Date;
12
+ rangeEnd?: Date;
13
+ hideWeekends?: boolean;
14
+ showHolidays?: boolean;
15
+ enableTasks?: boolean;
16
+ hasTasks?: (date: Date) => boolean;
17
+ onDateClick?: (e: React.MouseEvent<HTMLButtonElement>, date: Date) => void;
18
+ onDateDoubleClick?: (date: Date) => void;
19
+ onMouseEnter?: (e: React.MouseEvent<HTMLButtonElement>) => void;
20
+ onMouseLeave?: () => void;
21
+ }
22
+
23
+ export type BaseCalendarProps = Omit<
24
+ CalendarProps,
25
+ 'hasTasks' | 'onDoubleDateClick' | 'onMouseEnter' | 'onMouseLeave'
26
+ >;
@@ -0,0 +1,16 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Clear } from '.';
3
+
4
+ const meta: Meta<typeof Clear> = {
5
+ component: Clear,
6
+ };
7
+
8
+ export default meta;
9
+
10
+ type Story = StoryObj<typeof Clear>;
11
+
12
+ export const Default: Story = {
13
+ args: {
14
+ onClick: () => console.log('Click'),
15
+ },
16
+ };
@@ -0,0 +1,12 @@
1
+ import styles from './styles.module.scss';
2
+
3
+ interface ClearProps {
4
+ onClick: () => void;
5
+ }
6
+ export const Clear = ({ onClick }: ClearProps) => {
7
+ return (
8
+ <button className={styles.clear} type="button" onClick={onClick}>
9
+ Clear
10
+ </button>
11
+ );
12
+ };
@@ -0,0 +1,34 @@
1
+ .clear {
2
+ @include border;
3
+
4
+ @include themify-modules($themes) {
5
+ border-color: themed('border');
6
+ background-color: themed('background');
7
+ color: themed('text');
8
+ }
9
+
10
+ display: flex;
11
+ justify-content: center;
12
+ align-items: center;
13
+ padding: $gap-10;
14
+ width: 100%;
15
+ font-weight: $font-weight-semibold;
16
+ border-bottom-right-radius: $radius-8;
17
+ border-bottom-left-radius: $radius-8;
18
+
19
+ &:hover {
20
+ @include themify-modules($themes) {
21
+ background-color: themed('button-hover');
22
+ color: themed('white-color');
23
+ }
24
+ }
25
+
26
+ &:active {
27
+ transform: scale(0.98);
28
+
29
+ @include themify-modules($themes) {
30
+ background-color: themed('selected');
31
+ color: themed('white-color');
32
+ }
33
+ }
34
+ }