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
|
@@ -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
|
+
});
|
|
@@ -12,6 +12,7 @@ const meta = {
|
|
|
12
12
|
value: { control: { type: 'date' } },
|
|
13
13
|
minDate: { control: { type: 'date' } },
|
|
14
14
|
maxDate: { control: { type: 'date' } },
|
|
15
|
+
pickedMonth: { control: { type: 'date' } },
|
|
15
16
|
disabled: { control: { type: 'boolean' } },
|
|
16
17
|
clearable: { control: { type: 'boolean' } },
|
|
17
18
|
showAcademicWeeks: { control: { type: 'boolean' } },
|
|
@@ -22,6 +23,13 @@ const meta = {
|
|
|
22
23
|
export default meta;
|
|
23
24
|
type Story = StoryObj<typeof meta>;
|
|
24
25
|
|
|
26
|
+
// Convert UNIX timestamp from Storybook controls to `Date` object
|
|
27
|
+
// https://storybook.js.org/docs/essentials/controls#annotation
|
|
28
|
+
const parseDateFromUNIXTimestamp = (timestamp: number) => {
|
|
29
|
+
const date = new Date(timestamp);
|
|
30
|
+
return isNaN(date.getTime()) ? null : date;
|
|
31
|
+
};
|
|
32
|
+
|
|
25
33
|
const dateToISOString = (date: Date) => {
|
|
26
34
|
return date.toISOString().split('T')[0];
|
|
27
35
|
};
|
|
@@ -29,9 +37,9 @@ const dateToISOString = (date: Date) => {
|
|
|
29
37
|
export const Default: Story = {
|
|
30
38
|
render: () => {
|
|
31
39
|
const [args, updateArgs] = useArgs();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
args.pickedMonth = args.pickedMonth
|
|
41
|
+
? parseDateFromUNIXTimestamp(args.pickedMonth)
|
|
42
|
+
: null;
|
|
35
43
|
args.value = args.value ? new Date(args.value) : null;
|
|
36
44
|
args.minDate = args.minDate
|
|
37
45
|
? new Date(args.minDate).toLocaleDateString('sv-SE')
|
|
@@ -54,9 +62,9 @@ export const Default: Story = {
|
|
|
54
62
|
export const InAField: Story = {
|
|
55
63
|
render: () => {
|
|
56
64
|
const [args, updateArgs] = useArgs();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
args.pickedMonth = args.pickedMonth
|
|
66
|
+
? parseDateFromUNIXTimestamp(args.pickedMonth)
|
|
67
|
+
: null;
|
|
60
68
|
args.value = args.value ? new Date(args.value) : null;
|
|
61
69
|
args.minDate = args.minDate
|
|
62
70
|
? new Date(args.minDate).toLocaleDateString('sv-SE')
|
|
@@ -105,20 +113,43 @@ export const WithEvents: Story = {
|
|
|
105
113
|
{ date: dateToISOString(new Date(currentYear, currentMonth, 0)) },
|
|
106
114
|
{ date: dateToISOString(new Date(currentYear, currentMonth, 0)) },
|
|
107
115
|
// Blue event dots
|
|
108
|
-
{
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
{
|
|
113
|
-
|
|
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
|
+
},
|
|
114
140
|
];
|
|
115
141
|
})(),
|
|
142
|
+
statuses: {
|
|
143
|
+
pending: { variant: 'warning', ariaLabel: 'Pending' },
|
|
144
|
+
approved: { variant: 'success', ariaLabel: 'Approved' },
|
|
145
|
+
rejected: { variant: 'critical', ariaLabel: 'Rejected' },
|
|
146
|
+
},
|
|
116
147
|
},
|
|
117
148
|
render: () => {
|
|
118
149
|
const [args, updateArgs] = useArgs();
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
150
|
+
args.pickedMonth = args.pickedMonth
|
|
151
|
+
? parseDateFromUNIXTimestamp(args.pickedMonth)
|
|
152
|
+
: null;
|
|
122
153
|
args.value = args.value ? new Date(args.value) : null;
|
|
123
154
|
args.minDate = args.minDate
|
|
124
155
|
? new Date(args.minDate).toLocaleDateString('sv-SE')
|
|
@@ -204,9 +235,9 @@ export const WithAcademicWeeks: Story = {
|
|
|
204
235
|
},
|
|
205
236
|
render: () => {
|
|
206
237
|
const [args, updateArgs] = useArgs();
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
238
|
+
args.pickedMonth = args.pickedMonth
|
|
239
|
+
? parseDateFromUNIXTimestamp(args.pickedMonth)
|
|
240
|
+
: null;
|
|
210
241
|
args.value = args.value ? new Date(args.value) : null;
|
|
211
242
|
args.minDate = args.minDate
|
|
212
243
|
? new Date(args.minDate).toLocaleDateString('sv-SE')
|
|
@@ -253,9 +284,9 @@ export const MinMaxDates: Story = {
|
|
|
253
284
|
},
|
|
254
285
|
render: () => {
|
|
255
286
|
const [args, updateArgs] = useArgs();
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
287
|
+
args.pickedMonth = args.pickedMonth
|
|
288
|
+
? parseDateFromUNIXTimestamp(args.pickedMonth)
|
|
289
|
+
: null;
|
|
259
290
|
args.value = args.value ? new Date(args.value) : null;
|
|
260
291
|
args.minDate = args.minDate
|
|
261
292
|
? new Date(args.minDate).toLocaleDateString('sv-SE')
|
|
@@ -283,9 +314,9 @@ export const Clearable: Story = {
|
|
|
283
314
|
},
|
|
284
315
|
render: () => {
|
|
285
316
|
const [args, updateArgs] = useArgs();
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
args.pickedMonth = args.pickedMonth
|
|
318
|
+
? parseDateFromUNIXTimestamp(args.pickedMonth)
|
|
319
|
+
: null;
|
|
289
320
|
args.value = args.value ? new Date(args.value) : null;
|
|
290
321
|
args.minDate = args.minDate
|
|
291
322
|
? new Date(args.minDate).toLocaleDateString('sv-SE')
|
|
@@ -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;
|
|
@@ -14,7 +18,9 @@ interface BaseDatepickerProps {
|
|
|
14
18
|
maxDate?: string | null; // ISO date string: YYYY-MM-DD
|
|
15
19
|
minYear?: number;
|
|
16
20
|
maxYear?: number;
|
|
21
|
+
pickedMonth?: Date | null; // Pre-selects a month to display when no value is set
|
|
17
22
|
events?: CalendarEvent[];
|
|
23
|
+
statuses?: CalendarStatuses;
|
|
18
24
|
showAcademicWeeks?: boolean;
|
|
19
25
|
academicWeeks?: AcademicWeek[];
|
|
20
26
|
testId?: string;
|