uikit-react-public 0.14.21 → 0.17.4

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 (158) hide show
  1. package/README.md +4 -2
  2. package/dist/components/Accordion/Accordion.Heading.d.ts +4 -4
  3. package/dist/components/Accordion/Accordion.Panel.d.ts +2 -2
  4. package/dist/components/Accordion/Accordion.d.ts +1 -1
  5. package/dist/components/Accordion/Accordion.stories.d.ts +57 -0
  6. package/dist/components/Accordion/index.d.ts +2 -0
  7. package/dist/components/Avatar/Avatar.stories.d.ts +107 -1
  8. package/dist/components/Button/Button.d.ts +1 -0
  9. package/dist/components/Calendar/index.d.ts +1 -1
  10. package/dist/components/Datepicker/Datepicker.d.ts +1 -1
  11. package/dist/components/Datepicker/Datepicker.stories.d.ts +4 -3
  12. package/dist/components/Datepicker/Datepicker.types.d.ts +4 -5
  13. package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +4 -1
  14. package/dist/components/Datepicker/subcomponents/DatepickerInput.d.ts +15 -2
  15. package/dist/components/Datepicker/subcomponents/Panel.d.ts +1 -1
  16. package/dist/components/Datepicker/subcomponents/VisibleField.d.ts +6 -1
  17. package/dist/components/Datepicker/subcomponents/index.d.ts +0 -1
  18. package/dist/components/Datepicker/utils/index.d.ts +0 -1
  19. package/dist/components/Dialog/BaseDialog.d.ts +2 -1
  20. package/dist/components/Dialog/Dialog.d.ts +2 -0
  21. package/dist/components/Header/Header.d.ts +4 -1
  22. package/dist/components/Header/Header.stories.d.ts +40 -0
  23. package/dist/components/Main/Main.d.ts +21 -0
  24. package/dist/components/Main/Main.stories.d.ts +15 -0
  25. package/dist/components/Main/index.d.ts +2 -0
  26. package/dist/components/NativeDatepicker/NativeDatepicker.d.ts +3 -0
  27. package/dist/components/NativeDatepicker/NativeDatepicker.stories.d.ts +36 -0
  28. package/dist/components/NativeDatepicker/NativeDatepicker.types.d.ts +10 -0
  29. package/dist/components/NativeDatepicker/index.d.ts +2 -0
  30. package/dist/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.d.ts +1 -1
  31. package/dist/components/NativeDatepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts +1 -0
  32. package/dist/components/NativeDatepicker/utils/index.d.ts +1 -0
  33. package/dist/components/Select/Select.stories.d.ts +154 -2
  34. package/dist/components/Select/Select.types.d.ts +51 -22
  35. package/dist/components/Select/subcomponents/CustomOption.d.ts +1 -1
  36. package/dist/components/Select/subcomponents/CustomSelect.d.ts +3 -2
  37. package/dist/components/Select/subcomponents/FilterInput.d.ts +14 -0
  38. package/dist/components/Select/subcomponents/NativeSelect.d.ts +5 -1
  39. package/dist/components/Select/subcomponents/VisibleField.d.ts +3 -1
  40. package/dist/components/Select/subcomponents/index.d.ts +1 -0
  41. package/dist/components/WeekPicker/WeekPicker.d.ts +2 -2
  42. package/dist/components/WeekPicker/WeekPicker.stories.d.ts +41 -0
  43. package/dist/components/WeekPicker/WeekPicker.types.d.ts +16 -0
  44. package/dist/components/WeekPicker/index.d.ts +1 -0
  45. package/dist/components/WeekPicker/subcomponents/CustomDatepicker.d.ts +1 -1
  46. package/dist/components/index.d.ts +8 -0
  47. package/dist/hooks/useFocusTrap.d.ts +2 -1
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.js +4366 -3768
  50. package/dist/utils/__tests__/announce.test.d.ts +1 -0
  51. package/dist/utils/announce.d.ts +6 -0
  52. package/dist/utils/index.d.ts +1 -0
  53. package/lib/components/Accordion/Accordion.Heading.tsx +27 -8
  54. package/lib/components/Accordion/Accordion.Panel.tsx +11 -3
  55. package/lib/components/Accordion/Accordion.stories.tsx +139 -0
  56. package/lib/components/Accordion/Accordion.tsx +10 -8
  57. package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +7 -7
  58. package/lib/components/Accordion/index.ts +2 -0
  59. package/lib/components/Alert/Alert.stories.tsx +1 -1
  60. package/lib/components/Avatar/Avatar.mdx +117 -0
  61. package/lib/components/Avatar/Avatar.stories.tsx +110 -2
  62. package/lib/components/Blanket/Blanket.stories.tsx +1 -1
  63. package/lib/components/Button/Button.stories.tsx +1 -1
  64. package/lib/components/Button/Button.tsx +1 -0
  65. package/lib/components/Calendar/Calendar.stories.tsx +12 -32
  66. package/lib/components/Calendar/__tests__/Calendar.test.tsx +23 -15
  67. package/lib/components/Calendar/index.ts +1 -5
  68. package/lib/components/Calendar/subcomponents/AcademicWeeks.tsx +2 -1
  69. package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +5 -1
  70. package/lib/components/Calendar/subcomponents/EventDot.tsx +2 -1
  71. package/lib/components/Calendar/subcomponents/index.ts +1 -1
  72. package/lib/components/Calendar/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.ts +43 -11
  73. package/lib/components/Calendar/utils/normaliseMonth/normaliseMonth.test.ts +5 -5
  74. package/lib/components/Datepicker/Datepicker.lld.md +108 -0
  75. package/lib/components/Datepicker/Datepicker.stories.tsx +44 -5
  76. package/lib/components/Datepicker/Datepicker.tsx +14 -36
  77. package/lib/components/Datepicker/Datepicker.types.ts +5 -14
  78. package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +150 -8
  79. package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +10 -4
  80. package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +39 -5
  81. package/lib/components/Datepicker/subcomponents/DatepickerInput.tsx +30 -17
  82. package/lib/components/Datepicker/subcomponents/Panel.tsx +6 -2
  83. package/lib/components/Datepicker/subcomponents/VisibleField.tsx +40 -3
  84. package/lib/components/Datepicker/subcomponents/index.ts +0 -1
  85. package/lib/components/Datepicker/utils/index.ts +0 -1
  86. package/lib/components/Dialog/BaseDialog.tsx +11 -0
  87. package/lib/components/Dialog/Dialog.tsx +8 -1
  88. package/lib/components/Dialog/DialogBody.tsx +5 -1
  89. package/lib/components/Dialog/DialogHeader.tsx +2 -1
  90. package/lib/components/Divider/Divider.stories.tsx +1 -1
  91. package/lib/components/Field/ErrorText.tsx +1 -0
  92. package/lib/components/Field/Field.stories.tsx +1 -1
  93. package/lib/components/Field/__tests__/Field.test.tsx +13 -0
  94. package/lib/components/FileInput/FileInput.stories.tsx +1 -1
  95. package/lib/components/Footer/Footer.stories.tsx +1 -1
  96. package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +3 -3
  97. package/lib/components/Header/Header.mdx +52 -0
  98. package/lib/components/Header/Header.stories.tsx +98 -0
  99. package/lib/components/Header/Header.tsx +51 -6
  100. package/lib/components/Header/__tests__/Header.test.tsx +17 -1
  101. package/lib/components/Heading/Heading.stories.tsx +1 -1
  102. package/lib/components/Icon/Icon.stories.tsx +1 -1
  103. package/lib/components/IconButton/IconButton.stories.tsx +1 -1
  104. package/lib/components/Input/Input.stories.tsx +1 -1
  105. package/lib/components/Label/Label.stories.tsx +1 -1
  106. package/lib/components/Main/Main.stories.tsx +36 -0
  107. package/lib/components/Main/Main.tsx +46 -0
  108. package/lib/components/Main/__tests__/Main.test.tsx +80 -0
  109. package/lib/components/Main/__tests__/__snapshots__/Main.test.tsx.snap +33 -0
  110. package/lib/components/Main/index.ts +2 -0
  111. package/lib/components/NativeDatepicker/NativeDatepicker.stories.tsx +100 -0
  112. package/lib/components/{Datepicker/subcomponents → NativeDatepicker}/NativeDatepicker.tsx +14 -15
  113. package/lib/components/NativeDatepicker/NativeDatepicker.types.ts +19 -0
  114. package/lib/components/NativeDatepicker/index.ts +2 -0
  115. package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.ts +1 -1
  116. package/lib/components/NativeDatepicker/utils/index.ts +1 -0
  117. package/lib/components/Pagination/PaginationControls.tsx +55 -12
  118. package/lib/components/Pagination/PaginationInfo.tsx +5 -1
  119. package/lib/components/Paragraph/Paragraph.stories.tsx +1 -1
  120. package/lib/components/Search/Search.stories.tsx +1 -1
  121. package/lib/components/Search/Search.tsx +4 -1
  122. package/lib/components/Search/__tests__/Search.test.tsx +19 -1
  123. package/lib/components/Select/Select.mdx +169 -0
  124. package/lib/components/Select/Select.stories.tsx +191 -43
  125. package/lib/components/Select/Select.tsx +36 -12
  126. package/lib/components/Select/Select.types.ts +66 -48
  127. package/lib/components/Select/__tests__/Select.test.tsx +448 -7
  128. package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
  129. package/lib/components/Select/subcomponents/CustomOption.tsx +2 -1
  130. package/lib/components/Select/subcomponents/CustomSelect.tsx +303 -33
  131. package/lib/components/Select/subcomponents/FilterInput.tsx +80 -0
  132. package/lib/components/Select/subcomponents/NativeSelect.tsx +13 -1
  133. package/lib/components/Select/subcomponents/VisibleField.tsx +11 -3
  134. package/lib/components/Select/subcomponents/index.tsx +1 -0
  135. package/lib/components/Snackbar/Snackbar.stories.tsx +1 -1
  136. package/lib/components/Spinner/Spinner.stories.tsx +1 -1
  137. package/lib/components/Textarea/Textarea.stories.tsx +1 -1
  138. package/lib/components/Timepicker/Timepicker.tsx +4 -0
  139. package/lib/components/Timepicker/__tests__/__snapshots__/Timepicker.test.tsx.snap +2 -2
  140. package/lib/components/Toggle/Toggle.stories.tsx +1 -1
  141. package/lib/components/Tooltip/Tooltip.stories.tsx +1 -1
  142. package/lib/components/WeekPicker/WeekPicker.stories.tsx +147 -0
  143. package/lib/components/WeekPicker/WeekPicker.tsx +2 -2
  144. package/lib/components/WeekPicker/WeekPicker.types.ts +21 -0
  145. package/lib/components/WeekPicker/index.ts +1 -0
  146. package/lib/components/WeekPicker/subcomponents/CustomDatepicker.tsx +1 -1
  147. package/lib/components/common/Common.mdx +1 -1
  148. package/lib/components/index.ts +11 -2
  149. package/lib/hooks/useFocusTrap.ts +40 -4
  150. package/lib/index.ts +1 -0
  151. package/lib/utils/__tests__/announce.test.ts +121 -0
  152. package/lib/utils/announce.ts +134 -0
  153. package/lib/utils/index.ts +1 -0
  154. package/package.json +3 -6
  155. package/dist/components/Datepicker/subcomponents/NativeDatepicker.d.ts +0 -6
  156. package/lib/components/Accordion/Accordion.stories.tsx.NOT_READY +0 -93
  157. /package/dist/components/{Datepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts → Main/__tests__/Main.test.d.ts} +0 -0
  158. /package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.test.ts +0 -0
