uikit-react-public 0.11.16 → 0.11.24

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 (142) hide show
  1. package/dist/components/Calendar/Calendar.d.ts +3 -0
  2. package/dist/components/Calendar/Calendar.stories.d.ts +42 -0
  3. package/dist/components/Calendar/Calendar.types.d.ts +18 -0
  4. package/dist/components/Calendar/index.d.ts +2 -0
  5. package/dist/components/Calendar/subcomponents/AcademicWeek.d.ts +5 -0
  6. package/dist/components/Calendar/subcomponents/AcademicWeeks.d.ts +7 -0
  7. package/dist/components/Calendar/subcomponents/ColumnHeading.d.ts +7 -0
  8. package/dist/components/Calendar/subcomponents/Controls.d.ts +6 -0
  9. package/dist/components/Calendar/subcomponents/Day.d.ts +12 -0
  10. package/dist/components/{Datepicker/subcomponents/Day → Calendar/subcomponents}/Day.stories.d.ts +9 -1
  11. package/dist/components/Calendar/subcomponents/EventDot.d.ts +6 -0
  12. package/dist/components/Calendar/subcomponents/Grid.d.ts +11 -0
  13. package/dist/components/Calendar/subcomponents/index.d.ts +7 -0
  14. package/dist/components/Calendar/utils/getAcademicWeekNumbers/getAcademicWeekNumbers.d.ts +24 -0
  15. package/dist/components/Calendar/utils/index.d.ts +4 -0
  16. package/dist/components/Calendar/utils/normaliseMonth/normaliseMonth.d.ts +9 -0
  17. package/dist/components/Calendar/utils/normaliseMonth/normaliseMonth.test.d.ts +1 -0
  18. package/dist/components/Calendar/utils/parseDateFromString/parseDateFromString.d.ts +9 -0
  19. package/dist/components/Calendar/utils/parseDateFromString/parseDateFromString.test.d.ts +1 -0
  20. package/dist/components/Datepicker/Datepicker.d.ts +3 -12
  21. package/dist/components/Datepicker/Datepicker.stories.d.ts +16 -3
  22. package/dist/components/Datepicker/Datepicker.types.d.ts +23 -0
  23. package/dist/components/Datepicker/index.d.ts +1 -1
  24. package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +17 -0
  25. package/dist/components/Datepicker/subcomponents/DatepickerInput.d.ts +10 -0
  26. package/dist/components/Datepicker/subcomponents/NativeDatepicker.d.ts +6 -0
  27. package/dist/components/Datepicker/subcomponents/Panel.d.ts +6 -0
  28. package/dist/components/Datepicker/subcomponents/VisibleField.d.ts +12 -0
  29. package/dist/components/Datepicker/subcomponents/index.d.ts +5 -7
  30. package/dist/components/Datepicker/utils/dateToLocaleISOString/dateToLocaleISOString.d.ts +17 -0
  31. package/dist/components/Datepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts +1 -0
  32. package/dist/components/Datepicker/utils/index.d.ts +2 -2
  33. package/dist/components/Datepicker/utils/parseInputValue/parseInputValue.d.ts +11 -0
  34. package/dist/components/Datepicker/utils/parseInputValue/parseInputValue.test.d.ts +1 -0
  35. package/dist/components/Footer/Footer.d.ts +1 -1
  36. package/dist/components/Header/Header.d.ts +5 -4
  37. package/dist/components/Header/index.d.ts +1 -1
  38. package/dist/components/Menu/Menu.d.ts +2 -1
  39. package/dist/components/Menu/MenuContent.d.ts +1 -0
  40. package/dist/components/Select/Select.stories.d.ts +1 -1
  41. package/dist/components/Select/Select.types.d.ts +10 -50
  42. package/dist/components/Select/index.d.ts +1 -1
  43. package/dist/components/Select/subcomponents/CustomSelect.d.ts +2 -1
  44. package/dist/components/index.d.ts +2 -0
  45. package/dist/index.js +3332 -3063
  46. package/lib/components/Calendar/Calendar.stories.tsx +209 -0
  47. package/lib/components/Calendar/Calendar.tsx +121 -0
  48. package/lib/components/Calendar/Calendar.types.ts +21 -0
  49. package/lib/components/Calendar/__tests__/Calendar.test.tsx +71 -0
  50. package/lib/components/Calendar/__tests__/__snapshots__/Calendar.test.tsx.snap +1218 -0
  51. package/lib/components/Calendar/index.ts +6 -0
  52. package/lib/components/Calendar/subcomponents/AcademicWeek.tsx +36 -0
  53. package/lib/components/Calendar/subcomponents/AcademicWeeks.tsx +46 -0
  54. package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +40 -0
  55. package/lib/components/{Datepicker/subcomponents/MonthSelector/MonthSelector.tsx → Calendar/subcomponents/Controls.tsx} +17 -17
  56. package/lib/components/{Datepicker/subcomponents/Day → Calendar/subcomponents}/Day.stories.tsx +30 -7
  57. package/lib/components/Calendar/subcomponents/Day.tsx +130 -0
  58. package/lib/components/Calendar/subcomponents/EventDot.tsx +40 -0
  59. package/lib/components/Calendar/subcomponents/Grid.tsx +117 -0
  60. package/lib/components/Calendar/subcomponents/index.ts +7 -0
  61. package/lib/components/Calendar/utils/getAcademicWeekNumbers/getAcademicWeekNumbers.test.ts +104 -0
  62. package/lib/components/Calendar/utils/getAcademicWeekNumbers/getAcademicWeekNumbers.ts +85 -0
  63. package/lib/components/{Datepicker → Calendar}/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.test.ts +29 -65
  64. package/lib/components/{Datepicker → Calendar}/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.ts +11 -43
  65. package/lib/components/Calendar/utils/index.ts +4 -0
  66. package/lib/components/Calendar/utils/normaliseMonth/normaliseMonth.test.ts +40 -0
  67. package/lib/components/Calendar/utils/normaliseMonth/normaliseMonth.ts +16 -0
  68. package/lib/components/Calendar/utils/parseDateFromString/parseDateFromString.test.ts +15 -0
  69. package/lib/components/Calendar/utils/parseDateFromString/parseDateFromString.ts +19 -0
  70. package/lib/components/Datepicker/Datepicker.stories.tsx +220 -23
  71. package/lib/components/Datepicker/Datepicker.tsx +34 -137
  72. package/lib/components/Datepicker/Datepicker.types.ts +38 -0
  73. package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +53 -112
  74. package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +92 -638
  75. package/lib/components/Datepicker/index.ts +1 -1
  76. package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +209 -0
  77. package/lib/components/Datepicker/subcomponents/DatepickerInput.tsx +74 -0
  78. package/lib/components/Datepicker/subcomponents/NativeDatepicker.tsx +70 -0
  79. package/lib/components/Datepicker/subcomponents/Panel.tsx +32 -0
  80. package/lib/components/Datepicker/subcomponents/VisibleField.tsx +104 -0
  81. package/lib/components/Datepicker/subcomponents/index.ts +5 -7
  82. package/lib/components/Datepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.ts +32 -0
  83. package/lib/components/Datepicker/utils/dateToLocaleISOString/dateToLocaleISOString.ts +23 -0
  84. package/lib/components/Datepicker/utils/index.ts +2 -2
  85. package/lib/components/Datepicker/utils/parseInputValue/parseInputValue.test.ts +110 -0
  86. package/lib/components/Datepicker/utils/parseInputValue/parseInputValue.ts +57 -0
  87. package/lib/components/Footer/Footer.tsx +3 -3
  88. package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +6 -6
  89. package/lib/components/Header/Header.tsx +32 -33
  90. package/lib/components/Header/HeaderMenu.tsx +9 -2
  91. package/lib/components/Header/__tests__/__snapshots__/Header.test.tsx.snap +40 -48
  92. package/lib/components/Header/index.ts +5 -1
  93. package/lib/components/Menu/Menu.tsx +3 -0
  94. package/lib/components/Menu/MenuContent.tsx +4 -1
  95. package/lib/components/Select/Select.stories.tsx +38 -39
  96. package/lib/components/Select/Select.tsx +4 -18
  97. package/lib/components/Select/Select.types.ts +30 -69
  98. package/lib/components/Select/__tests__/Select.test.tsx +6 -6
  99. package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
  100. package/lib/components/Select/index.ts +1 -1
  101. package/lib/components/Select/subcomponents/CustomSelect.tsx +22 -12
  102. package/lib/components/Select/subcomponents/NativeSelect.tsx +7 -3
  103. package/lib/components/Select/subcomponents/Panel.tsx +4 -4
  104. package/lib/components/Select/subcomponents/VisibleField.tsx +1 -1
  105. package/lib/components/index.ts +3 -0
  106. package/package.json +4 -4
  107. package/LICENSE +0 -9
  108. package/dist/components/Datepicker/subcomponents/CalendarGrid/CalendarGrid.d.ts +0 -6
  109. package/dist/components/Datepicker/subcomponents/CalendarGrid/index.d.ts +0 -1
  110. package/dist/components/Datepicker/subcomponents/CalendarMenu/CalendarMenu.d.ts +0 -8
  111. package/dist/components/Datepicker/subcomponents/CalendarMenu/index.d.ts +0 -1
  112. package/dist/components/Datepicker/subcomponents/ColumnHeadings/ColumnHeadings.d.ts +0 -2
  113. package/dist/components/Datepicker/subcomponents/ColumnHeadings/index.d.ts +0 -1
  114. package/dist/components/Datepicker/subcomponents/DateField/DateField.d.ts +0 -7
  115. package/dist/components/Datepicker/subcomponents/DateField/index.d.ts +0 -1
  116. package/dist/components/Datepicker/subcomponents/Day/Day.d.ts +0 -10
  117. package/dist/components/Datepicker/subcomponents/Day/index.d.ts +0 -1
  118. package/dist/components/Datepicker/subcomponents/MonthSelector/MonthSelector.d.ts +0 -6
  119. package/dist/components/Datepicker/subcomponents/MonthSelector/index.d.ts +0 -1
  120. package/dist/components/Datepicker/subcomponents/Native/Native.d.ts +0 -9
  121. package/dist/components/Datepicker/subcomponents/Native/index.d.ts +0 -1
  122. package/dist/components/Datepicker/utils/parseDateForDateField/parseDateForDateField.d.ts +0 -20
  123. package/lib/components/Datepicker/subcomponents/CalendarGrid/CalendarGrid.tsx +0 -59
  124. package/lib/components/Datepicker/subcomponents/CalendarGrid/index.ts +0 -1
  125. package/lib/components/Datepicker/subcomponents/CalendarMenu/CalendarMenu.tsx +0 -64
  126. package/lib/components/Datepicker/subcomponents/CalendarMenu/index.ts +0 -1
  127. package/lib/components/Datepicker/subcomponents/ColumnHeadings/ColumnHeadings.tsx +0 -35
  128. package/lib/components/Datepicker/subcomponents/ColumnHeadings/index.ts +0 -1
  129. package/lib/components/Datepicker/subcomponents/DateField/DateField.tsx +0 -155
  130. package/lib/components/Datepicker/subcomponents/DateField/__tests__/DateField.test.tsx +0 -191
  131. package/lib/components/Datepicker/subcomponents/DateField/index.ts +0 -1
  132. package/lib/components/Datepicker/subcomponents/Day/Day.tsx +0 -94
  133. package/lib/components/Datepicker/subcomponents/Day/index.ts +0 -1
  134. package/lib/components/Datepicker/subcomponents/MonthSelector/index.ts +0 -1
  135. package/lib/components/Datepicker/subcomponents/Native/Native.tsx +0 -59
  136. package/lib/components/Datepicker/subcomponents/Native/index.ts +0 -1
  137. package/lib/components/Datepicker/utils/parseDateForDateField/parseDateForDateField.test.ts +0 -41
  138. package/lib/components/Datepicker/utils/parseDateForDateField/parseDateForDateField.ts +0 -48
  139. /package/dist/components/{Datepicker/subcomponents/DateField/__tests__/DateField.test.d.ts → Calendar/__tests__/Calendar.test.d.ts} +0 -0
  140. /package/dist/components/{Datepicker/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.test.d.ts → Calendar/utils/getAcademicWeekNumbers/getAcademicWeekNumbers.test.d.ts} +0 -0
  141. /package/dist/components/{Datepicker → Calendar}/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.d.ts +0 -0
  142. /package/dist/components/{Datepicker/utils/parseDateForDateField/parseDateForDateField.test.d.ts → Calendar/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.test.d.ts} +0 -0
