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.
- package/dist/components/Datepicker/Datepicker.d.ts +5 -1
- package/dist/components/Datepicker/Datepicker.stories.d.ts +8 -1
- package/dist/components/Datepicker/Datepicker.types.d.ts +7 -0
- package/dist/components/Datepicker/index.d.ts +1 -0
- package/dist/components/Datepicker/subcomponents/AcademicWeek.d.ts +5 -0
- package/dist/components/Datepicker/subcomponents/AcademicWeeks.d.ts +7 -0
- package/dist/components/Datepicker/subcomponents/CalendarGrid/CalendarGrid.d.ts +3 -1
- package/dist/components/Datepicker/subcomponents/CalendarMenu/CalendarMenu.d.ts +5 -1
- package/dist/components/Datepicker/subcomponents/Day/Day.d.ts +4 -2
- package/dist/components/Datepicker/subcomponents/Day/Day.stories.d.ts +9 -1
- package/dist/components/Datepicker/subcomponents/EventDot.d.ts +6 -0
- package/dist/components/Datepicker/utils/getAcademicWeekNumbers/getAcademicWeekNumbers.d.ts +24 -0
- package/dist/components/Datepicker/utils/getAcademicWeekNumbers/getAcademicWeekNumbers.test.d.ts +1 -0
- package/dist/components/Datepicker/utils/index.d.ts +1 -0
- package/dist/components/Header/Header.d.ts +5 -4
- package/dist/components/Header/index.d.ts +1 -1
- package/dist/components/Select/Select.stories.d.ts +1 -1
- package/dist/components/Select/Select.types.d.ts +10 -50
- package/dist/components/Select/index.d.ts +1 -1
- package/dist/components/Select/subcomponents/CustomSelect.d.ts +2 -1
- package/dist/index.js +1865 -1748
- package/lib/components/Datepicker/Datepicker.stories.tsx +133 -0
- package/lib/components/Datepicker/Datepicker.tsx +23 -46
- package/lib/components/Datepicker/Datepicker.types.ts +9 -0
- package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +487 -378
- package/lib/components/Datepicker/index.ts +1 -0
- package/lib/components/Datepicker/subcomponents/AcademicWeek.tsx +36 -0
- package/lib/components/Datepicker/subcomponents/AcademicWeeks.tsx +44 -0
- package/lib/components/Datepicker/subcomponents/CalendarGrid/CalendarGrid.tsx +9 -14
- package/lib/components/Datepicker/subcomponents/CalendarMenu/CalendarMenu.tsx +32 -6
- package/lib/components/Datepicker/subcomponents/Day/Day.stories.tsx +23 -0
- package/lib/components/Datepicker/subcomponents/Day/Day.tsx +31 -7
- package/lib/components/Datepicker/subcomponents/EventDot.tsx +40 -0
- package/lib/components/Datepicker/utils/getAcademicWeekNumbers/getAcademicWeekNumbers.test.ts +104 -0
- package/lib/components/Datepicker/utils/getAcademicWeekNumbers/getAcademicWeekNumbers.ts +85 -0
- package/lib/components/Datepicker/utils/index.ts +1 -0
- package/lib/components/Header/Header.tsx +32 -33
- package/lib/components/Header/HeaderMenu.tsx +9 -2
- package/lib/components/Header/__tests__/__snapshots__/Header.test.tsx.snap +40 -48
- package/lib/components/Header/index.ts +5 -1
- package/lib/components/Select/Select.stories.tsx +38 -39
- package/lib/components/Select/Select.tsx +4 -18
- package/lib/components/Select/Select.types.ts +30 -69
- package/lib/components/Select/__tests__/Select.test.tsx +6 -6
- package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
- package/lib/components/Select/index.ts +1 -1
- package/lib/components/Select/subcomponents/CustomSelect.tsx +22 -12
- package/lib/components/Select/subcomponents/NativeSelect.tsx +7 -3
- package/lib/components/Select/subcomponents/Panel.tsx +4 -4
- package/lib/components/Select/subcomponents/VisibleField.tsx +1 -1
- package/package.json +3 -3
- package/LICENSE +0 -9
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
13
|
+
events?: CalendarEvent[]; // Max 3 events are displayed as dots
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
const Day = ({
|
|
14
17
|
date,
|
|
15
|
-
|
|
18
|
+
onPick,
|
|
19
|
+
isSelected = false,
|
|
16
20
|
isToday = false,
|
|
17
21
|
isInCurrentMonth = true,
|
|
18
22
|
isDisabled = false,
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
9
|
-
export const
|
|
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: ${
|
|
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:
|
|
60
|
+
left: 16px;
|
|
70
61
|
bottom: -0.5px;
|
|
71
|
-
|
|
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:
|
|
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
|
-
<
|
|
89
|
-
|
|
90
|
-
{title && <h1 className={titleStyle}>{title}</h1>}
|
|
89
|
+
<UclLogo className={uclLogoStyle} />
|
|
90
|
+
{title && <h1 className={titleStyle}>{title}</h1>}
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
</div>
|
|
92
|
+
{children}
|
|
94
93
|
</header>
|
|
95
94
|
);
|
|
96
95
|
};
|