uikit-react-public 0.44.2 → 0.44.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.
- package/dist/components/Calendar/Calendar.d.ts +1 -1
- package/dist/components/Calendar/Calendar.stories.d.ts +1 -1
- package/dist/components/Calendar/Calendar.types.d.ts +8 -0
- package/dist/components/Calendar/index.d.ts +1 -1
- package/dist/components/Calendar/subcomponents/Day.d.ts +4 -2
- package/dist/components/Calendar/subcomponents/Day.stories.d.ts +1 -1
- package/dist/components/Calendar/subcomponents/EventDot.d.ts +3 -1
- package/dist/components/Calendar/subcomponents/Grid.d.ts +3 -2
- package/dist/components/Calendar/subcomponents/__tests__/Day.test.d.ts +1 -0
- package/dist/components/Calendar/subcomponents/__tests__/EventDot.test.d.ts +1 -0
- package/dist/components/Datepicker/Datepicker.types.d.ts +2 -1
- package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +3 -2
- package/dist/components/index.d.ts +1 -1
- package/dist/index.js +3275 -3227
- package/lib/components/Accordion/Accordion.tsx +1 -3
- package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +2 -2
- package/lib/components/Calendar/Calendar.stories.tsx +47 -12
- package/lib/components/Calendar/Calendar.tsx +2 -0
- package/lib/components/Calendar/Calendar.types.ts +15 -0
- package/lib/components/Calendar/__tests__/Calendar.test.tsx +37 -2
- package/lib/components/Calendar/__tests__/__snapshots__/Calendar.test.tsx.snap +462 -231
- package/lib/components/Calendar/index.ts +6 -1
- package/lib/components/Calendar/subcomponents/Day.tsx +44 -5
- package/lib/components/Calendar/subcomponents/EventDot.tsx +42 -6
- package/lib/components/Calendar/subcomponents/Grid.tsx +4 -1
- package/lib/components/Calendar/subcomponents/__tests__/Day.test.tsx +139 -0
- package/lib/components/Calendar/subcomponents/__tests__/EventDot.test.tsx +78 -0
- package/lib/components/Datepicker/Datepicker.stories.tsx +29 -6
- package/lib/components/Datepicker/Datepicker.types.ts +6 -1
- package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +26 -0
- package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +8 -1
- package/lib/components/index.ts +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { css, cx } from '@emotion/css';
|
|
2
2
|
import { EventDot } from './';
|
|
3
3
|
import { useTheme } from '../../../theme';
|
|
4
|
-
import type { CalendarEvent } from '../Calendar.types';
|
|
4
|
+
import type { CalendarEvent, CalendarStatuses } from '../Calendar.types';
|
|
5
5
|
|
|
6
6
|
export interface DayProps {
|
|
7
7
|
date: Date;
|
|
@@ -11,6 +11,8 @@ export interface DayProps {
|
|
|
11
11
|
isInCurrentMonth?: boolean;
|
|
12
12
|
isDisabled?: boolean;
|
|
13
13
|
events?: CalendarEvent[]; // Max 3 events are displayed as dots
|
|
14
|
+
statuses?: CalendarStatuses;
|
|
15
|
+
ariaLabel?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const NAME = 'ucl-uikit-calendar__day';
|
|
@@ -23,6 +25,8 @@ const Day = ({
|
|
|
23
25
|
isInCurrentMonth = true,
|
|
24
26
|
isDisabled = false,
|
|
25
27
|
events = [],
|
|
28
|
+
statuses = {},
|
|
29
|
+
ariaLabel,
|
|
26
30
|
}: DayProps) => {
|
|
27
31
|
const [theme] = useTheme();
|
|
28
32
|
const { md, mdSemibold } = theme.typography.body;
|
|
@@ -30,13 +34,37 @@ const Day = ({
|
|
|
30
34
|
// More than 3 dots displayed breaks the layout
|
|
31
35
|
const displayedEvents = events.slice(0, 3);
|
|
32
36
|
|
|
37
|
+
// Summarise all events (not just the displayed dots) as "<count> <label>" groups,
|
|
38
|
+
// e.g. "2 Pending, 1 Approved", so screen reader users learn about events beyond the 3 shown
|
|
39
|
+
const eventSummary = events.reduce<Map<string, number>>((counts, event) => {
|
|
40
|
+
const label =
|
|
41
|
+
(event.status && statuses[event.status]?.ariaLabel) || 'event';
|
|
42
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
43
|
+
return counts;
|
|
44
|
+
}, new Map());
|
|
45
|
+
|
|
46
|
+
const eventSummaryLabel = Array.from(eventSummary.entries())
|
|
47
|
+
.map(([label, count]) =>
|
|
48
|
+
label === 'event'
|
|
49
|
+
? `${count} ${count === 1 ? 'event' : 'events'}`
|
|
50
|
+
: `${count} ${label}`
|
|
51
|
+
)
|
|
52
|
+
.join(', ');
|
|
53
|
+
|
|
54
|
+
const defaultAriaLabel = `${date.getDate()} ${date.toLocaleString('default', { month: 'long' })} ${date.getFullYear()}`;
|
|
55
|
+
const computedAriaLabel =
|
|
56
|
+
ariaLabel ??
|
|
57
|
+
[defaultAriaLabel, eventSummaryLabel].filter(Boolean).join(', ');
|
|
58
|
+
|
|
33
59
|
const handlePick = (event: React.MouseEvent) => {
|
|
34
|
-
if (date &&
|
|
60
|
+
if (date && onPick) {
|
|
35
61
|
onPick(date, event);
|
|
36
62
|
}
|
|
37
63
|
};
|
|
38
64
|
|
|
39
65
|
const backgroundStyle = css`
|
|
66
|
+
all: unset;
|
|
67
|
+
box-sizing: border-box;
|
|
40
68
|
display: flex;
|
|
41
69
|
justify-content: center;
|
|
42
70
|
align-items: center;
|
|
@@ -45,12 +73,16 @@ const Day = ({
|
|
|
45
73
|
height: 40px;
|
|
46
74
|
background-color: ${theme.colour.fill.inverse};
|
|
47
75
|
cursor: pointer;
|
|
48
|
-
outline: none;
|
|
49
76
|
|
|
50
77
|
&:hover {
|
|
51
78
|
background-color: ${theme.colour.fill.primary};
|
|
52
79
|
}
|
|
53
80
|
|
|
81
|
+
&:focus-visible {
|
|
82
|
+
box-shadow: ${theme.boxShadow.focus};
|
|
83
|
+
z-index: 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
54
86
|
${
|
|
55
87
|
isSelected &&
|
|
56
88
|
css`
|
|
@@ -120,10 +152,14 @@ const Day = ({
|
|
|
120
152
|
const style = cx(NAME, backgroundStyle);
|
|
121
153
|
|
|
122
154
|
return (
|
|
123
|
-
<
|
|
155
|
+
<button
|
|
156
|
+
type='button'
|
|
124
157
|
onClick={handlePick}
|
|
158
|
+
disabled={isDisabled}
|
|
125
159
|
className={style}
|
|
126
160
|
data-testid={NAME}
|
|
161
|
+
aria-label={computedAriaLabel}
|
|
162
|
+
tabIndex={-1}
|
|
127
163
|
>
|
|
128
164
|
<div className={foregroundStyle}>{date.getDate()}</div>
|
|
129
165
|
<div className={eventDotsContainerStyle}>
|
|
@@ -131,11 +167,14 @@ const Day = ({
|
|
|
131
167
|
<EventDot
|
|
132
168
|
key={index}
|
|
133
169
|
inverted={isSelected}
|
|
170
|
+
variant={
|
|
171
|
+
_event.status ? statuses[_event.status]?.variant : undefined
|
|
172
|
+
}
|
|
134
173
|
inCurrentMonth={isInCurrentMonth}
|
|
135
174
|
/>
|
|
136
175
|
))}
|
|
137
176
|
</div>
|
|
138
|
-
</
|
|
177
|
+
</button>
|
|
139
178
|
);
|
|
140
179
|
};
|
|
141
180
|
|
|
@@ -3,19 +3,55 @@ import { useTheme } from '../../../theme';
|
|
|
3
3
|
|
|
4
4
|
const NAME = 'ucl-uikit-calendar__event-dot';
|
|
5
5
|
|
|
6
|
+
import type { CalendarEventVariant } from '../Calendar.types';
|
|
7
|
+
|
|
6
8
|
interface EventDotProps {
|
|
7
9
|
inverted: boolean;
|
|
8
10
|
inCurrentMonth: boolean;
|
|
11
|
+
variant?: CalendarEventVariant;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
const
|
|
14
|
+
const getBackgroundColour = (
|
|
15
|
+
theme: ReturnType<typeof useTheme>[0],
|
|
16
|
+
inverted: boolean,
|
|
17
|
+
inCurrentMonth: boolean,
|
|
18
|
+
variant?: CalendarEventVariant
|
|
19
|
+
) => {
|
|
20
|
+
if (!inCurrentMonth) {
|
|
21
|
+
return theme.colour.icon.disabled;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
switch (variant) {
|
|
25
|
+
case 'critical':
|
|
26
|
+
return theme.colour.icon.critical;
|
|
27
|
+
case 'success':
|
|
28
|
+
return theme.colour.icon.success;
|
|
29
|
+
case 'caution':
|
|
30
|
+
return theme.colour.icon.caution;
|
|
31
|
+
case 'warning':
|
|
32
|
+
return theme.colour.icon.warning;
|
|
33
|
+
case 'info':
|
|
34
|
+
if (inverted) {
|
|
35
|
+
return theme.colour.icon.inverse;
|
|
36
|
+
}
|
|
37
|
+
return theme.colour.fill.brand;
|
|
38
|
+
default:
|
|
39
|
+
if (inverted) {
|
|
40
|
+
return theme.colour.icon.inverse;
|
|
41
|
+
}
|
|
42
|
+
return theme.colour.fill.brand;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const EventDot = ({ inverted, inCurrentMonth, variant }: EventDotProps) => {
|
|
12
47
|
const [theme] = useTheme();
|
|
13
48
|
|
|
14
|
-
const backgroundColour =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
49
|
+
const backgroundColour = getBackgroundColour(
|
|
50
|
+
theme,
|
|
51
|
+
inverted,
|
|
52
|
+
inCurrentMonth,
|
|
53
|
+
variant
|
|
54
|
+
);
|
|
19
55
|
|
|
20
56
|
const baseStyle = css`
|
|
21
57
|
width: 6px;
|
|
@@ -3,7 +3,7 @@ import { useMemo } from 'react';
|
|
|
3
3
|
import { ColumnHeading, Day } from './';
|
|
4
4
|
import { getDatesForCalendarGrid } from '../utils';
|
|
5
5
|
import { useTheme } from '../../../theme';
|
|
6
|
-
import type { CalendarEvent } from '../Calendar.types';
|
|
6
|
+
import type { CalendarEvent, CalendarStatuses } from '../Calendar.types';
|
|
7
7
|
|
|
8
8
|
interface GridProps {
|
|
9
9
|
month: Date; // 1st day of the month
|
|
@@ -12,6 +12,7 @@ interface GridProps {
|
|
|
12
12
|
minDate: Date | null;
|
|
13
13
|
maxDate: Date | null;
|
|
14
14
|
events: CalendarEvent[];
|
|
15
|
+
statuses?: CalendarStatuses;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
const NAME = 'ucl-uikit-calendar__grid';
|
|
@@ -51,6 +52,7 @@ const Grid = ({
|
|
|
51
52
|
minDate,
|
|
52
53
|
maxDate,
|
|
53
54
|
events,
|
|
55
|
+
statuses = {},
|
|
54
56
|
}: GridProps) => {
|
|
55
57
|
// Fix minDate timezone issues by zeroing time if valid
|
|
56
58
|
if (minDate && !isNaN(minDate.getTime())) minDate.setHours(0, 0, 0, 0);
|
|
@@ -106,6 +108,7 @@ const Grid = ({
|
|
|
106
108
|
}
|
|
107
109
|
onPick={onDatePick}
|
|
108
110
|
events={dateEvents}
|
|
111
|
+
statuses={statuses}
|
|
109
112
|
/>
|
|
110
113
|
);
|
|
111
114
|
})}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { ThemeContextProvider, theme } from '../../../../theme';
|
|
4
|
+
import Day from '../Day';
|
|
5
|
+
import type { CalendarStatuses } from '../../Calendar.types';
|
|
6
|
+
|
|
7
|
+
const defaultTestId = 'ucl-uikit-calendar__day';
|
|
8
|
+
|
|
9
|
+
const hexToRgb = (hex: string) => {
|
|
10
|
+
const value = hex.replace('#', '');
|
|
11
|
+
const red = parseInt(value.slice(0, 2), 16);
|
|
12
|
+
const green = parseInt(value.slice(2, 4), 16);
|
|
13
|
+
const blue = parseInt(value.slice(4, 6), 16);
|
|
14
|
+
|
|
15
|
+
return `rgb(${red}, ${green}, ${blue})`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const customRender = (ui: React.ReactElement) => {
|
|
19
|
+
return render(ui, {
|
|
20
|
+
wrapper: ({ children }) => (
|
|
21
|
+
<ThemeContextProvider>{children}</ThemeContextProvider>
|
|
22
|
+
),
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const statuses: CalendarStatuses = {
|
|
27
|
+
pending: { variant: 'warning', ariaLabel: 'Pending' },
|
|
28
|
+
approved: { variant: 'success', ariaLabel: 'Approved' },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('Day', () => {
|
|
32
|
+
test('Default aria-label has no event summary when there are no events', () => {
|
|
33
|
+
customRender(<Day date={new Date(2026, 7, 27)} />);
|
|
34
|
+
expect(screen.getByTestId(defaultTestId)).toHaveAttribute(
|
|
35
|
+
'aria-label',
|
|
36
|
+
'27 August 2026'
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('Summarises event statuses with counts in the aria-label', () => {
|
|
41
|
+
customRender(
|
|
42
|
+
<Day
|
|
43
|
+
date={new Date(2026, 7, 27)}
|
|
44
|
+
events={[
|
|
45
|
+
{ date: '2026-08-27', status: 'pending' },
|
|
46
|
+
{ date: '2026-08-27', status: 'pending' },
|
|
47
|
+
{ date: '2026-08-27', status: 'approved' },
|
|
48
|
+
]}
|
|
49
|
+
statuses={statuses}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
expect(screen.getByTestId(defaultTestId)).toHaveAttribute(
|
|
53
|
+
'aria-label',
|
|
54
|
+
'27 August 2026, 2 Pending, 1 Approved'
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('Falls back to a generic "event" label for events with an unmatched status', () => {
|
|
59
|
+
customRender(
|
|
60
|
+
<Day
|
|
61
|
+
date={new Date(2026, 7, 27)}
|
|
62
|
+
events={[{ date: '2026-08-27', status: 'unknown' }]}
|
|
63
|
+
statuses={statuses}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
expect(screen.getByTestId(defaultTestId)).toHaveAttribute(
|
|
67
|
+
'aria-label',
|
|
68
|
+
'27 August 2026, 1 event'
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('Summarises events beyond the 3 displayed as dots', () => {
|
|
73
|
+
customRender(
|
|
74
|
+
<Day
|
|
75
|
+
date={new Date(2026, 7, 27)}
|
|
76
|
+
events={[
|
|
77
|
+
{ date: '2026-08-27', status: 'pending' },
|
|
78
|
+
{ date: '2026-08-27', status: 'pending' },
|
|
79
|
+
{ date: '2026-08-27', status: 'pending' },
|
|
80
|
+
{ date: '2026-08-27', status: 'pending' },
|
|
81
|
+
]}
|
|
82
|
+
statuses={statuses}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
expect(screen.getByTestId(defaultTestId)).toHaveAttribute(
|
|
86
|
+
'aria-label',
|
|
87
|
+
'27 August 2026, 4 Pending'
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('An explicit ariaLabel overrides the computed one', () => {
|
|
92
|
+
customRender(
|
|
93
|
+
<Day
|
|
94
|
+
date={new Date(2026, 7, 27)}
|
|
95
|
+
ariaLabel='Custom label'
|
|
96
|
+
events={[{ date: '2026-08-27', status: 'pending' }]}
|
|
97
|
+
statuses={statuses}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
expect(screen.getByTestId(defaultTestId)).toHaveAttribute(
|
|
101
|
+
'aria-label',
|
|
102
|
+
'Custom label'
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('Resolves each event dot colour via the statuses config', () => {
|
|
107
|
+
customRender(
|
|
108
|
+
<Day
|
|
109
|
+
date={new Date(2026, 7, 27)}
|
|
110
|
+
events={[
|
|
111
|
+
{ date: '2026-08-27', status: 'pending' },
|
|
112
|
+
{ date: '2026-08-27', status: 'approved' },
|
|
113
|
+
]}
|
|
114
|
+
statuses={statuses}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
const dots = screen.getAllByTestId('ucl-uikit-calendar__event-dot');
|
|
118
|
+
expect(getComputedStyle(dots[0]).backgroundColor).toBe(
|
|
119
|
+
hexToRgb(theme.colour.icon.warning)
|
|
120
|
+
);
|
|
121
|
+
expect(getComputedStyle(dots[1]).backgroundColor).toBe(
|
|
122
|
+
hexToRgb(theme.colour.icon.success)
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('Falls back to the default dot colour when an event status has no matching config', () => {
|
|
127
|
+
customRender(
|
|
128
|
+
<Day
|
|
129
|
+
date={new Date(2026, 7, 27)}
|
|
130
|
+
events={[{ date: '2026-08-27', status: 'unknown' }]}
|
|
131
|
+
statuses={statuses}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
const dot = screen.getByTestId('ucl-uikit-calendar__event-dot');
|
|
135
|
+
expect(getComputedStyle(dot).backgroundColor).toBe(
|
|
136
|
+
hexToRgb(theme.colour.icon.brand)
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { ThemeContextProvider, theme } from '../../../../theme';
|
|
4
|
+
import EventDot from '../EventDot';
|
|
5
|
+
|
|
6
|
+
const defaultTestId = 'ucl-uikit-calendar__event-dot';
|
|
7
|
+
|
|
8
|
+
const hexToRgb = (hex: string) => {
|
|
9
|
+
const value = hex.replace('#', '');
|
|
10
|
+
const red = parseInt(value.slice(0, 2), 16);
|
|
11
|
+
const green = parseInt(value.slice(2, 4), 16);
|
|
12
|
+
const blue = parseInt(value.slice(4, 6), 16);
|
|
13
|
+
|
|
14
|
+
return `rgb(${red}, ${green}, ${blue})`;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const customRender = (ui: React.ReactElement) => {
|
|
18
|
+
return render(ui, {
|
|
19
|
+
wrapper: ({ children }) => (
|
|
20
|
+
<ThemeContextProvider>{children}</ThemeContextProvider>
|
|
21
|
+
),
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getBackgroundColour = () =>
|
|
26
|
+
getComputedStyle(screen.getByTestId(defaultTestId)).backgroundColor;
|
|
27
|
+
|
|
28
|
+
describe('EventDot', () => {
|
|
29
|
+
test('Uses the disabled colour when outside the current month, regardless of variant', () => {
|
|
30
|
+
customRender(
|
|
31
|
+
<EventDot
|
|
32
|
+
inverted={false}
|
|
33
|
+
inCurrentMonth={false}
|
|
34
|
+
variant='success'
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
expect(getBackgroundColour()).toBe(hexToRgb(theme.colour.icon.disabled));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test.each([
|
|
41
|
+
['critical', theme.colour.icon.critical],
|
|
42
|
+
['success', theme.colour.icon.success],
|
|
43
|
+
['caution', theme.colour.icon.caution],
|
|
44
|
+
['warning', theme.colour.icon.warning],
|
|
45
|
+
] as const)(
|
|
46
|
+
'Uses the icon colour for the %s variant',
|
|
47
|
+
(variant, expectedColour) => {
|
|
48
|
+
customRender(
|
|
49
|
+
<EventDot
|
|
50
|
+
inverted={false}
|
|
51
|
+
inCurrentMonth
|
|
52
|
+
variant={variant}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
expect(getBackgroundColour()).toBe(hexToRgb(expectedColour));
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
test('Defaults to the brand colour when no variant is provided', () => {
|
|
60
|
+
customRender(
|
|
61
|
+
<EventDot
|
|
62
|
+
inverted={false}
|
|
63
|
+
inCurrentMonth
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
expect(getBackgroundColour()).toBe(hexToRgb(theme.colour.icon.brand));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('Uses the inverse fill colour when inverted and no variant is provided', () => {
|
|
70
|
+
customRender(
|
|
71
|
+
<EventDot
|
|
72
|
+
inverted
|
|
73
|
+
inCurrentMonth
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
expect(getBackgroundColour()).toBe(hexToRgb(theme.colour.fill.inverse));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -113,14 +113,37 @@ export const WithEvents: Story = {
|
|
|
113
113
|
{ date: dateToISOString(new Date(currentYear, currentMonth, 0)) },
|
|
114
114
|
{ date: dateToISOString(new Date(currentYear, currentMonth, 0)) },
|
|
115
115
|
// Blue event dots
|
|
116
|
-
{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
{
|
|
121
|
-
|
|
116
|
+
{
|
|
117
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 1)),
|
|
118
|
+
status: 'pending',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 2)),
|
|
122
|
+
status: 'approved',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 2)),
|
|
126
|
+
status: 'rejected',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 3)),
|
|
130
|
+
status: 'pending',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 3)),
|
|
134
|
+
status: 'approved',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 3)),
|
|
138
|
+
status: 'approved',
|
|
139
|
+
},
|
|
122
140
|
];
|
|
123
141
|
})(),
|
|
142
|
+
statuses: {
|
|
143
|
+
pending: { variant: 'warning', ariaLabel: 'Pending' },
|
|
144
|
+
approved: { variant: 'success', ariaLabel: 'Approved' },
|
|
145
|
+
rejected: { variant: 'critical', ariaLabel: 'Rejected' },
|
|
146
|
+
},
|
|
124
147
|
},
|
|
125
148
|
render: () => {
|
|
126
149
|
const [args, updateArgs] = useArgs();
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { HTMLAttributes, RefObject } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
CalendarEvent,
|
|
4
|
+
CalendarStatuses,
|
|
5
|
+
AcademicWeek,
|
|
6
|
+
} from '../Calendar';
|
|
3
7
|
import type { InputProps } from './subcomponents/DatepickerInput';
|
|
4
8
|
|
|
5
9
|
export type DatepickerValue = Date | null;
|
|
@@ -16,6 +20,7 @@ interface BaseDatepickerProps {
|
|
|
16
20
|
maxYear?: number;
|
|
17
21
|
pickedMonth?: Date | null; // Pre-selects a month to display when no value is set
|
|
18
22
|
events?: CalendarEvent[];
|
|
23
|
+
statuses?: CalendarStatuses;
|
|
19
24
|
showAcademicWeeks?: boolean;
|
|
20
25
|
academicWeeks?: AcademicWeek[];
|
|
21
26
|
testId?: string;
|
|
@@ -447,6 +447,32 @@ describe('Datepicker', () => {
|
|
|
447
447
|
expect(screen.getByRole('button', { name: 'After' })).toHaveFocus();
|
|
448
448
|
});
|
|
449
449
|
|
|
450
|
+
test('Forwards the statuses prop to the underlying Calendar', () => {
|
|
451
|
+
customRender(
|
|
452
|
+
<Datepicker
|
|
453
|
+
value={new Date(2026, 2, 10)} // 10th March 2026 (2 is March, 0-indexed)
|
|
454
|
+
events={[{ date: '2026-03-10', status: 'pending' }]}
|
|
455
|
+
statuses={{
|
|
456
|
+
pending: { variant: 'warning', ariaLabel: 'Pending' },
|
|
457
|
+
}}
|
|
458
|
+
/>
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const openCalendarButton = screen.getByTestId(
|
|
462
|
+
'ucl-uikit-datepicker__visible-field-icon-button'
|
|
463
|
+
);
|
|
464
|
+
fireEvent.click(openCalendarButton);
|
|
465
|
+
|
|
466
|
+
const days = screen.getAllByTestId('ucl-uikit-calendar__day');
|
|
467
|
+
// Pick 10th March 2026: 6 days of previous month + 10 days of current month - 1 for 0-indexing
|
|
468
|
+
const dateArrayIndex = 6 + 10 - 1;
|
|
469
|
+
|
|
470
|
+
expect(days[dateArrayIndex]).toHaveAttribute(
|
|
471
|
+
'aria-label',
|
|
472
|
+
expect.stringContaining('Pending')
|
|
473
|
+
);
|
|
474
|
+
});
|
|
475
|
+
|
|
450
476
|
test('Disabled Datepicker icon control is not tabbable and cannot open', async () => {
|
|
451
477
|
const user = userEvent.setup();
|
|
452
478
|
|
|
@@ -7,7 +7,11 @@ import { parseInputValue } from '../utils';
|
|
|
7
7
|
import announce from '../../../utils/announce';
|
|
8
8
|
import { useTheme } from '../../../theme';
|
|
9
9
|
import type { DatepickerValue } from '../Datepicker.types';
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
CalendarEvent,
|
|
12
|
+
CalendarStatuses,
|
|
13
|
+
AcademicWeek,
|
|
14
|
+
} from '../../Calendar';
|
|
11
15
|
import type { InputProps } from './DatepickerInput';
|
|
12
16
|
|
|
13
17
|
interface CustomDatepickerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -24,6 +28,7 @@ interface CustomDatepickerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
24
28
|
disabled?: boolean;
|
|
25
29
|
clearable?: boolean;
|
|
26
30
|
events?: CalendarEvent[];
|
|
31
|
+
statuses?: CalendarStatuses;
|
|
27
32
|
showAcademicWeeks?: boolean;
|
|
28
33
|
academicWeeks?: AcademicWeek[];
|
|
29
34
|
testId?: string;
|
|
@@ -45,6 +50,7 @@ const CustomDatepicker = ({
|
|
|
45
50
|
disabled = false,
|
|
46
51
|
clearable = false,
|
|
47
52
|
events,
|
|
53
|
+
statuses,
|
|
48
54
|
showAcademicWeeks,
|
|
49
55
|
academicWeeks = [],
|
|
50
56
|
testId = NAME,
|
|
@@ -274,6 +280,7 @@ const CustomDatepicker = ({
|
|
|
274
280
|
minYear={minYear}
|
|
275
281
|
maxYear={maxYear}
|
|
276
282
|
events={events}
|
|
283
|
+
statuses={statuses}
|
|
277
284
|
showAcademicWeeks={showAcademicWeeks}
|
|
278
285
|
academicWeeks={academicWeeks}
|
|
279
286
|
/>
|
package/lib/components/index.ts
CHANGED
|
@@ -211,7 +211,7 @@ export { default as NativeDatepicker } from './NativeDatepicker';
|
|
|
211
211
|
export type { NativeDatepickerProps } from './NativeDatepicker';
|
|
212
212
|
|
|
213
213
|
export { default as Calendar } from './Calendar';
|
|
214
|
-
export type { CalendarProps } from './Calendar';
|
|
214
|
+
export type { CalendarProps, CalendarStatuses } from './Calendar';
|
|
215
215
|
|
|
216
216
|
export { default as WeekPicker } from './WeekPicker';
|
|
217
217
|
|