uikit-react-public 0.11.16 → 0.11.20

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 (52) hide show
  1. package/dist/components/Datepicker/Datepicker.d.ts +5 -1
  2. package/dist/components/Datepicker/Datepicker.stories.d.ts +8 -1
  3. package/dist/components/Datepicker/Datepicker.types.d.ts +7 -0
  4. package/dist/components/Datepicker/index.d.ts +1 -0
  5. package/dist/components/Datepicker/subcomponents/AcademicWeek.d.ts +5 -0
  6. package/dist/components/Datepicker/subcomponents/AcademicWeeks.d.ts +7 -0
  7. package/dist/components/Datepicker/subcomponents/CalendarGrid/CalendarGrid.d.ts +3 -1
  8. package/dist/components/Datepicker/subcomponents/CalendarMenu/CalendarMenu.d.ts +5 -1
  9. package/dist/components/Datepicker/subcomponents/Day/Day.d.ts +4 -2
  10. package/dist/components/Datepicker/subcomponents/Day/Day.stories.d.ts +9 -1
  11. package/dist/components/Datepicker/subcomponents/EventDot.d.ts +6 -0
  12. package/dist/components/Datepicker/utils/getAcademicWeekNumbers/getAcademicWeekNumbers.d.ts +24 -0
  13. package/dist/components/Datepicker/utils/getAcademicWeekNumbers/getAcademicWeekNumbers.test.d.ts +1 -0
  14. package/dist/components/Datepicker/utils/index.d.ts +1 -0
  15. package/dist/components/Header/Header.d.ts +5 -4
  16. package/dist/components/Header/index.d.ts +1 -1
  17. package/dist/components/Select/Select.stories.d.ts +1 -1
  18. package/dist/components/Select/Select.types.d.ts +10 -50
  19. package/dist/components/Select/index.d.ts +1 -1
  20. package/dist/components/Select/subcomponents/CustomSelect.d.ts +2 -1
  21. package/dist/index.js +1865 -1748
  22. package/lib/components/Datepicker/Datepicker.stories.tsx +133 -0
  23. package/lib/components/Datepicker/Datepicker.tsx +23 -46
  24. package/lib/components/Datepicker/Datepicker.types.ts +9 -0
  25. package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +487 -378
  26. package/lib/components/Datepicker/index.ts +1 -0
  27. package/lib/components/Datepicker/subcomponents/AcademicWeek.tsx +36 -0
  28. package/lib/components/Datepicker/subcomponents/AcademicWeeks.tsx +44 -0
  29. package/lib/components/Datepicker/subcomponents/CalendarGrid/CalendarGrid.tsx +9 -14
  30. package/lib/components/Datepicker/subcomponents/CalendarMenu/CalendarMenu.tsx +32 -6
  31. package/lib/components/Datepicker/subcomponents/Day/Day.stories.tsx +23 -0
  32. package/lib/components/Datepicker/subcomponents/Day/Day.tsx +31 -7
  33. package/lib/components/Datepicker/subcomponents/EventDot.tsx +40 -0
  34. package/lib/components/Datepicker/utils/getAcademicWeekNumbers/getAcademicWeekNumbers.test.ts +104 -0
  35. package/lib/components/Datepicker/utils/getAcademicWeekNumbers/getAcademicWeekNumbers.ts +85 -0
  36. package/lib/components/Datepicker/utils/index.ts +1 -0
  37. package/lib/components/Header/Header.tsx +32 -33
  38. package/lib/components/Header/HeaderMenu.tsx +9 -2
  39. package/lib/components/Header/__tests__/__snapshots__/Header.test.tsx.snap +40 -48
  40. package/lib/components/Header/index.ts +5 -1
  41. package/lib/components/Select/Select.stories.tsx +38 -39
  42. package/lib/components/Select/Select.tsx +4 -18
  43. package/lib/components/Select/Select.types.ts +30 -69
  44. package/lib/components/Select/__tests__/Select.test.tsx +6 -6
  45. package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
  46. package/lib/components/Select/index.ts +1 -1
  47. package/lib/components/Select/subcomponents/CustomSelect.tsx +22 -12
  48. package/lib/components/Select/subcomponents/NativeSelect.tsx +7 -3
  49. package/lib/components/Select/subcomponents/Panel.tsx +4 -4
  50. package/lib/components/Select/subcomponents/VisibleField.tsx +1 -1
  51. package/package.json +3 -3
  52. package/LICENSE +0 -9