@@ -34,17 +34,12 @@ const dateToISOString = (date: Date) => {
34
34
  export const Default: Story = {
35
35
  render: () => {
36
36
  const [args, updateArgs] = useArgs();
37
- args.pickedDate = args.pickedDate
38
- ? parseDateFromUNIXTimestamp(args.pickedDate)
39
- : null;
37
+ args.pickedDate = args.pickedDate ? parseDateFromUNIXTimestamp(args.pickedDate) : null;
40
38
  const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
41
39
  return (
42
- <Calendar
43
- {...args}
44
- onDatePick={onDatePick}
45
- />
40
+ <Calendar {...args} onDatePick={onDatePick} />
46
41
  );
47
- },
42
+ }
48
43
  };
49
44
 
50
45
  // Story repeated in Datepicker.stories.tsx
@@ -76,17 +71,12 @@ export const WithEvents: Story = {
76
71
  },
77
72
  render: () => {
78
73
  const [args, updateArgs] = useArgs();
79
- args.pickedDate = args.pickedDate
80
- ? parseDateFromUNIXTimestamp(args.pickedDate)
81
- : null;
74
+ args.pickedDate = args.pickedDate ? parseDateFromUNIXTimestamp(args.pickedDate) : null;
82
75
  const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
83
76
  return (
84
- <Calendar
85
- {...args}
86
- onDatePick={onDatePick}
87
- />
77
+ <Calendar {...args} onDatePick={onDatePick} />
88
78
  );
89
- },
79
+ }
90
80
  };
