uikit-react-public 0.44.0 → 0.44.3
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 +6 -1
- package/dist/components/Calendar/Calendar.types.d.ts +9 -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.stories.d.ts +5 -0
- package/dist/components/Datepicker/Datepicker.types.d.ts +3 -1
- package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +4 -2
- package/dist/components/index.d.ts +1 -1
- package/dist/index.js +1805 -1751
- package/lib/components/Calendar/Calendar.stories.tsx +60 -12
- package/lib/components/Calendar/Calendar.tsx +27 -20
- package/lib/components/Calendar/Calendar.types.ts +17 -1
- package/lib/components/Calendar/__tests__/Calendar.test.tsx +76 -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 +55 -24
- package/lib/components/Datepicker/Datepicker.types.ts +7 -1
- package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +90 -0
- package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +16 -1
- package/lib/components/index.ts +1 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ const meta = {
|
|
|
9
9
|
parameters: { layout: 'padded' },
|
|
10
10
|
argTypes: {
|
|
11
11
|
pickedDate: { control: { type: 'date' } },
|
|
12
|
+
pickedMonth: { control: { type: 'date' } },
|
|
12
13
|
minDate: { control: { type: 'date' } },
|
|
13
14
|
maxDate: { control: { type: 'date' } },
|
|
14
15
|
showAcademicWeeks: { control: { type: 'boolean' } },
|
|
@@ -37,6 +38,9 @@ export const Default: Story = {
|
|
|
37
38
|
args.pickedDate = args.pickedDate
|
|
38
39
|
? parseDateFromUNIXTimestamp(args.pickedDate)
|
|
39
40
|
: null;
|
|
41
|
+
args.pickedMonth = args.pickedMonth
|
|
42
|
+
? parseDateFromUNIXTimestamp(args.pickedMonth)
|
|
43
|
+
: null;
|
|
40
44
|
const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
|
|
41
45
|
return (
|
|
42
46
|
<Calendar
|
|
@@ -57,28 +61,66 @@ export const WithEvents: Story = {
|
|
|
57
61
|
const currentYear = currentDate.getFullYear();
|
|
58
62
|
const currentMonth = currentDate.getMonth();
|
|
59
63
|
return [
|
|
60
|
-
//
|
|
64
|
+
// Events outside the current month are always shown as disabled, regardless of status
|
|
61
65
|
{ date: dateToISOString(new Date(currentYear, currentMonth, -2)) },
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
|
|
66
|
+
{
|
|
67
|
+
date: dateToISOString(new Date(currentYear, currentMonth, -1)),
|
|
68
|
+
status: 'pending',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
date: dateToISOString(new Date(currentYear, currentMonth, -1)),
|
|
72
|
+
status: 'pending',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 0)),
|
|
76
|
+
status: 'approved',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 0)),
|
|
80
|
+
status: 'approved',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 0)),
|
|
84
|
+
status: 'approved',
|
|
85
|
+
},
|
|
86
|
+
// Events within the current month show variant colours: brand (default), warning (pending), success (approved)
|
|
68
87
|
{ date: dateToISOString(new Date(currentYear, currentMonth, 1)) },
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
{
|
|
88
|
+
{
|
|
89
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 2)),
|
|
90
|
+
status: 'pending',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 2)),
|
|
94
|
+
status: 'pending',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 3)),
|
|
98
|
+
status: 'approved',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 3)),
|
|
102
|
+
status: 'pending',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
date: dateToISOString(new Date(currentYear, currentMonth, 3)),
|
|
106
|
+
status: 'rejected',
|
|
107
|
+
},
|
|
74
108
|
];
|
|
75
109
|
})(),
|
|
110
|
+
statuses: {
|
|
111
|
+
pending: { variant: 'warning', ariaLabel: 'Pending' },
|
|
112
|
+
approved: { variant: 'success', ariaLabel: 'Approved' },
|
|
113
|
+
rejected: { variant: 'critical', ariaLabel: 'Rejected' },
|
|
114
|
+
},
|
|
76
115
|
},
|
|
77
116
|
render: () => {
|
|
78
117
|
const [args, updateArgs] = useArgs();
|
|
79
118
|
args.pickedDate = args.pickedDate
|
|
80
119
|
? parseDateFromUNIXTimestamp(args.pickedDate)
|
|
81
120
|
: null;
|
|
121
|
+
args.pickedMonth = args.pickedMonth
|
|
122
|
+
? parseDateFromUNIXTimestamp(args.pickedMonth)
|
|
123
|
+
: null;
|
|
82
124
|
const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
|
|
83
125
|
return (
|
|
84
126
|
<Calendar
|
|
@@ -158,6 +200,9 @@ export const WithAcademicWeeks: Story = {
|
|
|
158
200
|
args.pickedDate = args.pickedDate
|
|
159
201
|
? parseDateFromUNIXTimestamp(args.pickedDate)
|
|
160
202
|
: null;
|
|
203
|
+
args.pickedMonth = args.pickedMonth
|
|
204
|
+
? parseDateFromUNIXTimestamp(args.pickedMonth)
|
|
205
|
+
: null;
|
|
161
206
|
const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
|
|
162
207
|
return (
|
|
163
208
|
<Calendar
|
|
@@ -198,6 +243,9 @@ export const MinMaxDates: Story = {
|
|
|
198
243
|
args.pickedDate = args.pickedDate
|
|
199
244
|
? parseDateFromUNIXTimestamp(args.pickedDate)
|
|
200
245
|
: null;
|
|
246
|
+
args.pickedMonth = args.pickedMonth
|
|
247
|
+
? parseDateFromUNIXTimestamp(args.pickedMonth)
|
|
248
|
+
: null;
|
|
201
249
|
const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
|
|
202
250
|
return (
|
|
203
251
|
<Calendar
|
|
@@ -11,12 +11,14 @@ const DEFAULT_MAX_YEAR = 2100;
|
|
|
11
11
|
|
|
12
12
|
const Calendar = ({
|
|
13
13
|
pickedDate = null,
|
|
14
|
+
pickedMonth = null,
|
|
14
15
|
onDatePick,
|
|
15
16
|
minDate = null,
|
|
16
17
|
maxDate = null,
|
|
17
18
|
minYear = DEFAULT_MIN_YEAR,
|
|
18
19
|
maxYear = DEFAULT_MAX_YEAR,
|
|
19
20
|
events = [],
|
|
21
|
+
statuses = {},
|
|
20
22
|
showAcademicWeeks = false,
|
|
21
23
|
academicWeeks = [],
|
|
22
24
|
testId = NAME,
|
|
@@ -37,6 +39,11 @@ const Calendar = ({
|
|
|
37
39
|
pickedDate = null;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
if (pickedMonth && isNaN(pickedMonth.getTime())) {
|
|
43
|
+
console.warn('Calendar: pickedMonth is invalid, defaulting to null');
|
|
44
|
+
pickedMonth = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
const clampDisplayMonth = useCallback(
|
|
41
48
|
(date: Date) => {
|
|
42
49
|
const year = date.getFullYear();
|
|
@@ -54,36 +61,35 @@ const Calendar = ({
|
|
|
54
61
|
[yearRangeEnd, yearRangeStart]
|
|
55
62
|
);
|
|
56
63
|
|
|
64
|
+
// pickedDate takes precedence over pickedMonth
|
|
65
|
+
const activePickedDate = pickedDate ?? pickedMonth;
|
|
66
|
+
|
|
57
67
|
// Used to track prop value changes
|
|
58
|
-
const
|
|
68
|
+
const activePickedDateRef = useRef<Date | null>(activePickedDate);
|
|
59
69
|
|
|
60
70
|
// Determines the month currently displayed in the calendar
|
|
61
71
|
const [displayMonth, setDisplayMonth] = useState<Date>(
|
|
62
|
-
// Display month initialises as
|
|
63
|
-
clampDisplayMonth(
|
|
72
|
+
// Display month initialises as the picked date's month, else the picked month, else the current month
|
|
73
|
+
clampDisplayMonth(activePickedDate ?? new Date())
|
|
64
74
|
);
|
|
65
75
|
|
|
66
|
-
// Snap displayed month to the picked date
|
|
76
|
+
// Snap displayed month to the active picked date/month if it changes
|
|
67
77
|
useEffect(() => {
|
|
68
78
|
if (
|
|
69
|
-
// If
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
// If there's an active picked date/month
|
|
80
|
+
activePickedDate &&
|
|
81
|
+
// If it has changed
|
|
82
|
+
activePickedDateRef.current?.getTime() !== activePickedDate.getTime() &&
|
|
83
|
+
// If it's not in the currently displayed month
|
|
84
|
+
(activePickedDate.getMonth() !== displayMonth.getMonth() ||
|
|
85
|
+
activePickedDate.getFullYear() !== displayMonth.getFullYear())
|
|
74
86
|
) {
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
pickedDate.getMonth() !== displayMonth.getMonth() ||
|
|
78
|
-
pickedDate.getFullYear() !== displayMonth.getFullYear()
|
|
79
|
-
) {
|
|
80
|
-
// Update the display month to the picked date's month
|
|
81
|
-
setDisplayMonth(clampDisplayMonth(pickedDate));
|
|
82
|
-
}
|
|
87
|
+
// Update the display month to match
|
|
88
|
+
setDisplayMonth(clampDisplayMonth(activePickedDate));
|
|
83
89
|
}
|
|
84
|
-
// Update the ref to the new picked date
|
|
85
|
-
|
|
86
|
-
}, [
|
|
90
|
+
// Update the ref to the new active picked date/month
|
|
91
|
+
activePickedDateRef.current = activePickedDate;
|
|
92
|
+
}, [activePickedDate, displayMonth, clampDisplayMonth]);
|
|
87
93
|
|
|
88
94
|
useEffect(() => {
|
|
89
95
|
const clampedDisplayMonth = clampDisplayMonth(displayMonth);
|
|
@@ -161,6 +167,7 @@ const Calendar = ({
|
|
|
161
167
|
minDate={minDateParsed}
|
|
162
168
|
maxDate={maxDateParsed}
|
|
163
169
|
events={events}
|
|
170
|
+
statuses={statuses}
|
|
164
171
|
/>
|
|
165
172
|
</div>
|
|
166
173
|
</div>
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
// Used to determine the colour of the EventDot
|
|
2
|
+
export type CalendarEventVariant =
|
|
3
|
+
'critical' | 'success' | 'caution' | 'warning' | 'info';
|
|
4
|
+
|
|
5
|
+
// Maps a status key to the EventDot variant and Day aria-label it should apply
|
|
6
|
+
export type CalendarStatusConfig = {
|
|
7
|
+
variant: CalendarEventVariant;
|
|
8
|
+
ariaLabel: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Keyed by the status names referenced by `CalendarEvent.status`
|
|
12
|
+
export type CalendarStatuses = Record<string, CalendarStatusConfig>;
|
|
13
|
+
|
|
1
14
|
// Used to display EventDots inside `<Day>` component
|
|
2
15
|
export type CalendarEvent = {
|
|
3
16
|
date: string; // Date string in YYYY-MM-DD format
|
|
17
|
+
status?: string; // Key into the `statuses` prop on `<Calendar>`
|
|
4
18
|
};
|
|
5
19
|
|
|
6
20
|
export type AcademicWeek = {
|
|
@@ -9,13 +23,15 @@ export type AcademicWeek = {
|
|
|
9
23
|
};
|
|
10
24
|
|
|
11
25
|
export interface CalendarProps {
|
|
12
|
-
pickedDate?: Date | null;
|
|
26
|
+
pickedDate?: Date | null; // Date to select and display
|
|
27
|
+
pickedMonth?: Date | null; // Date whose month is displayed when pickedDate is not set
|
|
13
28
|
onDatePick?: (date: Date | null, event?: React.SyntheticEvent) => void;
|
|
14
29
|
minDate?: string | null; // ISO date string: YYYY-MM-DD format
|
|
15
30
|
maxDate?: string | null; // ISO date string: YYYY-MM-DD format
|
|
16
31
|
minYear?: number;
|
|
17
32
|
maxYear?: number;
|
|
18
33
|
events?: CalendarEvent[];
|
|
34
|
+
statuses?: CalendarStatuses;
|
|
19
35
|
showAcademicWeeks?: boolean;
|
|
20
36
|
academicWeeks?: AcademicWeek[];
|
|
21
37
|
testId?: string;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { describe, test, expect, vi, beforeAll } from 'vitest';
|
|
2
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
-
import { ThemeContextProvider } from '../../../theme';
|
|
2
|
+
import { fireEvent, render, screen, within } from '@testing-library/react';
|
|
3
|
+
import { ThemeContextProvider, theme } from '../../../theme';
|
|
4
4
|
import Calendar from '../Calendar';
|
|
5
5
|
|
|
6
6
|
const defaultTestId = 'ucl-uikit-calendar';
|
|
7
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
|
+
|
|
8
17
|
const customRender = (ui: React.ReactElement) => {
|
|
9
18
|
return render(ui, {
|
|
10
19
|
wrapper: ({ children }) => (
|
|
@@ -84,6 +93,45 @@ describe('Calendar', () => {
|
|
|
84
93
|
);
|
|
85
94
|
});
|
|
86
95
|
|
|
96
|
+
test('Displays the picked month without selecting an individual date', () => {
|
|
97
|
+
customRender(<Calendar pickedMonth={new Date('2025-07-01')} />);
|
|
98
|
+
|
|
99
|
+
expect(screen.getByRole('combobox', { name: 'Month' })).toHaveTextContent(
|
|
100
|
+
'July'
|
|
101
|
+
);
|
|
102
|
+
expect(screen.getByRole('combobox', { name: 'Year' })).toHaveTextContent(
|
|
103
|
+
'2025'
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('pickedDate takes precedence over pickedMonth for the displayed month', () => {
|
|
108
|
+
customRender(
|
|
109
|
+
<Calendar
|
|
110
|
+
pickedDate={new Date('2025-03-22')}
|
|
111
|
+
pickedMonth={new Date('2025-07-01')}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
expect(screen.getByRole('combobox', { name: 'Month' })).toHaveTextContent(
|
|
116
|
+
'March'
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('Updates the displayed month when the pickedMonth prop changes', () => {
|
|
121
|
+
const { rerender } = customRender(
|
|
122
|
+
<Calendar pickedMonth={new Date('2025-07-01')} />
|
|
123
|
+
);
|
|
124
|
+
expect(screen.getByRole('combobox', { name: 'Month' })).toHaveTextContent(
|
|
125
|
+
'July'
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
rerender(<Calendar pickedMonth={new Date('2025-09-01')} />);
|
|
129
|
+
|
|
130
|
+
expect(screen.getByRole('combobox', { name: 'Month' })).toHaveTextContent(
|
|
131
|
+
'September'
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
87
135
|
test('Can change the displayed year with the year select', () => {
|
|
88
136
|
customRender(<Calendar pickedDate={new Date('2025-03-01')} />);
|
|
89
137
|
|
|
@@ -142,6 +190,32 @@ describe('Calendar', () => {
|
|
|
142
190
|
).not.toBeInTheDocument();
|
|
143
191
|
});
|
|
144
192
|
|
|
193
|
+
test('Applies EventDot colours and Day aria-labels from the statuses prop', () => {
|
|
194
|
+
customRender(
|
|
195
|
+
<Calendar
|
|
196
|
+
pickedDate={new Date('2025-03-01')} // To ensure the calendar is displayed for March 2025
|
|
197
|
+
events={[
|
|
198
|
+
{ date: '2025-03-15', status: 'pending' },
|
|
199
|
+
{ date: '2025-03-15', status: 'pending' },
|
|
200
|
+
]}
|
|
201
|
+
statuses={{
|
|
202
|
+
pending: { variant: 'warning', ariaLabel: 'Pending' },
|
|
203
|
+
}}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const day = screen
|
|
208
|
+
.getByText('15')
|
|
209
|
+
.closest('[data-testid="ucl-uikit-calendar__day"]') as HTMLElement;
|
|
210
|
+
|
|
211
|
+
expect(day).toHaveAttribute('aria-label', '15 March 2025, 2 Pending');
|
|
212
|
+
|
|
213
|
+
const dot = within(day).getAllByTestId('ucl-uikit-calendar__event-dot')[0];
|
|
214
|
+
expect(getComputedStyle(dot).backgroundColor).toBe(
|
|
215
|
+
hexToRgb(theme.colour.icon.warning)
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
145
219
|
test('Year select uses minDate and maxDate years when provided', () => {
|
|
146
220
|
customRender(
|
|
147
221
|
<Calendar
|