@@ -0,0 +1,209 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import Calendar from './Calendar';
3
+ import { useArgs } from '@storybook/preview-api';
4
+ import type { AcademicWeek } from './Calendar.types';
5
+
6
+ const meta = {
7
+ title: 'Components/Work in progress/Calendar',
8
+ component: Calendar,
9
+ parameters: { layout: 'padded' },
10
+ argTypes: {
11
+ pickedDate: { control: { type: 'date' } },
12
+ minDate: { control: { type: 'date' } },
13
+ maxDate: { control: { type: 'date' } },
14
+ showAcademicWeeks: { control: { type: 'boolean' } },
15
+ testId: { control: { type: 'text' } },
16
+ },
17
+ tags: ['autodocs'],
18
+ } satisfies Meta<typeof Calendar>;
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof meta>;
22
+
23
+ // Convert UNIX timestamp from Storybook controls to `Date` object
24
+ // https://storybook.js.org/docs/essentials/controls#annotation
25
+ const parseDateFromUNIXTimestamp = (timestamp: number) => {
26
+ const date = new Date(timestamp);
27
+ return isNaN(date.getTime()) ? null : date;
28
+ };
29
+
30
+ const dateToISOString = (date: Date) => {
31
+ return date.toISOString().split('T')[0];
32
+ };
33
+
34
+ export const Default: Story = {
35
+ render: () => {
36
+ const [args, updateArgs] = useArgs();
37
+ args.pickedDate = args.pickedDate
38
+ ? parseDateFromUNIXTimestamp(args.pickedDate)
39
+ : null;
40
+ const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
41
+ return (
42
+ <Calendar
43
+ {...args}
44
+ onDatePick={onDatePick}
45
+ />
46
+ );
47
+ },
48
+ };
49
+
50
+ // Story repeated in Datepicker.stories.tsx
51
+ export const WithEvents: Story = {
52
+ name: 'With events',
53
+ args: {
54
+ // IIFE gives us event dots for the current month
55
+ events: (() => {
56
+ const currentDate = new Date();
57
+ const currentYear = currentDate.getFullYear();
58
+ const currentMonth = currentDate.getMonth();
59
+ return [
60
+ // Grey event dots
61
+ { date: dateToISOString(new Date(currentYear, currentMonth, -2)) },
62
+ { date: dateToISOString(new Date(currentYear, currentMonth, -1)) },
63
+ { date: dateToISOString(new Date(currentYear, currentMonth, -1)) },
64
+ { date: dateToISOString(new Date(currentYear, currentMonth, 0)) },
65
+ { date: dateToISOString(new Date(currentYear, currentMonth, 0)) },
66
+ { date: dateToISOString(new Date(currentYear, currentMonth, 0)) },
67
+ // Blue event dots
68
+ { date: dateToISOString(new Date(currentYear, currentMonth, 1)) },
69
+ { date: dateToISOString(new Date(currentYear, currentMonth, 2)) },
70
+ { date: dateToISOString(new Date(currentYear, currentMonth, 2)) },
71
+ { date: dateToISOString(new Date(currentYear, currentMonth, 3)) },
72
+ { date: dateToISOString(new Date(currentYear, currentMonth, 3)) },
73
+ { date: dateToISOString(new Date(currentYear, currentMonth, 3)) },
74
+ ];
75
+ })(),
76
+ },
77
+ render: () => {
78
+ const [args, updateArgs] = useArgs();
79
+ args.pickedDate = args.pickedDate
80
+ ? parseDateFromUNIXTimestamp(args.pickedDate)
81
+ : null;
82
+ const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
83
+ return (
84
+ <Calendar
85
+ {...args}
86
+ onDatePick={onDatePick}
87
+ />
88
+ );
89
+ },
90
+ };
91
+
92
+ const academicWeeks: AcademicWeek[] = [
93
+ { start: '2025-08-25', number: 1 },
94
+ { start: '2025-09-01', number: 2 },
95
+ { start: '2025-09-08', number: 3 },
96
+ { start: '2025-09-15', number: 4 },
97
+ { start: '2025-09-22', number: 5 },
98
+ { start: '2025-09-29', number: 6 },
99
+ { start: '2025-10-06', number: 7 },
100
+ { start: '2025-10-13', number: 8 },
101
+ { start: '2025-10-20', number: 9 },
102
+ { start: '2025-10-27', number: 10 },
103
+ { start: '2025-11-03', number: 11 },
104
+ { start: '2025-11-10', number: 12 },
105
+ { start: '2025-11-17', number: 13 },
106
+ { start: '2025-11-24', number: 14 },
107
+ { start: '2025-12-01', number: 15 },
108
+ { start: '2025-12-08', number: 16 },
109
+ { start: '2025-12-15', number: 17 },
110
+ { start: '2025-12-22', number: 18 },
111
+ { start: '2025-12-29', number: 19 },
112
+ { start: '2026-01-05', number: 20 },
113
+ { start: '2026-01-12', number: 21 },
114
+ { start: '2026-01-19', number: 22 },
115
+ { start: '2026-01-26', number: 23 },
116
+ { start: '2026-02-02', number: 24 },
117
+ { start: '2026-02-09', number: 25 },
118
+ { start: '2026-02-16', number: 26 },
119
+ { start: '2026-02-23', number: 27 },
120
+ { start: '2026-03-02', number: 28 },
121
+ { start: '2026-03-09', number: 29 },
122
+ { start: '2026-03-16', number: 30 },
123
+ { start: '2026-03-23', number: 31 },
124
+ { start: '2026-03-30', number: 32 },
125
+ { start: '2026-04-06', number: 33 },
126
+ { start: '2026-04-13', number: 34 },
127
+ { start: '2026-04-20', number: 35 },
128
+ { start: '2026-04-27', number: 36 },
129
+ { start: '2026-05-04', number: 37 },
130
+ { start: '2026-05-11', number: 38 },
131
+ { start: '2026-05-18', number: 39 },
132
+ { start: '2026-05-25', number: 40 },
133
+ { start: '2026-06-01', number: 41 },
134
+ { start: '2026-06-08', number: 42 },
135
+ { start: '2026-06-15', number: 43 },
136
+ { start: '2026-06-22', number: 44 },
137
+ { start: '2026-06-29', number: 45 },
138
+ { start: '2026-07-06', number: 46 },
139
+ { start: '2026-07-13', number: 47 },
140
+ { start: '2026-07-20', number: 48 },
141
+ { start: '2026-07-27', number: 49 },
142
+ { start: '2026-08-03', number: 50 },
143
+ { start: '2026-08-10', number: 51 },
144
+ { start: '2026-08-17', number: 52 },
145
+ { start: '2026-08-24', number: 53 },
146
+ ];
147
+
148
+ // Story repeated in Datepicker.stories.tsx
149
+ export const WithAcademicWeeks: Story = {
150
+ name: 'With academic weeks',
151
+ args: {
152
+ showAcademicWeeks: true,
153
+ academicWeeks: academicWeeks,
154
+ pickedDate: new Date(academicWeeks[0].start), // So the week numbers appear on story mount
155
+ },
156
+ render: () => {
157
+ const [args, updateArgs] = useArgs();
158
+ args.pickedDate = args.pickedDate
159
+ ? parseDateFromUNIXTimestamp(args.pickedDate)
160
+ : null;
161
+ const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
162
+ return (
163
+ <Calendar
164
+ {...args}
165
+ onDatePick={onDatePick}
166
+ />
167
+ );
168
+ },
169
+ };
170
+
171
+ // Story repeated in Datepicker.stories.tsx
172
+ export const MinMaxDates: Story = {
173
+ name: 'With min and max dates',
174
+ args: {
175
+ // Initialise min date as 5 days into the current month
176
+ minDate: (() => {
177
+ const now = new Date();
178
+ return new Date(now.getFullYear(), now.getMonth(), 5)
179
+ .toISOString()
180
+ .split('T')[0];
181
+ })(),
182
+ // Initialise max date as 5 days before the last day of the current month
183
+ maxDate: (() => {
184
+ const now = new Date();
185
+ // Get last day of current month
186
+ const lastDay = new Date(
187
+ now.getFullYear(),
188
+ now.getMonth() + 1,
189
+ 0
190
+ ).getDate();
191
+ return new Date(now.getFullYear(), now.getMonth(), lastDay - 5)
192
+ .toISOString()
193
+ .split('T')[0];
194
+ })(),
195
+ },
196
+ render: () => {
197
+ const [args, updateArgs] = useArgs();
198
+ args.pickedDate = args.pickedDate
199
+ ? parseDateFromUNIXTimestamp(args.pickedDate)
200
+ : null;
201
+ const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
202
+ return (
203
+ <Calendar
204
+ {...args}
205
+ onDatePick={onDatePick}
206
+ />
207
+ );
208
+ },
209
+ };
@@ -0,0 +1,121 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+ import { Controls, AcademicWeeks, Grid } from './subcomponents';
4
+ import { normaliseMonth, parseDateFromString } from './utils';
5
+ import { useTheme } from '../../theme';
6
+ import type { CalendarProps } from './Calendar.types';
7
+
8
+ const NAME = 'ucl-uikit-calendar';
9
+
10
+ const Calendar = ({
11
+ pickedDate = null,
12
+ onDatePick,
13
+ minDate = null,
14
+ maxDate = null,
15
+ events = [],
16
+ showAcademicWeeks = false,
17
+ academicWeeks = [],
18
+ testId = NAME,
19
+ className,
20
+ }: CalendarProps) => {
21
+ const [theme] = useTheme();
22
+
23
+ if (pickedDate && isNaN(pickedDate.getTime())) {
24
+ console.warn('Calendar: pickedDate is invalid, defaulting to null');
25
+ pickedDate = null;
26
+ }
27
+
28
+ // Used to track prop value changes
29
+ const pickedDateRef = useRef<Date | null>(pickedDate ?? null);
30
+
31
+ // Determines the month currently displayed in the calendar
32
+ const [displayMonth, setDisplayMonth] = useState<Date>(
33
+ // Display month initialises as either the picked date's month or the current month
34
+ normaliseMonth(pickedDate ?? new Date()) as Date
35
+ );
36
+
37
+ // Parse min and max dates from strings to Date objects
38
+ const minDateParsed = minDate ? parseDateFromString(minDate) : null;
39
+ const maxDateParsed = maxDate ? parseDateFromString(maxDate) : null;
40
+
41
+ // Snap displayed month to the picked date's month if it changes
42
+ useEffect(() => {
43
+ if (
44
+ // If the picked date is valid
45
+ pickedDate &&
46
+ !isNaN(pickedDate.getTime()) &&
47
+ // If the picked date has changed
48
+ pickedDateRef.current?.getTime() !== pickedDate.getTime()
49
+ ) {
50
+ // If the picked date is not in the currently displayed month
51
+ if (
52
+ pickedDate.getMonth() !== displayMonth.getMonth() ||
53
+ pickedDate.getFullYear() !== displayMonth.getFullYear()
54
+ ) {
55
+ // Update the display month to the picked date's month
56
+ setDisplayMonth(normaliseMonth(pickedDate) as Date);
57
+ }
58
+ }
59
+ // Update the ref to the new picked date
60
+ pickedDateRef.current = pickedDate;
61
+ }, [pickedDate, displayMonth]);
62
+
63
+ const handleMonthChange = (change: number) => {
64
+ const newDate = new Date(displayMonth);
65
+ newDate.setMonth(newDate.getMonth() + change);
66
+ setDisplayMonth(normaliseMonth(newDate) as Date);
67
+ };
68
+
69
+ const width = showAcademicWeeks ? '370' : '312';
70
+
71
+ const baseStyle = css`
72
+ display: flex;
73
+ flex-direction: column;
74
+ align-items: center;
75
+ gap: 16px;
76
+ width: ${width}px;
77
+ box-sizing: border-box;
78
+ border: 1px solid ${theme.color.neutral.grey20};
79
+ padding: 16px;
80
+ background-color: ${theme.color.neutral.white};
81
+ `;
82
+
83
+ const innerContainerStyle = css`
84
+ display: flex;
85
+ flex-direction: row;
86
+ gap: 8px;
87
+ width: 100%;
88
+ `;
89
+
90
+ const style = cx(testId, baseStyle, className);
91
+
92
+ return (
93
+ <div
94
+ className={style}
95
+ data-testid={testId}
96
+ >
97
+ <Controls
98
+ month={displayMonth}
99
+ changeMonth={handleMonthChange}
100
+ />
101
+ <div className={innerContainerStyle}>
102
+ {showAcademicWeeks && (
103
+ <AcademicWeeks
104
+ date={displayMonth}
105
+ weeks={academicWeeks}
106
+ />
107
+ )}
108
+ <Grid
109
+ month={displayMonth}
110
+ pickedDate={pickedDate}
111
+ onDatePick={onDatePick}
112
+ minDate={minDateParsed}
113
+ maxDate={maxDateParsed}
114
+ events={events}
115
+ />
116
+ </div>
117
+ </div>
118
+ );
119
+ };
120
+
121
+ export default Calendar;
@@ -0,0 +1,21 @@
1
+ // Used to display EventDots inside `<Day>` component
2
+ export type CalendarEvent = {
3
+ date: string; // Date string in YYYY-MM-DD format
4
+ };
5
+
6
+ export type AcademicWeek = {
7
+ start: string; // ISO date string: YYYY-MM-DD format
8
+ number: number;
9
+ };
10
+
11
+ export interface CalendarProps {
12
+ pickedDate?: Date | null;
13
+ onDatePick?: (date: Date | null, event?: React.SyntheticEvent) => void;
14
+ minDate?: string | null; // ISO date string: YYYY-MM-DD format
15
+ maxDate?: string | null; // ISO date string: YYYY-MM-DD format
16
+ events?: CalendarEvent[];
17
+ showAcademicWeeks?: boolean;
18
+ academicWeeks?: AcademicWeek[];
19
+ testId?: string;
20
+ className?: string;
21
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, test, expect, vi, beforeAll } from 'vitest';
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import { ThemeContextProvider } from '../../../theme';
4
+ import Calendar from '../Calendar';
5
+
6
+ const defaultTestId = 'ucl-uikit-calendar';
7
+
8
+ const customRender = (ui: React.ReactElement) => {
9
+ return render(ui, {
10
+ wrapper: ({ children }) => (
11
+ <ThemeContextProvider>{children}</ThemeContextProvider>
12
+ ),
13
+ });
14
+ };
15
+
16
+ describe('Calendar', () => {
17
+ beforeAll(() => {
18
+ // Snapshot tests will fail because the Calendar Grid includes styling for "today's date".
19
+ // Therefore, we need to use Vitest to mock the current date.
20
+ vi.useFakeTimers();
21
+ vi.setSystemTime(new Date('2025-03-10')); // Arbitrary fixed date -- Alex's birthday :)
22
+ });
23
+
24
+ // Snapshot tests
25
+
26
+ test('Snapshot: no date provided', () => {
27
+ const renderResult = customRender(<Calendar />);
28
+ expect(renderResult.container.firstChild).toMatchSnapshot();
29
+ });
30
+
31
+ test('Snapshot: with date provided', () => {
32
+ const renderResult = customRender(
33
+ <Calendar pickedDate={new Date('2025-01-06')} />
34
+ );
35
+ expect(renderResult.container.firstChild).toMatchSnapshot();
36
+ });
37
+
38
+ // Unit tests
39
+
40
+ test('Can be found via default test id', () => {
41
+ customRender(<Calendar />);
42
+ const calendar = screen.getByTestId(defaultTestId);
43
+ expect(calendar).toBeInTheDocument();
44
+ });
45
+
46
+ test('Can be found custom test id', () => {
47
+ const customTestId = '123';
48
+ customRender(<Calendar testId={customTestId} />);
49
+ const calendar = screen.getByTestId(customTestId);
50
+ expect(calendar).toBeInTheDocument();
51
+ });
52
+
53
+ test('Can pick a date', () => {
54
+ const dateToPick = new Date('2025-03-22');
55
+ const onDatePick = vi.fn();
56
+ customRender(
57
+ <Calendar
58
+ pickedDate={new Date('2025-03-01')} // To ensure the calendar is displayed for March 2025
59
+ onDatePick={onDatePick}
60
+ />
61
+ );
62
+ const dayButton = screen.getByText('22');
63
+
64
+ fireEvent.click(dayButton);
65
+ // OnDatePick = (date: Date | null, event: React.SyntheticEvent) => void
66
+ const [firstArg, secondArg] = onDatePick.mock.calls[0];
67
+ expect(firstArg).toEqual(dateToPick);
68
+ expect(secondArg).toHaveProperty('type', 'click');
69
+ expect(secondArg).toHaveProperty('target');
70
+ });
71
+ });