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,6 @@
1
+ export { default } from './Calendar';
2
+ export type {
3
+ CalendarProps,
4
+ CalendarEvent,
5
+ AcademicWeek,
6
+ } from './Calendar.types';
@@ -0,0 +1,36 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import { useTheme } from '../../../theme';
3
+
4
+ const NAME = 'ucl-uikit-calendar__academic-week';
5
+
6
+ interface AcademicWeekProps {
7
+ weekNumber?: number | null;
8
+ }
9
+
10
+ const AcademicWeek = ({ weekNumber }: AcademicWeekProps) => {
11
+ const [theme] = useTheme();
12
+
13
+ const baseStyle = css`
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ width: 100%;
18
+ height: 40px;
19
+ font-family: ${theme.font.family.primary};
20
+ font-size: ${theme.font.size.f14};
21
+ color: #6345a5; // TODO: Needs a sensible design token
22
+ `;
23
+
24
+ const style = cx(NAME, baseStyle);
25
+
26
+ return (
27
+ <div
28
+ data-testid={NAME}
29
+ className={style}
30
+ >
31
+ {weekNumber && 'W ' + weekNumber.toString().padStart(2, '0')}
32
+ </div>
33
+ );
34
+ };
35
+
36
+ export default AcademicWeek;
@@ -0,0 +1,46 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import { AcademicWeek } from './';
3
+ import { getAcademicWeekNumbers } from '../utils';
4
+ import { useTheme } from '../../../theme';
5
+
6
+ import type { AcademicWeek as AcademicWeekType } from '../Calendar.types';
7
+
8
+ const NAME = 'ucl-uikit-calendar__academic-weeks';
9
+
10
+ interface AcademicWeeksProps {
11
+ date: Date;
12
+ weeks: AcademicWeekType[];
13
+ }
14
+
15
+ const AcademicWeeks = ({ date, weeks }: AcademicWeeksProps) => {
16
+ const [theme] = useTheme();
17
+
18
+ const academicWeekNumbers = getAcademicWeekNumbers(weeks, date);
19
+
20
+ const baseStyle = css`
21
+ display: flex;
22
+ flex-direction: column;
23
+ padding-top: 32px;
24
+ width: 50px;
25
+ background-color: ${theme.color.interaction.blue5};
26
+ user-select: none;
27
+ `;
28
+
29
+ const style = cx(NAME, baseStyle);
30
+
31
+ return (
32
+ <div
33
+ data-testid={NAME}
34
+ className={style}
35
+ >
36
+ {academicWeekNumbers.map((weekNumber) => (
37
+ <AcademicWeek
38
+ key={weekNumber}
39
+ weekNumber={weekNumber}
40
+ />
41
+ ))}
42
+ </div>
43
+ );
44
+ };
45
+
46
+ export default AcademicWeeks;
@@ -0,0 +1,40 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import { useTheme } from '../../../theme';
3
+
4
+ interface ColumnHeadingProps {
5
+ index: number;
6
+ day: string; // 'M', 'T', 'W', 'T', 'F', 'S', 'S'
7
+ isWeekend: boolean;
8
+ }
9
+
10
+ const NAME = 'ucl-uikit-calendar__column-heading';
11
+
12
+ const ColumnHeading = ({ index, day, isWeekend }: ColumnHeadingProps) => {
13
+ const [theme] = useTheme();
14
+
15
+ const baseStyle = css`
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ width: 40px;
20
+ height: 32px;
21
+ color: ${isWeekend
22
+ ? theme.color.system.orange100
23
+ : theme.color.neutral.grey60};
24
+ font-weight: 700;
25
+ `;
26
+
27
+ const style = cx(NAME, baseStyle);
28
+
29
+ return (
30
+ <div
31
+ className={style}
32
+ data-testid={NAME}
33
+ key={index}
34
+ >
35
+ {day}
36
+ </div>
37
+ );
38
+ };
39
+
40
+ export default ColumnHeading;
@@ -1,16 +1,15 @@
1
- import { css } from '@emotion/css';
1
+ import { css, cx } from '@emotion/css';
2
2
  import { Icon } from '../../..';