@@ -1,2 +1,3 @@
1
1
  export { default } from './Datepicker';
2
2
  export type { DatepickerProps } from './Datepicker';
3
+ export type { CalendarEvent, AcademicWeek } from './Datepicker.types';
@@ -0,0 +1,36 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import { useTheme } from '../../../theme';
3
+
4
+ const NAME = 'ucl-uikit-datepicker__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,44 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import AcademicWeek from './AcademicWeek';
3
+ import { getAcademicWeekNumbers } from './../utils';
4
+ import { useTheme } from '../../../theme';
5
+ import type { AcademicWeek as AcademicWeekType } from '../Datepicker.types';
6
+
7
+ const NAME = 'ucl-uikit-datepicker__academic-weeks';
8
+
9
+ interface AcademicWeeksProps {
10
+ date: Date;
11
+ academicWeeks: AcademicWeekType[];
12
+ }
13
+
14
+ const AcademicWeeks = ({ date, academicWeeks }: AcademicWeeksProps) => {
15
+ const [theme] = useTheme();
16
+
17
+ const academicWeekNumbers = getAcademicWeekNumbers(academicWeeks, date);
18
+
19
+ const baseStyle = css`
20
+ display: flex;
21
+ flex-direction: column;
22
+ padding-top: 32px;
23
+ width: 50px;
24
+ background-color: ${theme.color.interaction.blue5};
25
+ `;
26
+
27
+ const style = cx(NAME, baseStyle);
28
+
29
+ return (
30
+ <div
31
+ data-testid={NAME}
32
+ className={style}
33
+ >
34
+ {academicWeekNumbers.map((weekNumber) => (
35
+ <AcademicWeek
36
+ key={weekNumber}
37
+ weekNumber={weekNumber}
38
+ />
39
+ ))}
40
+ </div>
41
+ );
42
+ };
43
+
44
+ export default AcademicWeeks;
@@ -2,16 +2,15 @@ import { css } from '@emotion/css';
2
2
  import { ColumnHeadings, Day } from '../';
3
3
  import useTheme from '../../../../theme/useTheme';
4
4
  import { getDatesForCalendarGrid } from '../../utils';
5
+ import { CalendarEvent } from '../../Datepicker.types';
5
6
 
6
7
  interface CalendarGridProps {
7
8
  date: Date | null | undefined;
8
9
  onDatePick: (date: Date) => void;
10
+ events: CalendarEvent[];
9
11
  }
10
12
 