91
81
 
92
82
  const academicWeeks: AcademicWeek[] = [
@@ -155,17 +145,12 @@ export const WithAcademicWeeks: Story = {
155
145
  },
156
146
  render: () => {
157
147
  const [args, updateArgs] = useArgs();
158
- args.pickedDate = args.pickedDate
159
- ? parseDateFromUNIXTimestamp(args.pickedDate)
160
- : null;
148
+ args.pickedDate = args.pickedDate ? parseDateFromUNIXTimestamp(args.pickedDate) : null;
161
149
  const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
162
150
  return (
163
- <Calendar
164
- {...args}
165
- onDatePick={onDatePick}
166
- />
151
+ <Calendar {...args} onDatePick={onDatePick} />
167
152
  );
168
- },
153
+ }
169
154
  };
170
155
 
171
156
  // Story repeated in Datepicker.stories.tsx
@@ -195,15 +180,10 @@ export const MinMaxDates: Story = {
195
180
  },
196
181
  render: () => {
197
182
  const [args, updateArgs] = useArgs();
198
- args.pickedDate = args.pickedDate
199
- ? parseDateFromUNIXTimestamp(args.pickedDate)
200
- : null;
183
+ args.pickedDate = args.pickedDate ? parseDateFromUNIXTimestamp(args.pickedDate) : null;
201
184
  const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
202
185
  return (
203
- <Calendar
204
- {...args}
205
- onDatePick={onDatePick}
206
- />
186
+ <Calendar {...args} onDatePick={onDatePick} />
207
187
  );
208
- },
188
+ }
209
189
  };
@@ -8,12 +8,15 @@ const defaultTestId = 'ucl-uikit-calendar';
8
8
  const customRender = (ui: React.ReactElement) => {
9
9
  return render(ui, {
10
10
  wrapper: ({ children }) => (
11
- <ThemeContextProvider>{children}</ThemeContextProvider>
11
+ <ThemeContextProvider>
12
+ {children}
13
+ </ThemeContextProvider>
12
14
  ),
13
15
  });
14
16
  };
15
17
 
