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.
Files changed (32) hide show
  1. package/dist/components/Calendar/Calendar.d.ts +1 -1
  2. package/dist/components/Calendar/Calendar.stories.d.ts +6 -1
  3. package/dist/components/Calendar/Calendar.types.d.ts +9 -0
  4. package/dist/components/Calendar/index.d.ts +1 -1
  5. package/dist/components/Calendar/subcomponents/Day.d.ts +4 -2
  6. package/dist/components/Calendar/subcomponents/Day.stories.d.ts +1 -1
  7. package/dist/components/Calendar/subcomponents/EventDot.d.ts +3 -1
  8. package/dist/components/Calendar/subcomponents/Grid.d.ts +3 -2
  9. package/dist/components/Calendar/subcomponents/__tests__/Day.test.d.ts +1 -0
  10. package/dist/components/Calendar/subcomponents/__tests__/EventDot.test.d.ts +1 -0
  11. package/dist/components/Datepicker/Datepicker.stories.d.ts +5 -0
  12. package/dist/components/Datepicker/Datepicker.types.d.ts +3 -1
  13. package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +4 -2
  14. package/dist/components/index.d.ts +1 -1
  15. package/dist/index.js +1805 -1751
  16. package/lib/components/Calendar/Calendar.stories.tsx +60 -12
  17. package/lib/components/Calendar/Calendar.tsx +27 -20
  18. package/lib/components/Calendar/Calendar.types.ts +17 -1
  19. package/lib/components/Calendar/__tests__/Calendar.test.tsx +76 -2
  20. package/lib/components/Calendar/__tests__/__snapshots__/Calendar.test.tsx.snap +462 -231
  21. package/lib/components/Calendar/index.ts +6 -1
  22. package/lib/components/Calendar/subcomponents/Day.tsx +44 -5
  23. package/lib/components/Calendar/subcomponents/EventDot.tsx +42 -6
  24. package/lib/components/Calendar/subcomponents/Grid.tsx +4 -1
  25. package/lib/components/Calendar/subcomponents/__tests__/Day.test.tsx +139 -0
  26. package/lib/components/Calendar/subcomponents/__tests__/EventDot.test.tsx +78 -0
  27. package/lib/components/Datepicker/Datepicker.stories.tsx +55 -24
  28. package/lib/components/Datepicker/Datepicker.types.ts +7 -1
  29. package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +90 -0
  30. package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +16 -1
  31. package/lib/components/index.ts +1 -1
  32. package/package.json +1 -1
@@ -1,2 +1,7 @@
1
1
  export { default } from './Calendar';
2
- export type { CalendarProps, CalendarEvent, AcademicWeek } from './Calendar.types';
2
+ export type {
3
+ CalendarProps,
4
+ CalendarEvent,
5
+ CalendarStatuses,
6
+ AcademicWeek,
7
+ } from './Calendar.types';
@@ -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 && !isDisabled && onPick) {
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
- <div
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
- </div>
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 EventDot = ({ inverted, inCurrentMonth }: EventDotProps) => {
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 = inCurrentMonth
15
- ? inverted
16
- ? theme.colour.icon.inverse
17
- : theme.colour.icon.brand
18
- : theme.colour.icon.tertiary;
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
- // Storybook controls provide UNIX timestamps for dates, need to convert
34
- // https://storybook.js.org/docs/essentials/controls#annotation
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
- // Storybook controls provide UNIX timestamps for dates, need to convert
59
- // https://storybook.js.org/docs/essentials/controls#annotation
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
- { date: dateToISOString(new Date(currentYear, currentMonth, 1)) },
109
- { date: dateToISOString(new Date(currentYear, currentMonth, 2)) },
110
- { date: dateToISOString(new Date(currentYear, currentMonth, 2)) },
111
- { date: dateToISOString(new Date(currentYear, currentMonth, 3)) },
112
- { date: dateToISOString(new Date(currentYear, currentMonth, 3)) },
113
- { date: dateToISOString(new Date(currentYear, currentMonth, 3)) },
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
- // Storybook controls provide UNIX timestamps for dates, need to convert
121
- // https://storybook.js.org/docs/essentials/controls#annotation
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
- // Storybook controls provide UNIX timestamps for dates, need to convert
209
- // https://storybook.js.org/docs/essentials/controls#annotation
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
- // Storybook controls provide UNIX timestamps for dates, need to convert
258
- // https://storybook.js.org/docs/essentials/controls#annotation
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
- // Storybook controls provide UNIX timestamps for dates, need to convert
288
- // https://storybook.js.org/docs/essentials/controls#annotation
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 { CalendarEvent, AcademicWeek } from '../Calendar';
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;