11
- const CalendarGrid = ({
12
- date,
13
- onDatePick,
14
- }: CalendarGridProps) => {
13
+ const CalendarGrid = ({ date, onDatePick, events }: CalendarGridProps) => {
15
14
  const [theme] = useTheme();
16
15
 
17
16
  const dates = date
@@ -35,21 +34,17 @@ const CalendarGrid = ({
35
34
  <Day
36
35
  key={mappedDate.toISOString()}
37
36
  date={mappedDate}
38
- isSelected={
39
- date?.toDateString() ===
40
- mappedDate.toDateString()
41
- }
42
- isToday={
43
- mappedDate.toDateString() ===
44
- new Date().toDateString()
45
- }
37
+ isSelected={date?.toDateString() === mappedDate.toDateString()}
38
+ isToday={mappedDate.toDateString() === new Date().toDateString()}
46
39
  isInCurrentMonth={
47
40
  date
48
41
  ? mappedDate.getMonth() === date?.getMonth()
49
- : mappedDate.getMonth() ===
50
- new Date().getMonth()
42
+ : mappedDate.getMonth() === new Date().getMonth()
51
43
  }
52
44
  onPick={onDatePick}
45
+ events={events.filter(
46
+ (event) => event.date === mappedDate.toISOString().split('T')[0]
47
+ )}
53
48
  />
54
49
  ))}
55
50
  </div>
@@ -1,12 +1,17 @@
1
1
  import { css } from '@emotion/css';
2
2
  import MonthSelector from '../MonthSelector';
3
3
  import CalendarGrid from '../CalendarGrid';
4
+ import AcademicWeeks from '../AcademicWeeks';
4
5
  import useTheme from '../../../../theme/useTheme';
6
+ import { CalendarEvent, AcademicWeek } from '../../Datepicker.types';
5
7
 
6
8
  interface CalendarMenuProps {
7
9
  date: Date | null | undefined;
8
10
  setDate: (date: Date) => void;
9
11
  onDatePick: () => void;
12
+ events: CalendarEvent[];
13
+ showAcademicWeeks?: boolean;
14
+ academicWeeks: AcademicWeek[];
10
15
  testId?: string;
11
16
  }
12
17
 
@@ -14,6 +19,9 @@ const CalendarMenu = ({
14
19
  date,
15
20
  setDate,
16
21
  onDatePick,
22
+ events,
23
+ showAcademicWeeks,
24
+ academicWeeks,
17
25
  testId = 'ucl',
18
26
  }: CalendarMenuProps) => {
19
27
  const [theme] = useTheme();
@@ -24,21 +32,30 @@ const CalendarMenu = ({
24
32
  setDate(newDate);
25
33
  };
26
34
 
35
+ const width = showAcademicWeeks ? '370' : '312';
36
+
27
37
  const style = css`
28
38
  display: flex;
29
39
  flex-direction: column;
30
40
  align-items: center;
31
41
  gap: 16px;
32
- width: 280px;
33
- height: 320px;
42
+ width: ${width}px;
34
43
  z-index: 10;
35
44
  position: absolute;
36
45
  top: 60px;
46
+ box-sizing: border-box;
37
47
  border: 1px solid ${theme.color.neutral.grey20};
38
48
  padding: 16px;
39
49
  background-color: ${theme.color.neutral.white};
40
50
  `;
41
51
 
52
+ const innerContainerStyle = css`
53
+ display: flex;
54
+ flex-direction: row;
55
+ gap: 8px;
56
+ width: 100%;
57
+ `;
58
+
42
59
  const handlePick = (pickedDate: Date) => {
43
60
  setDate(pickedDate);
44
61
  onDatePick();
@@ -53,10 +70,19 @@ const CalendarMenu = ({
53
70
  date={date}
54
71
  changeMonth={onMonthChange}
55
72
  />
56
- <CalendarGrid
57
- date={date}
58
- onDatePick={handlePick}
59
- />
73
+ <div className={innerContainerStyle}>
74
+ {showAcademicWeeks && (
75
+ <AcademicWeeks
76
+ date={date || new Date()} // TODO: more comprehsensive handling of null/undefined dates
77
+ academicWeeks={academicWeeks}
78
+ />
79
+ )}
80
+ <CalendarGrid
81
+ date={date}
82
+ onDatePick={handlePick}
83
+ events={events}
84
+ />
85
+ </div>
60
86
  </div>
61
87
  );
62
88
  };
@@ -4,6 +4,14 @@ import Day from './Day';
4
4
  const meta = {
5
5
  title: 'Components/Work in progress/Datepicker/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;
@@ -63,6 +71,21 @@ export const NotInCurrentMonth: Story = {
63
71
  },
64
72
  };
65
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 = args.date ? new Date(args.date) : null;
85
+ return <Day {...args} />;
86
+ },
87
+ };
88
+
66
89
  export const AlertOnPick: Story = {
67
90
  name: '(Trigger alert on pick)',
68
91
  args: {
@@ -1,25 +1,32 @@
1
1
  import { css } from '@emotion/css';
2
+ import EventDot from '../EventDot';
2
3
  import { useTheme } from '../../../../theme/';
4
+ import { CalendarEvent } from '../../Datepicker.types';
3
5
 
4
6
  export interface DayProps {
5
7
  date: Date | null | undefined;
8
+ onPick?: (date: Date) => void;
6
9
  isSelected?: boolean;
7
10
  isToday?: boolean;
8
11
  isInCurrentMonth?: boolean;
9
12
  isDisabled?: boolean;
10
- onPick?: (date: Date) => void;
13
+ events?: CalendarEvent[]; // Max 3 events are displayed as dots
11
14
  }
12
15
 
13
16
  const Day = ({
14
17
  date,
15
- isSelected,
18
+ onPick,
19
+ isSelected = false,
16
20
  isToday = false,
17
21
  isInCurrentMonth = true,
18
22
  isDisabled = false,
19
- onPick,
23
+ events = [],
20
24
  }: DayProps) => {
21
25
  const [theme] = useTheme();
22
26
 
27
+ // More than 3 dots displayed breaks the layout
28
+ const displayedEvents = events.slice(0, 3);
29
+
23
30
  const onClick = () => {
24
31
  if (date && onPick) onPick(date);
25
32
  };
@@ -28,6 +35,7 @@ const Day = ({
28
35
  display: flex;
29
36
  justify-content: center;
30
37
  align-items: center;
38
+ position: relative;
31
39
  width: 40px;
32
40
  height: 40px;
33
41
  background-color: ${theme.color.neutral.white};
@@ -44,8 +52,7 @@ const Day = ({
44
52
  color: ${theme.color.text.inverted};
45
53
 
46
54
  &:hover {
47
- background-color: ${theme.color.interaction
48
- .blue100};
55
+ background-color: ${theme.color.interaction.blue100};
49
56
  }
50
57
  `}
51
58
  ${isDisabled &&
@@ -77,6 +84,16 @@ const Day = ({
77
84
  `}
78
85
  `;
79
86
 
87
+ const eventDotsContainerStyle = css`
88
+ display: flex;
89
+ justify-content: center;
90
+ align-items: center;
91
+ gap: 3px;
92
+ position: absolute;
93
+ bottom: 3px;
94
+ width: 100%;
95
+ `;
96
+
80
97
  return (
81
98
  <div
82
99
  className={backgroundStyle}
@@ -84,8 +101,15 @@ const Day = ({
84
101
  aria-label={`Select ${date?.toDateString()}`}
85
102
  onClick={onClick}
86
103
  >
87
- <div className={foregroundStyle}>
88
- {date?.getDate()}
104
+ <div className={foregroundStyle}>{date?.getDate()}</div>
105
+ <div className={eventDotsContainerStyle}>
106
+ {displayedEvents.map((_event, index) => (
107
+ <EventDot
108
+ key={index}
109
+ inverted={isSelected}
110
+ inCurrentMonth={isInCurrentMonth}
111
+ />
112
+ ))}
89
113
  </div>
90
114
  </div>
91
115
  );
@@ -0,0 +1,40 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import { useTheme } from '../../../theme';
3
+
4
+ const NAME = 'ucl-uikit-datepicker__event-dot';
5
+
6
+ type 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,104 @@
1
+ import { describe, expect, test, vi } from 'vitest';
2
+ import getAcademicWeekNumbers, { getMonday } from './getAcademicWeekNumbers';
3
+ import type { AcademicWeek } from '../../Datepicker.types';
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
+ });
@@ -0,0 +1,85 @@
1
+ import type { AcademicWeek } from '../../Datepicker.types';
2
+
3
+ // The 'calendar period' is defined here as all the dates the calendar displays,
4
+ // which may include some dates in the previous month and some dates in the next month.
5
+ // This is because we display complete weeks in the calendar.
6
+
7
+ /**
8
+ * Returns the Monday for the week of the given date.
9
+ * For example:
10
+ * - If the input date is a Wednesday, it returns the Monday of that week.
11
+ * - If the input date is already a Monday, it returns that date.
12
+ * @param {Date} d - The input date.
13
+ * @returns {Date} The Monday of the week of the input date.
14
+ */
15
+ export const getMonday = (d: Date) => {
16
+ const date = new Date(d);
17
+ const day = date.getDay();
18
+ const delta = (day + 6) % 7;
19
+ date.setDate(date.getDate() - delta);
20
+ date.setHours(0, 0, 0, 0);
21
+ return date;
22
+ };
23
+
24
+ /**
25
+ * Calculates the academic week numbers for each week displayed in a calendar month.
26
+ * Used by `<AcademicWeeks />` component in the Datepicker.
27
+ * It determines the Mondays of each week shown in the calendar for the `targetDate`'s month.
28
+ * Then, it maps these Mondays to the corresponding academic week number from the `academicWeeks` array.
29
+ * If an academic week is not found for a given Monday, `undefined` is used, in place in the returned array.
30
+ *
31
+ * @param {AcademicWeek[]} academicWeeks - An array of academic week objects, each with a `start` date and `number`.
32
+ * @param {Date} targetDate - The date used to generate the calendar display.
33
+ * @returns {(number | undefined)[]} An array of academic week numbers or `undefined` for each week in the calendar display.
34
+ * Returns an empty array if `academicWeeks` is empty or `targetDate` is invalid, with console warnings, in the interest of safe usage.
35
+ */
36
+ const getAcademicWeekNumbers = (
37
+ academicWeeks: AcademicWeek[],
38
+ targetDate: Date
39
+ ) => {
40
+ if (!academicWeeks || academicWeeks.length === 0) {
41
+ console.warn('Datepicker: No academic weeks provided');
42
+ return [];
43
+ }
44
+ if (
45
+ !targetDate ||
46
+ !(targetDate instanceof Date) ||
47
+ Number.isNaN(targetDate.getTime())
48
+ ) {
49
+ console.warn('Datepicker: Invalid target date provided');
50
+ return [];
51
+ }
52
+
53
+ // Normalise to the first Monday of calendar period
54
+ const targetYear = targetDate.getFullYear();
55
+ const targetMonth = targetDate.getMonth();
56
+ const firstOfMonth = new Date(targetYear, targetMonth, 1);
57
+ const firstMonday = getMonday(firstOfMonth);
58
+
59
+ const calendarWeeks: Date[] = [];
60
+ calendarWeeks.push(firstMonday);
61
+
62
+ // Increment to the next Monday of the calendar period
63
+ const nextMonday = new Date(firstMonday);
64
+ nextMonday.setDate(firstMonday.getDate() + 7);
65
+
66
+ // Then keep incrementing until the end of the calendar period
67
+ while (nextMonday.getMonth() === targetMonth) {
68
+ calendarWeeks.push(new Date(nextMonday));
69
+ nextMonday.setDate(nextMonday.getDate() + 7);
70
+ }
71
+
72
+ // We need an array of actual week numbers to pass to the UI
73
+ const weekNumbers: (number | undefined)[] = calendarWeeks.map((date) => {
74
+ // `Array.find()` will return `undefined` if no match is found.
75
+ // `undefined` is used by `<AcademicWeek />` for row placement.
76
+ const matchedWeek = academicWeeks.find(
77
+ (week) => new Date(week.start).toDateString() === date.toDateString()
78
+ );
79
+ return matchedWeek?.number;
80
+ });
81
+
82
+ return weekNumbers;
83
+ };
84
+
85
+ export default getAcademicWeekNumbers;
@@ -1,2 +1,3 @@
1
1
  export { default as getDatesForCalendarGrid } from './getDatesForCalendarGrid/getDatesForCalendarGrid';
2
2
  export { default as parseDate } from './parseDateForDateField/parseDateForDateField';
3
+ export { default as getAcademicWeekNumbers } from './getAcademicWeekNumbers/getAcademicWeekNumbers';
@@ -1,12 +1,13 @@
1
- import React, { memo, HTMLAttributes } from 'react';
1
+ import { memo, HTMLAttributes } from 'react';
2
2
  import { css, cx } from '@emotion/css';
3
3
  import { useTheme } from '../..';
4
4
  import UclLogo from '../UclLogo/UclLogo';
5
5
  import HeaderMenu from './HeaderMenu';
6
6
 
7
7
  export const NAME = 'ucl-uikit-header';
8
- export const HEADER_TOP_HEIGHT = 72;
9
- export const Z_INDEX = 3;
8
+ export const HEADER_DESKTOP_HEIGHT_PX = 72;
9
+ export const HEADER_MOBILE_HEIGHT_PX = 48;
10
+ export const DEFAULT_Z_INDEX = 3;
10
11
 
11
12
  export interface HeaderProps extends HTMLAttributes<HTMLElement> {
12
13
  title?: string;
@@ -24,17 +25,6 @@ const Header = ({
24
25
  }: HeaderProps) => {
25
26
  const [theme] = useTheme();
26
27
 
27
- const menuChildren: React.ReactNode[] = [];
28
- const nonMenuChildren: React.ReactNode[] = [];
29
-
30
- React.Children.forEach(children, (child) => {
31
- if (React.isValidElement(child) && child.type === HeaderMenu) {
32
- menuChildren.push(child);
33
- } else {
34
- nonMenuChildren.push(child);
35
- }
36
- });
37
-
38
28
  const baseStyle = css`
39
29
  background-color: ${theme.color.neutral.white};
40
30
  font-family: ${theme.font.family.primary};
@@ -42,6 +32,17 @@ const Header = ({
42
32
  text-rendering: optimizeLegibility;
43
33
  -webkit-font-smoothing: antialiased;
44
34
  -moz-osx-font-smoothing: grayscale;
35
+ position: relative;
36
+ background-color: ${theme.color.neutral.grey90};
37
+ color: ${theme.color.text.inverted};
38
+ display: flex;
39
+ justify-content: center;
40
+ align-items: center;
41
+ height: ${HEADER_MOBILE_HEIGHT_PX}px;
42
+
43
+ @media (min-width: ${theme.breakpoints.desktop}px) {
44
+ height: ${HEADER_DESKTOP_HEIGHT_PX}px;
45
+ }
45
46
  `;
46
47
 
47
48
  const fixedStyle = css`
@@ -49,34 +50,34 @@ const Header = ({
49
50
  left: 0;
50
51
  top: 0;
51
52
  width: 100%;
52
- z-index: ${Z_INDEX};
53
+ z-index: ${DEFAULT_Z_INDEX};
53
54
  `;
54
55
 
55
56
  const style = cx(NAME, baseStyle, fixed && fixedStyle, className);
56
57
 
57
- const topStyle = css`
58
- position: relative;
59
- height: ${HEADER_TOP_HEIGHT}px;
60
- background-color: ${theme.color.neutral.grey90};
61
- color: ${theme.color.text.inverted};
62
- display: flex;
63
- justify-content: center;
64
- align-items: center;
65
- `;
66
-
67
58
  const uclLogoStyle = css`
68
59
  position: absolute;
69
- left: 56px;
60
+ left: 16px;
70
61
  bottom: -0.5px;
71
- height: 40px;
62
+ width: auto;
63
+ height: 21px;
64
+
65
+ @media (min-width: ${theme.breakpoints.desktop}px) {
66
+ left: 56px;
67
+ height: 40px;
68
+ }
72
69
  `;
73
70
 
74
71
  const titleStyle = css`
75
- display: block;
72
+ display: none;
76
73
  margin: 0;
77
74
  font-size: ${theme.font.size.f18};
78
75
  font-weight: 700;
79
76
  text-align: center;
77
+
78
+ @media (min-width: ${theme.breakpoints.desktop}px) {
79
+ display: block;
80
+ }
80
81
  `;
81
82
 
82
83
  return (
@@ -85,12 +86,10 @@ const Header = ({
85
86
  data-testid={testId}
86
87
  {...props}
87
88
  >
88
- <div className={topStyle}>
89
- <UclLogo className={uclLogoStyle} />
90
- {title && <h1 className={titleStyle}>{title}</h1>}
89
+ <UclLogo className={uclLogoStyle} />
90
+ {title && <h1 className={titleStyle}>{title}</h1>}
91
91
 
92
- {children}
93
- </div>
92
+ {children}
94
93
  </header>
95
94
  );
96
95
  };