3
- import { useTheme } from '../../../../theme';
3
+ import { useTheme } from '../../../theme';
4
4
 
5
- interface MonthSelectorProps {
6
- date: Date | null | undefined;
5
+ interface ControlsProps {
6
+ month: Date;
7
7
  changeMonth: (change: number) => void;
8
8
  }
9
9
 
10
- const MonthSelector = ({
11
- date,
12
- changeMonth,
13
- }: MonthSelectorProps) => {
10
+ const NAME = 'ucl-uikit-calendar__controls';
11
+
12
+ const Controls = ({ month, changeMonth }: ControlsProps) => {
14
13
  const [theme] = useTheme();
15
14
 
16
15
  const baseStyle = css`
@@ -37,27 +36,28 @@ const MonthSelector = ({
37
36
  }
38
37
  `;
39
38
 
39
+ const style = cx(NAME, baseStyle);
40
+
40
41
  return (
41
- <div className={baseStyle}>
42
+ <div
43
+ className={style}
44
+ data-testid={NAME}
45
+ >
42
46
  <Icon.ChevronLeft
43
47
  className={chevronIconStyle}
44
48
  onClick={() => changeMonth(-1)}
45
49
  />
46
50
  <span className={monthAndYearStyle}>
47
51
  <span>
48
- {date
49
- ? date.toLocaleDateString('default', {
52
+ {month
53
+ ? month.toLocaleDateString('default', {
50
54
  month: 'long',
51
55
  })
52
56
  : new Date().toLocaleDateString('default', {
53
57
  month: 'long',
54
58
  })}
55
59
  </span>
56
- <span>
57
- {date
58
- ? date.getFullYear()
59
- : new Date().getFullYear()}
60
- </span>
60
+ <span>{month ? month.getFullYear() : new Date().getFullYear()}</span>
61
61
  </span>
62
62
  <Icon.ChevronRight
63
63
  className={chevronIconStyle}
@@ -67,4 +67,4 @@ const MonthSelector = ({
67
67
  );
68
68
  };
69
69
 
70
- export default MonthSelector;
70
+ export default Controls;
@@ -2,8 +2,16 @@ import type { Meta, StoryObj } from '@storybook/react';
2
2
  import Day from './Day';
3
3
 
4
4
  const meta = {
5
- title: 'Components/Work in progress/Datepicker/Day',
5
+ title: 'Components/Work in progress/Calendar/Day',
6
6
  component: Day,
7
+ argTypes: {
8
+ events: {
9
+ table: {
10
+ // We don't want to show an empty array in the controls
11
+ disable: true,
12
+ },
13
+ },
14
+ },
7
15
  } satisfies Meta<typeof Day>;
8
16
 
9
17
  export default meta;
@@ -14,7 +22,7 @@ export const Default: Story = {
14
22
  date: new Date(),
15
23
  },
16
24
  render: (args) => {
17
- args.date = args.date ? new Date(args.date) : null;
25
+ args.date = new Date(args.date);
18
26
  return <Day {...args} />;
19
27
  },
20
28
  };
@@ -25,7 +33,7 @@ export const Selected: Story = {
25
33
  isSelected: true,
26
34
  },
27
35
  render: (args) => {
28
- args.date = args.date ? new Date(args.date) : null;
36
+ args.date = new Date(args.date);
29
37
  return <Day {...args} />;
30
38
  },
31
39
  };
@@ -36,7 +44,7 @@ export const Today: Story = {
36
44
  isToday: true,
37
45
  },
38
46
  render: (args) => {
39
- args.date = args.date ? new Date(args.date) : null;
47
+ args.date = new Date(args.date);
40
48
  return <Day {...args} />;
41
49
  },
42
50
  };
@@ -47,7 +55,7 @@ export const Disabled: Story = {
47
55
  isDisabled: true,
48
56
  },
49
57
  render: (args) => {
50
- args.date = args.date ? new Date(args.date) : null;
58
+ args.date = new Date(args.date);
51
59
  return <Day {...args} />;
52
60
  },
53
61
  };
@@ -58,7 +66,22 @@ export const NotInCurrentMonth: Story = {
58
66
  isInCurrentMonth: false,
59
67
  },
60
68
  render: (args) => {
61
- args.date = args.date ? new Date(args.date) : null;
69
+ args.date = new Date(args.date);
70
+ return <Day {...args} />;
71
+ },
72
+ };
73
+
74
+ export const WithEventDots: Story = {
75
+ args: {
76
+ date: new Date(),
77
+ events: [
78
+ { date: new Date().toISOString() },
79
+ { date: new Date().toISOString() },
80
+ { date: new Date().toISOString() },
81
+ ],
82
+ },
83
+ render: (args) => {
84
+ args.date = new Date(args.date);
62
85
  return <Day {...args} />;
63
86
  },
64
87
  };
@@ -69,7 +92,7 @@ export const AlertOnPick: Story = {
69
92
  date: new Date(),
70
93
  },
71
94
  render: (args) => {
72
- args.date = args.date ? new Date(args.date) : null;
95
+ args.date = new Date(args.date);
73
96
  const onPick = (date: Date) => alert(`Picked date: ${date.toDateString()}`);
74
97
  return (
75
98
  <Day
@@ -0,0 +1,130 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import { EventDot } from './';
3
+ import { useTheme } from '../../../theme';
4
+ import type { CalendarEvent } from '../Calendar.types';
5
+
6
+ export interface DayProps {
7
+ date: Date;
8
+ onPick?: (date: Date, event: React.SyntheticEvent) => void;
9
+ isSelected?: boolean;
10
+ isToday?: boolean;
11
+ isInCurrentMonth?: boolean;
12
+ isDisabled?: boolean;
13
+ events?: CalendarEvent[]; // Max 3 events are displayed as dots
14
+ }
15
+
16
+ const NAME = 'ucl-uikit-calendar__day';
17
+
18
+ const Day = ({
19
+ date,
20
+ onPick,
21
+ isSelected = false,
22
+ isToday = false,
23
+ isInCurrentMonth = true,
24
+ isDisabled = false,
25
+ events = [],
26
+ }: DayProps) => {
27
+ const [theme] = useTheme();
28
+
29
+ // More than 3 dots displayed breaks the layout
30
+ const displayedEvents = events.slice(0, 3);
31
+
32
+ const handlePick = (event: React.MouseEvent) => {
33
+ if (date && !isDisabled && onPick) {
34
+ onPick(date, event);
35
+ }
36
+ };
37
+
38
+ const backgroundStyle = css`
39
+ display: flex;
40
+ justify-content: center;
41
+ align-items: center;
42
+ position: relative;
43
+ width: 40px;
44
+ height: 40px;
45
+ background-color: ${theme.color.neutral.white};
46
+ cursor: pointer;
47
+ outline: none;
48
+
49
+ &:hover {
50
+ background-color: ${theme.color.neutral.grey10};
51
+ }
52
+
53
+ ${isSelected &&
54
+ css`
55
+ /* background-color: ${theme.color.interaction.blue70}; */
56
+ background-color: ${isDisabled
57
+ ? theme.color.neutral.grey10
58
+ : theme.color.interaction.blue70};
59
+
60
+ color: ${theme.color.text.inverted};
61
+
62
+ &:hover {
63
+ background-color: ${theme.color.interaction.blue100};
64
+ }
65
+ `}
66
+ ${isDisabled &&
67
+ css`
68
+ cursor: not-allowed;
69
+
70
+ &:hover {
71
+ /* background-color: ${theme.color.neutral.white}; */
72
+ background-color: ${isSelected
73
+ ? theme.color.neutral.grey10
74
+ : theme.color.neutral.white};
75
+ }
76
+ `}
77
+ `;
78
+
79
+ const foregroundStyle = css`
80
+ font-family: ${theme.font.family.primary};
81
+ user-select: none;
82
+ font-size: 16px;
83
+
84
+ ${!isInCurrentMonth &&
85
+ css`
86
+ color: ${theme.color.neutral.grey40};
87
+ `}
88
+ ${isToday &&
89
+ css`
90
+ font-weight: 700;
91
+ `}
92
+ ${isDisabled &&
93
+ css`
94
+ color: ${theme.color.neutral.grey20};
95
+ `}
96
+ `;
97
+
98
+ const eventDotsContainerStyle = css`
99
+ display: flex;
100
+ justify-content: center;
101
+ align-items: center;
102
+ gap: 3px;
103
+ position: absolute;
104
+ bottom: 3px;
105
+ width: 100%;
106
+ `;
107
+
108
+ const style = cx(NAME, backgroundStyle);
109
+
110
+ return (
111
+ <div
112
+ onClick={handlePick}
113
+ className={style}
114
+ data-testid={NAME}
115
+ >
116
+ <div className={foregroundStyle}>{date.getDate()}</div>
117
+ <div className={eventDotsContainerStyle}>
118
+ {displayedEvents.map((_event, index) => (
119
+ <EventDot
120
+ key={index}
121
+ inverted={isSelected}
122
+ inCurrentMonth={isInCurrentMonth}
123
+ />
124
+ ))}
125
+ </div>
126
+ </div>
127
+ );
128
+ };
129
+
130
+ export default Day;
@@ -0,0 +1,40 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import { useTheme } from '../../../theme';
3
+
4
+ const NAME = 'ucl-uikit-calendar__event-dot';
5
+
6
+ interface EventDotProps {
7
+ inverted: boolean;
8
+ inCurrentMonth: boolean;
9
+ }
10
+
11
+ const EventDot = ({ inverted, inCurrentMonth }: EventDotProps) => {
12
+ const [theme] = useTheme();
13
+
14
+ const invertedColour = theme.color.neutral.white;
15
+ const outOfCurrentMonthColour = '#8C8C8C'; // TODO: Needs adding to `defaultTheme.ts`, as a design token
16
+
17
+ const backgroundColour = inCurrentMonth
18
+ ? inverted
19
+ ? invertedColour
20
+ : theme.color.interaction.blue70
21
+ : outOfCurrentMonthColour;
22
+
23
+ const baseStyle = css`
24
+ width: 6px;
25
+ height: 6px;
26
+ border-radius: 50%;
27
+ background-color: ${backgroundColour};
28
+ `;
29
+
30
+ const style = cx(NAME, baseStyle);
31
+
32
+ return (
33
+ <div
34
+ data-testid={NAME}
35
+ className={style}
36
+ />
37
+ );
38
+ };
39
+
40
+ export default EventDot;
@@ -0,0 +1,117 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import { useMemo } from 'react';
3
+ import { ColumnHeading, Day } from './';
4
+ import { getDatesForCalendarGrid } from '../utils';
5
+ import { useTheme } from '../../../theme';
6
+ import type { CalendarEvent } from '../Calendar.types';
7
+
8
+ interface GridProps {
9
+ month: Date; // 1st day of the month
10
+ pickedDate: Date | null;
11
+ onDatePick?: (date: Date, event: React.SyntheticEvent) => void;
12
+ minDate: Date | null;
13
+ maxDate: Date | null;
14
+ events: CalendarEvent[];
15
+ }
16
+
17
+ const NAME = 'ucl-uikit-calendar__grid';
18
+
19
+ // Helper function: Convert ISO string 'yyyy-mm-dd' to local Date without timezone shift
20
+ function toLocalDate(dateStr: string): Date {
21
+ const [year, month, day] = dateStr.split('-').map(Number);
22
+ return new Date(year, month - 1, day);
23
+ }
24
+
25
+ function getDateKey(date: Date): string {
26
+ return date.toISOString().split('T')[0];
27
+ }
28
+
29
+ // Create events map grouped by date
30
+ function createEventsMap(
31
+ events: CalendarEvent[]
32
+ ): Map<string, CalendarEvent[]> {
33
+ const eventsMap = new Map<string, CalendarEvent[]>();
34
+
35
+ events.forEach((event) => {
36
+ const eventDate = toLocalDate(event.date);
37
+ const dateKey = getDateKey(eventDate);
38
+
39
+ if (!eventsMap.has(dateKey)) {
40
+ eventsMap.set(dateKey, []);
41
+ }
42
+ eventsMap.get(dateKey)!.push(event);
43
+ });
44
+ console.log('Events map created:', eventsMap);
45
+ return eventsMap;
46
+ }
47
+
48
+ const Grid = ({
49
+ month,
50
+ pickedDate,
51
+ onDatePick = () => {},
52
+ minDate,
53
+ maxDate,
54
+ events,
55
+ }: GridProps) => {
56
+ // Fix minDate timezone issues by zeroing time if valid
57
+ if (minDate && !isNaN(minDate.getTime())) minDate.setHours(0, 0, 0, 0);
58
+
59
+ const [theme] = useTheme();
60
+
61
+ const daysOfTheWeek = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
62
+ const dates = getDatesForCalendarGrid(month);
63
+
64
+ const eventsMap = useMemo(() => createEventsMap(events), [events]);
65
+
66
+ const baseStyle = css`
67
+ display: grid;
68
+ grid-template-columns: repeat(7, 1fr);
69
+ gap: 0;
70
+ text-align: center;
71
+ font-family: ${theme.font.family.primary};
72
+ font-weight: 400;
73
+ user-select: none;
74
+ `;
75
+
76
+ const style = cx(NAME, baseStyle);
77
+
78
+ return (
79
+ <div
80
+ className={style}
81
+ data-testid={NAME}
82
+ >
83
+ {daysOfTheWeek.map((day, index) => (
84
+ <ColumnHeading
85
+ key={index}
86
+ index={index}
87
+ day={day}
88
+ isWeekend={index >= 5}
89
+ />
90
+ ))}
91
+ {dates.map((mappedDate) => {
92
+ const dateKey = getDateKey(mappedDate);
93
+ const dateEvents = eventsMap.get(dateKey) || [];
94
+
95
+ return (
96
+ <Day
97
+ key={mappedDate.toISOString()}
98
+ date={mappedDate}
99
+ isSelected={
100
+ pickedDate?.toDateString() === mappedDate.toDateString()
101
+ }
102
+ isToday={mappedDate.toDateString() === new Date().toDateString()}
103
+ isInCurrentMonth={month.getMonth() === mappedDate.getMonth()}
104
+ isDisabled={
105
+ (minDate !== null && mappedDate < minDate) ||
106
+ (maxDate !== null && mappedDate > maxDate)
107
+ }
108
+ onPick={onDatePick}
109
+ events={dateEvents}
110
+ />
111
+ );
112
+ })}
113
+ </div>
114
+ );
115
+ };
116
+
117
+ export default Grid;
@@ -0,0 +1,7 @@
1
+ export { default as Controls } from './Controls';
2
+ export { default as AcademicWeeks } from './AcademicWeeks';
3
+ export { default as AcademicWeek } from './AcademicWeek';
4
+ export { default as Grid } from './Grid';
5
+ export { default as ColumnHeading } from './ColumnHeading';
6
+ export { default as Day } from './Day';
7
+ export { default as EventDot } from './EventDot';
@@ -0,0 +1,104 @@
1
+ import { describe, expect, test, vi } from 'vitest';
2
+ import getAcademicWeekNumbers, { getMonday } from './getAcademicWeekNumbers';
3
+ import type { AcademicWeek } from '../../';
4
+
5
+ describe('getMonday', () => {
6
+ test('Returns the Monday for the week of the given date', () => {
7
+ const date = new Date('2025-09-03'); // Wednesday
8
+ const monday = getMonday(date);
9
+ expect(monday.getFullYear()).toBe(2025);
10
+ expect(monday.getMonth()).toBe(8); // September (0-indexed)
11
+ expect(monday.getDate()).toBe(1); // 1st September 2025
12
+ expect(monday.getDay()).toBe(1); // Monday
13
+ });
14
+
15
+ test('Works for a date that is already a Monday', () => {
16
+ const date = new Date('2025-10-06'); // Monday
17
+ const monday = getMonday(date);
18
+ expect(monday.getFullYear()).toBe(2025);
19
+ expect(monday.getMonth()).toBe(9); // October (0-indexed)
20
+ expect(monday.getDate()).toBe(6); // 6th October 2025
21
+ expect(monday.getDay()).toBe(1); // Monday
22
+ });
23
+ });
24
+
25
+ describe('getAcademicWeekNumbers', () => {
26
+ test('Returns an array of the correct numbers (works as expected)', () => {
27
+ const targetDate = new Date('2025-09-01'); // Monday 1st September 2025
28
+ const academicWeeks: AcademicWeek[] = [
29
+ { start: '2025-08-25', number: 1 },
30
+ { start: '2025-09-01', number: 2 },
31
+ { start: '2025-09-08', number: 3 },
32
+ { start: '2025-09-15', number: 4 },
33
+ { start: '2025-09-22', number: 5 },
34
+ { start: '2025-09-29', number: 6 },
35
+ { start: '2025-10-06', number: 7 },
36
+ { start: '2025-10-13', number: 8 },
37
+ { start: '2025-10-20', number: 9 },
38
+ { start: '2025-10-27', number: 10 },
39
+ { start: '2025-11-03', number: 11 },
40
+ { start: '2025-11-10', number: 12 },
41
+ { start: '2025-11-17', number: 13 },
42
+ { start: '2025-11-24', number: 14 },
43
+ // Etc ...
44
+ ];
45
+
46
+ const result = getAcademicWeekNumbers(academicWeeks, targetDate);
47
+ expect(result).toEqual([2, 3, 4, 5, 6]);
48
+ });
49
+
50
+ // Empty array for `academicWeeks` is default parameter value in `Datepicker.tsx`
51
+ test('Returns empty array if academicWeeks are empty', () => {
52
+ console.warn = vi.fn();
53
+ const emptyArray: AcademicWeek[] = [];
54
+ const result = getAcademicWeekNumbers(emptyArray, new Date());
55
+ expect(result).toEqual([]);
56
+ expect(console.warn).toHaveBeenCalledWith(
57
+ 'Datepicker: No academic weeks provided'
58
+ );
59
+ });
60
+
61
+ test('Returns empty array if targetDate is invalid', () => {
62
+ const result = getAcademicWeekNumbers(
63
+ [{ start: '2025-08-25', number: 1 }],
64
+ new Date('invalid-date')
65
+ );
66
+ expect(result).toEqual([]);
67
+
68
+ const result2 = getAcademicWeekNumbers(
69
+ [{ start: '2025-08-25', number: 1 }],
70
+ null as unknown as Date
71
+ );
72
+ expect(result2).toEqual([]);
73
+
74
+ const result3 = getAcademicWeekNumbers(
75
+ [{ start: '2025-08-25', number: 1 }],
76
+ undefined as unknown as Date
77
+ );
78
+ expect(result3).toEqual([]);
79
+ });
80
+
81
+ test('Returns array of undefined if no academic weeks match the current date', () => {
82
+ // A pretend very short example for academic weeks
83
+ const academicWeeks: AcademicWeek[] = [
84
+ { start: '2025-09-01', number: 1 },
85
+ { start: '2025-09-08', number: 2 },
86
+ { start: '2025-09-15', number: 3 },
87
+ { start: '2025-09-22', number: 4 },
88
+ ];
89
+
90
+ // The date is in October, which has no matching academic weeks
91
+ const targetDate = new Date('2025-10-01');
92
+
93
+ const result = getAcademicWeekNumbers(academicWeeks, targetDate);
94
+
95
+ // We expect 5 weeks displayed in the calendar, with no matching academic weeks
96
+ expect(result).toEqual([
97
+ undefined,
98
+ undefined,
99
+ undefined,
100
+ undefined,
101
+ undefined,
102
+ ]);
103
+ });
104
+ });