16
- describe('Calendar', () => {
18
+ describe("Calendar", () => {
19
+
17
20
  beforeAll(() => {
18
21
  // Snapshot tests will fail because the Calendar Grid includes styling for "today's date".
19
22
  // Therefore, we need to use Vitest to mock the current date.
@@ -21,43 +24,48 @@ describe('Calendar', () => {
21
24
  vi.setSystemTime(new Date('2025-03-10')); // Arbitrary fixed date -- Alex's birthday :)
22
25
  });
23
26
 
24
- // Snapshot tests
27
+ // Snapshot tests
25
28
 
26
- test('Snapshot: no date provided', () => {
27
- const renderResult = customRender(<Calendar />);
29
+ test("Snapshot: no date provided", () => {
30
+ const renderResult = customRender(
31
+ <Calendar />
32
+ );
28
33
  expect(renderResult.container.firstChild).toMatchSnapshot();
29
34
  });
30
35
 
31
- test('Snapshot: with date provided', () => {
36
+ test("Snapshot: with date provided", () => {
32
37
  const renderResult = customRender(
33
38
  <Calendar pickedDate={new Date('2025-01-06')} />
34
39
  );
35
40
  expect(renderResult.container.firstChild).toMatchSnapshot();
36
41
  });
37
42
 
38
- // Unit tests
43
+ // Unit tests
39
44
 
40
- test('Can be found via default test id', () => {
41
- customRender(<Calendar />);
45
+ test("Can be found via default test id", () => {
46
+ customRender(
47
+ <Calendar />
48
+ );
42
49
  const calendar = screen.getByTestId(defaultTestId);
43
50
  expect(calendar).toBeInTheDocument();
44
51
  });
45
52
 
46
- test('Can be found custom test id', () => {
53
+ test("Can be found custom test id", () => {
47
54
  const customTestId = '123';
48
- customRender(<Calendar testId={customTestId} />);
55
+ customRender(
56
+ <Calendar testId={customTestId} />
57
+ );
49
58
  const calendar = screen.getByTestId(customTestId);
50
59
  expect(calendar).toBeInTheDocument();
51
60
  });
52
61
 
53
- test('Can pick a date', () => {
62
+ test("Can pick a date", () => {
54
63
  const dateToPick = new Date('2025-03-22');
55
64
  const onDatePick = vi.fn();
56
65
  customRender(
57
66
  <Calendar
58
- pickedDate={new Date('2025-03-01')} // To ensure the calendar is displayed for March 2025
59
- onDatePick={onDatePick}
60
- />
67
+ pickedDate={new Date('2025-03-01')} // To ensure the calendar is displayed for March 2025
68
+ onDatePick={onDatePick} />
61
69
  );
62
70
  const dayButton = screen.getByText('22');
63
71
 
@@ -1,6 +1,2 @@
1
1
  export { default } from './Calendar';
2
- export type {
3
- CalendarProps,
4
- CalendarEvent,
5
- AcademicWeek,
6
- } from './Calendar.types';
2
+ export type { CalendarProps, CalendarEvent, AcademicWeek } from './Calendar.types';
@@ -13,6 +13,7 @@ interface AcademicWeeksProps {
13
13
  }
14
14
 
15
15
  const AcademicWeeks = ({ date, weeks }: AcademicWeeksProps) => {
16
+
16
17
  const [theme] = useTheme();
17
18
 
18
19
  const academicWeekNumbers = getAcademicWeekNumbers(weeks, date);
@@ -41,6 +42,6 @@ const AcademicWeeks = ({ date, weeks }: AcademicWeeksProps) => {
41
42
  ))}
42
43
  </div>
43
44
  );
44
- };
45
+ }
45
46
 
46
47
  export default AcademicWeeks;
@@ -9,7 +9,11 @@ interface ColumnHeadingProps {
9
9
 
10
10
  const NAME = 'ucl-uikit-calendar__column-heading';
11
11
 
12
- const ColumnHeading = ({ index, day, isWeekend }: ColumnHeadingProps) => {
12
+ const ColumnHeading = ({
13
+ index,
14
+ day,
15
+ isWeekend
16
+ }: ColumnHeadingProps) => {
13
17
  const [theme] = useTheme();
14
18
 
15
19
  const baseStyle = css`
@@ -9,6 +9,7 @@ interface EventDotProps {
9
9
  }
10
10
 
11
11
  const EventDot = ({ inverted, inCurrentMonth }: EventDotProps) => {
12
+
12
13
  const [theme] = useTheme();
13
14
 
14
15
  const invertedColour = theme.color.neutral.white;
@@ -33,7 +34,7 @@ const EventDot = ({ inverted, inCurrentMonth }: EventDotProps) => {
33
34
  <div
34
35
  data-testid={NAME}
35
36
  className={style}
36
- />
37
+ />
37
38
  );
38
39
  };
39
40
 
@@ -4,4 +4,4 @@ export { default as AcademicWeek } from './AcademicWeek';
4
4
  export { default as Grid } from './Grid';
5
5
  export { default as ColumnHeading } from './ColumnHeading';
6
6
  export { default as Day } from './Day';
7
- export { default as EventDot } from './EventDot';
7
+ export { default as EventDot } from './EventDot';
@@ -17,15 +17,24 @@
17
17
  * ```
18
18
  */
19
19
  const getDatesForCalendarGrid = (date: Date): Date[] => {
20
- if (!date || !(date instanceof Date)) throw new Error('No date provided');
20
+ if (!date || !(date instanceof Date))
21
+ throw new Error('No date provided');
21
22
 
22
23
  const month = date.getMonth();
23
- const daysInMonth = new Date(date.getFullYear(), month + 1, 0).getDate();
24
+ const daysInMonth = new Date(
25
+ date.getFullYear(),
26
+ month + 1,
27
+ 0
28
+ ).getDate();
24
29
 
25
30
  // Get all days in current month
26
31
  const dates: Date[] = [];
27
32
  for (let day = 1; day <= daysInMonth; day++) {
28
- const newDate = new Date(date.getFullYear(), month, day);
33
+ const newDate = new Date(
34
+ date.getFullYear(),
35
+ month,
36
+ day
37
+ );
29
38
  if (newDate.getMonth() !== month) break;
30
39
  dates.push(newDate);
31
40
  }
@@ -34,14 +43,28 @@ const getDatesForCalendarGrid = (date: Date): Date[] => {
34
43
  // Sunday (0) should become 6
35
44
  // Monday (1) should become 0
36
45
  // Tuesday (2) should become 1, etc.
37
- const adjustDay = (day: number): number => (day === 0 ? 6 : day - 1);
46
+ const adjustDay = (day: number): number =>
47
+ day === 0 ? 6 : day - 1;
38
48
 
39
49
  // Calculate previous month's "grey days"
40
50
  const prevMonthGreyDays = [];
41
- const prevMonth = new Date(date.getFullYear(), month - 1, 1);
42
- const firstDayOfMonth = new Date(date.getFullYear(), month, 1).getDay();
43
- const numberOfDaysFromPrevMonth = adjustDay(firstDayOfMonth);
44
- const totalDaysInPrevMonth = new Date(date.getFullYear(), month, 0).getDate();
51
+ const prevMonth = new Date(
52
+ date.getFullYear(),
53
+ month - 1,
54
+ 1
55
+ );
56
+ const firstDayOfMonth = new Date(
57
+ date.getFullYear(),
58
+ month,
59
+ 1
60
+ ).getDay();
61
+ const numberOfDaysFromPrevMonth =
62
+ adjustDay(firstDayOfMonth);
63
+ const totalDaysInPrevMonth = new Date(
64
+ date.getFullYear(),
65
+ month,
66
+ 0
67
+ ).getDate();
45
68
 
46
69
  for (let i = numberOfDaysFromPrevMonth; i > 0; i--) {
47
70
  prevMonthGreyDays.push(
@@ -55,17 +78,26 @@ const getDatesForCalendarGrid = (date: Date): Date[] => {
55
78
 
56
79
  // Calculate next month's "grey days"
57
80
  const nextMonthGreyDays = [];
58
- const nextMonth = new Date(date.getFullYear(), month + 1, 1);
81
+ const nextMonth = new Date(
82
+ date.getFullYear(),
83
+ month + 1,
84
+ 1
85
+ );
59
86
  const lastDayOfMonth = new Date(
60
87
  date.getFullYear(),
61
88
  month,
62
89
  daysInMonth
63
90
  ).getDay();
64
- const numberOfDaysFromNextMonth = 6 - adjustDay(lastDayOfMonth);
91
+ const numberOfDaysFromNextMonth =
92
+ 6 - adjustDay(lastDayOfMonth);
65
93
 
66
94
  for (let i = 1; i <= numberOfDaysFromNextMonth; i++) {
67
95
  nextMonthGreyDays.push(
68
- new Date(nextMonth.getFullYear(), nextMonth.getMonth(), i)
96
+ new Date(
97
+ nextMonth.getFullYear(),
98
+ nextMonth.getMonth(),
99
+ i
100
+ )
69
101
  );
70
102
  }
71
103
 
@@ -2,15 +2,15 @@ import { describe, expect, test } from 'vitest';
2
2
  import normaliseMonth from './normaliseMonth';
3
3
 
4
4
  describe('Calendar: normaliseMonth', () => {
5
- test('Should return null if given undefined', () => {
5
+ test("Should return null if given undefined", () => {
6
6
  expect(normaliseMonth(undefined)).toBeNull();
7
7
  });
8
8
 
9
- test('Should return null if given null', () => {
9
+ test("Should return null if given null", () => {
10
10
  expect(normaliseMonth(null)).toBeNull();
11
11
  });
12
12
 
13
- test('Returns a Date object set to the first of the month', () => {
13
+ test("Returns a Date object set to the first of the month", () => {
14
14
  const date = new Date('2025-03-10');
15
15
  const normalisedDate = normaliseMonth(date);
16
16
  expect(normalisedDate).toBeInstanceOf(Date);
@@ -19,7 +19,7 @@ describe('Calendar: normaliseMonth', () => {
19
19
  expect(normalisedDate?.getDate()).toBe(1);
20
20
  });
21
21
 
22
- test('Works for a date within daylight saving time', () => {
22
+ test("Works for a date within daylight saving time", () => {
23
23
  const date = new Date('2025-06-15');
24
24
  const normalisedDate = normaliseMonth(date);
25
25
  expect(normalisedDate).toBeInstanceOf(Date);
@@ -28,7 +28,7 @@ describe('Calendar: normaliseMonth', () => {
28
28
  expect(normalisedDate?.getDate()).toBe(1);
29
29
  });
30
30
 
31
- test('Returned date is normalised for midnight', () => {
31
+ test("Returned date is normalised for midnight", () => {
32
32
  const date = new Date('2025-01-01T12:34:56');
33
33
  const normalisedDate = normaliseMonth(date);
34
34
  expect(normalisedDate).toBeInstanceOf(Date);
@@ -0,0 +1,108 @@
1
+ # Datepicker - Low-level design documentation
2
+
3
+ ## Overview
4
+
5
+ The `<Datepicker>` allows the user to enter a date into the system.
6
+
7
+ By default, it is a 'custom' implementation built using React logic.
8
+
9
+ A 'browser-native' implementation is also available from this component, using prop `native={true}`.
10
+
11
+ ## Design specifications
12
+
13
+ - Figma: [UCL Design System - UIKit Datepicker](https://www.figma.com/design/8Sm5PxWOWJYpYXAzhRzUzt/UCL-Design-System-UI-Kit?node-id=6531-3397)
14
+
15
+ ## Conceptual model
16
+
17
+ The Calendar displayed is a separate UI 'unit' in the Design System, so it is imported rather than defined in this component.
18
+
19
+ There are additional useful features, such as displaying when 'events' are in the calendar. The Datepicker needs to pass these to the Calendar.
20
+
21
+ The Datepicker supports minimum & maximum dates, and validates against a date being outside this range.
22
+
23
+ ### User interaction
24
+
25
+ The user wants to enter a date into the system. They do this by clicking on the datepicker and typing. This is the **primary** interaction model.
26
+
27
+ The user might also want to see and then select a date from the calendar view. This is the **secondary** interaction model.
28
+
29
+ ## Implementation details
30
+
31
+ As much as possible, all core logic should be defined in `<CustomDatepicker>`. Subcomponents should be simple and focused on a single responsibility.
32
+
33
+ ### Props
34
+
35
+ The Datepicker receives a `Date` object via `value`, and returns a `Date` object via `onValueChange`. `onValueChange` also returns the raw event object as its second argument, in case the consumer needs to access it. This aligns with the props pattern used by other form-field-level components in `uikit-react`.
36
+
37
+ ### Subcomponents
38
+
39
+ - `<CustomDatepicker>`: The default implementation of the Datepicker, built using React logic.
40
+ - `<VisibleField>`: Handles the part of the Datepicker that is _always_ visible to the user -- as a 'form field' input. Composes the controlled input and buttons for opening the calendar and clearing the field.
41
+ - `<DatepickerInput>`: The input field for the Datepicker, used in the custom implementation.
42
+ - `<Panel>`: Wrapper component that handles positioning.
43
+ - `<NativeDatepicker>`: An alternative implementation of the Datepicker, using the browser's native date input.
44
+
45
+ **Additional components:**
46
+ - `<Calendar>`: Imported as a separate component and passed as child to `<Panel>`.
47
+
48
+ #### Subcomponent hierarchy
49
+
50
+ ```mermaid
51
+ graph TD
52
+ A[Datepicker] -->| native=false | B[CustomDatepicker]
53
+ A -->| native=true | C[NativeDatepicker]
54
+ B --> D[VisibleField]
55
+ D --> E[DatepickerInput]
56
+ B --> F[Panel]
57
+ F --> G[Calendar]
58
+ ```
59
+
60
+ ### Date parsing
61
+
62
+ After the user types a date into the input, they need to press ENTER or TAB to parse the date.
63
+
64
+ ### Accessibility
65
+
66
+ Utility function [`announce`](../../utils/announce.ts) is used to provide screen-reader feedback when a date is selected. Places where this is used:
67
+
68
+ - When a date is 'picked' with the Calendar
69
+ - When a date is entered into the input field and successfully parsed.
70
+
71
+ `announce` is only called in `CustomDatepicker`.
72
+
73
+ Automatic association of `<Label>` with `DatepickerInput` when `<Datepicker>` is in a `<Field>` would improve default accessibility -- described in [Datepicker_improv_004].
74
+
75
+ ## Known issues
76
+
77
+ - **[Datepicker_issue_001]**: The Calendar itself, when rendered in the Panel, may not pass Accessibility tests. However, we expect users with Accessibility needs are able to successfully enter a date using the controlled input.
78
+ - The Calendar panel lacks ARIA roles (`role="dialog"`, `role="grid"`, `aria-selected`, etc.), keyboard navigation (arrow keys between days, Escape to close), and the calendar toggle in `<VisibleField>` is a `<div>` with `role="button"` rather than a native `<button>`. This is a WCAG 2.2 AA compliance risk.
79
+ - **[Datepicker_issue_002]**: The Panel only appears below the Datepicker's visible field. If the Datepicker is close to the bottom of the browser viewport, this increases the viewport size while the Panel is open (temporarily).
80
+ - **[Datepicker_issue_003]**: `minDate` and `maxDate` are ISO date strings (`'YYYY-MM-DD'`), which are parsed via `new Date(string)` in several places (`CustomDatepicker.tsx`, `parseInputValue.ts`). This creates UTC dates, not local dates, causing off-by-one day bugs in timezones behind UTC. Options:
81
+ - **Option A**: Create a `parseISODateLocal` utility that splits the string and uses `new Date(year, month - 1, day)`, and use it everywhere.
82
+ - **Option B**: Change `minDate`/`maxDate` to accept `Date` objects instead of strings, aligning with the `value` prop. This would push the parsing responsibility to the consumer and eliminate the UTC ambiguity entirely.
83
+ - **[Datepicker_issue_004]**: The Calendar panel renders inside the component's DOM tree, so it can be clipped by `overflow: hidden` ancestors. Fix: [Datepicker_improv_002].
84
+ - **[Datepicker_issue_005]**: Date handling uses raw `Date` objects with no timezone awareness or locale-aware formatting. Input parsing is hardcoded to `DD/MM/YYYY`.
85
+
86
+ ## Suggested future improvements
87
+
88
+ - **[Datepicker_improv_001]**: Extract `<Panel>` into a shared utility component.
89
+ - `<Select>` also uses a similar Panel (etc).
90
+ - **[Datepicker_improv_002]**: Use library [Floating UI](https://floating-ui.com/) to handle Panel positioning and portalling (via `FloatingPortal`).
91
+ - Fix for [Datepicker_issue_002] and [Datepicker_issue_004].
92
+ - **[Datepicker_improv_003]**: Extract `<NativeDatepicker>` into a separate component.
93
+ - This would simplify props typing.
94
+ - This would remove the need for `Datepicker.tsx` itself to act as a 'routing' component.
95
+ - **[Datepicker_improv_004]**: Integrate `<Datepicker>` with `<Field>` and `<Label>`, to use the shared context solution already used in `<Input>` & `<Textarea>`.
96
+ - This would simplify the API for consumers, and ensure consistent behaviour across form-field-level components.
97
+ - Developers using these components get automatic label->input association without having to manually pass `id` and `htmlFor` props.
98
+ - **[Datepicker_improv_005]**: Separate the button that opens the Calendar and the button that clears the input field, so they can be seen & used independently -- currently they are conditionally rendered with joined logic.
99
+ - **[Datepicker_improv_006]**: Extract the button that opens the Calendar into a separate subcomponent, for cleaner composition in the return statement of `VisibleField`.
100
+ - **[Datepicker_improv_007]**: Extract the button that clears the input field into a separate subcomponent, for cleaner composition in the return statement of `VisibleField`.
101
+ - **[Datepicker_improv_008]**: Extract core logic from `<CustomDatepicker>` into a `useDatepicker` hook. Benefits:
102
+ - **Testability**: Hook logic can be tested in isolation with `renderHook()`, without rendering the full component tree.
103
+ - **Reusability**: Logic can be shared across variants (e.g. date range picker, inline calendar, mobile-specific version).
104
+ - **Composition**: Consumers could use the hook directly for advanced/custom layouts.
105
+ - **Debugging**: Cleanly separates logic bugs from rendering bugs.
106
+ - **Performance**: Allows more granular control over which state changes trigger re-renders.
107
+ - **[Datepicker_improv_009]**: Adopt a date utility library (`date-fns` or `dayjs`) to replace manual string parsing and provide timezone/locale support.
108
+ - Fix for [Datepicker_issue_005].
@@ -1,9 +1,11 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
2
  import { useArgs } from '@storybook/preview-api';
3
3
  import Datepicker from './Datepicker';
4
+ import Field from '../Field';
5
+ import Label from '../Label';
4
6
 
5
7
  const meta = {
6
- title: 'Components/Ready to use/Datepicker',
8
+ title: 'Components/Datepicker',
7
9
  component: Datepicker,
8
10
  parameters: { layout: 'padded' },
9
11
  argTypes: {
@@ -11,7 +13,7 @@ const meta = {
11
13
  minDate: { control: { type: 'date' } },
12
14
  maxDate: { control: { type: 'date' } },
13
15
  disabled: { control: { type: 'boolean' } },
14
- native: { control: { type: 'boolean' } },
16
+ clearable: { control: { type: 'boolean' } },
15
17
  showAcademicWeeks: { control: { type: 'boolean' } },
16
18
  },
17
19
  tags: ['autodocs'],
@@ -49,6 +51,42 @@ export const Default: Story = {
49
51
  },
50
52
  };
51
53
 
54
+ export const InAField: Story = {
55
+ render: () => {
56
+ const [args, updateArgs] = useArgs();
57
+
58
+ // Storybook controls provide UNIX timestamps for dates, need to convert
59
+ // https://storybook.js.org/docs/essentials/controls#annotation
60
+ args.value = args.value ? new Date(args.value) : null;
61
+ args.minDate = args.minDate
62
+ ? new Date(args.minDate).toLocaleDateString('sv-SE')
63
+ : null;
64
+ args.maxDate = args.maxDate
65
+ ? new Date(args.maxDate).toLocaleDateString('sv-SE')
66
+ : null;
67
+
68
+ const onValueChange = (value: Date | null | undefined) =>
69
+ updateArgs({ value: value });
70
+
71
+ // Could be `useId` or a hardcoded string..
72
+ const datepickerInputId = 'datepicker-input';
73
+ // TODO: Associate <Label> and <DatepickerInput> via <Field> context automatically -- [Datepicker_improv_004]
74
+ return (
75
+ <Field>
76
+ <Label htmlFor={datepickerInputId}>Date of event</Label>
77
+ <Field.HelperText>
78
+ Please enter your preferred date for the event
79
+ </Field.HelperText>
80
+ <Datepicker
81
+ {...args}
82
+ onValueChange={onValueChange}
83
+ inputProps={{ id: datepickerInputId }}
84
+ />
85
+ </Field>
86
+ );
87
+ },
88
+ };
89
+
52
90
  // Story repeated in Calendar.stories.tsx
53
91
  export const WithEvents: Story = {
54
92
  name: 'With events',
@@ -237,10 +275,11 @@ export const MinMaxDates: Story = {
237
275
  },
238
276
  };
239
277
 
240
- export const Native: Story = {
241
- name: 'As native fallback',
278
+ export const Clearable: Story = {
279
+ name: 'With clearable',
242
280
  args: {
243
- native: true,
281
+ clearable: true,
282
+ value: new Date(), // Start with a value to show the clear button
244
283
  },
245
284
  render: () => {
246
285
  const [args, updateArgs] = useArgs();
@@ -1,5 +1,4 @@
1
- import { NativeDatepicker, CustomDatepicker } from './subcomponents';
2
- import { dateToLocaleISOString } from './utils';
1
+ import { CustomDatepicker } from './subcomponents';
3
2
  import type { DatepickerProps } from './Datepicker.types';
4
3
 
5
4
  const Datepicker = ({
@@ -9,42 +8,21 @@ const Datepicker = ({
9
8
  maxDate,
10
9
  disabled,
11
10
  className,
12
- native,
13
- nativeRef,
14
- nativeHTMLAttributes,
11
+ clearable,
15
12
  ...props
16
13
  }: DatepickerProps) => {
17
- if (native) {
18
- const nativeValue = dateToLocaleISOString(value);
19
-
20
- return (
21
- <NativeDatepicker
22
- value={nativeValue || ''}
23
- onChange={(event) => {
24
- const dateString = event.target.value;
25
- onValueChange?.(dateString ? new Date(dateString) : null, event);
26
- }}
27
- min={minDate || undefined}
28
- max={maxDate || undefined}
29
- disabled={disabled}
30
- className={className}
31
- ref={nativeRef}
32
- {...nativeHTMLAttributes}
33
- />
34
- );
35
- } else {
36
- return (
37
- <CustomDatepicker
38
- value={value}
39
- onValueChange={onValueChange}
40
- minDate={minDate}
41
- maxDate={maxDate}
42
- className={className}
43
- disabled={disabled}
44
- {...props}
45
- />
46
- );
47
- }
14
+ return (
15
+ <CustomDatepicker
16
+ value={value}
17
+ onValueChange={onValueChange}
18
+ minDate={minDate}
19
+ maxDate={maxDate}
20
+ className={className}
21
+ disabled={disabled}
22
+ clearable={clearable}
23
+ {...props}
24
+ />
25
+ );
48
26
  };
49
27
 
50
28
  export default Datepicker;
@@ -1,5 +1,6 @@
1
- import type { InputHTMLAttributes, HTMLAttributes, RefObject } from 'react';
1
+ import type { HTMLAttributes, RefObject } from 'react';
2
2
  import type { CalendarEvent, AcademicWeek } from '../Calendar';
3
+ import type { InputProps } from './subcomponents/DatepickerInput';
3
4
 
4
5
  export type DatepickerValue = Date | null;
5
6
 
@@ -16,23 +17,13 @@ interface BaseDatepickerProps {
16
17
  academicWeeks?: AcademicWeek[];
17
18
  testId?: string;
18
19
  disabled?: boolean;
20
+ clearable?: boolean;
19
21
  ref?: RefObject<HTMLDivElement>;
20
22
  inputRef?: RefObject<HTMLInputElement>;
23
+ inputProps?: InputProps;
21
24
  }
22
25
 
23
- // Valid HTML attributes for <input type="date">
24
- type NativeDatepickerAttributeProps = Omit<
25
- InputHTMLAttributes<HTMLInputElement>,
26
- // Remove shared 'top-level' & handled props
27
- 'value' | 'onChange' | 'min' | 'max' | 'className' | 'disabled'
28
- >;
29
-
30
26
  // `className?: string` is received automatically from `HTMLAttributes<HTMLDivElement>`
31
27
  export interface DatepickerProps
32
28
  extends BaseDatepickerProps,
33
- HTMLAttributes<HTMLDivElement> {
34
- native?: boolean;
35
- // Attached as a nested object
36
- nativeHTMLAttributes?: NativeDatepickerAttributeProps;
37
- nativeRef?: RefObject<HTMLInputElement>;
38
- }
29
+ HTMLAttributes<HTMLDivElement> {}