uikit-react-public 0.14.21 → 0.17.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/README.md +4 -2
- package/dist/components/Accordion/Accordion.Heading.d.ts +4 -4
- package/dist/components/Accordion/Accordion.Panel.d.ts +2 -2
- package/dist/components/Accordion/Accordion.d.ts +1 -1
- package/dist/components/Accordion/Accordion.stories.d.ts +57 -0
- package/dist/components/Accordion/index.d.ts +2 -0
- package/dist/components/Avatar/Avatar.stories.d.ts +107 -1
- package/dist/components/Button/Button.d.ts +1 -0
- package/dist/components/Calendar/index.d.ts +1 -1
- package/dist/components/Datepicker/Datepicker.d.ts +1 -1
- package/dist/components/Datepicker/Datepicker.stories.d.ts +4 -3
- package/dist/components/Datepicker/Datepicker.types.d.ts +4 -5
- package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +4 -1
- package/dist/components/Datepicker/subcomponents/DatepickerInput.d.ts +15 -2
- package/dist/components/Datepicker/subcomponents/Panel.d.ts +1 -1
- package/dist/components/Datepicker/subcomponents/VisibleField.d.ts +6 -1
- package/dist/components/Datepicker/subcomponents/index.d.ts +0 -1
- package/dist/components/Datepicker/utils/index.d.ts +0 -1
- package/dist/components/Dialog/BaseDialog.d.ts +2 -1
- package/dist/components/Dialog/Dialog.d.ts +2 -0
- package/dist/components/Header/Header.d.ts +4 -1
- package/dist/components/Header/Header.stories.d.ts +40 -0
- package/dist/components/Main/Main.d.ts +21 -0
- package/dist/components/Main/Main.stories.d.ts +15 -0
- package/dist/components/Main/index.d.ts +2 -0
- package/dist/components/NativeDatepicker/NativeDatepicker.d.ts +3 -0
- package/dist/components/NativeDatepicker/NativeDatepicker.stories.d.ts +36 -0
- package/dist/components/NativeDatepicker/NativeDatepicker.types.d.ts +10 -0
- package/dist/components/NativeDatepicker/index.d.ts +2 -0
- package/dist/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.d.ts +1 -1
- package/dist/components/NativeDatepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts +1 -0
- package/dist/components/NativeDatepicker/utils/index.d.ts +1 -0
- package/dist/components/Select/Select.stories.d.ts +154 -2
- package/dist/components/Select/Select.types.d.ts +51 -22
- package/dist/components/Select/subcomponents/CustomOption.d.ts +1 -1
- package/dist/components/Select/subcomponents/CustomSelect.d.ts +3 -2
- package/dist/components/Select/subcomponents/FilterInput.d.ts +14 -0
- package/dist/components/Select/subcomponents/NativeSelect.d.ts +5 -1
- package/dist/components/Select/subcomponents/VisibleField.d.ts +3 -1
- package/dist/components/Select/subcomponents/index.d.ts +1 -0
- package/dist/components/WeekPicker/WeekPicker.d.ts +2 -2
- package/dist/components/WeekPicker/WeekPicker.stories.d.ts +41 -0
- package/dist/components/WeekPicker/WeekPicker.types.d.ts +16 -0
- package/dist/components/WeekPicker/index.d.ts +1 -0
- package/dist/components/WeekPicker/subcomponents/CustomDatepicker.d.ts +1 -1
- package/dist/components/index.d.ts +8 -0
- package/dist/hooks/useFocusTrap.d.ts +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4366 -3768
- package/dist/utils/__tests__/announce.test.d.ts +1 -0
- package/dist/utils/announce.d.ts +6 -0
- package/dist/utils/index.d.ts +1 -0
- package/lib/components/Accordion/Accordion.Heading.tsx +27 -8
- package/lib/components/Accordion/Accordion.Panel.tsx +11 -3
- package/lib/components/Accordion/Accordion.stories.tsx +139 -0
- package/lib/components/Accordion/Accordion.tsx +10 -8
- package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +7 -7
- package/lib/components/Accordion/index.ts +2 -0
- package/lib/components/Alert/Alert.stories.tsx +1 -1
- package/lib/components/Avatar/Avatar.mdx +117 -0
- package/lib/components/Avatar/Avatar.stories.tsx +110 -2
- package/lib/components/Blanket/Blanket.stories.tsx +1 -1
- package/lib/components/Button/Button.stories.tsx +1 -1
- package/lib/components/Button/Button.tsx +1 -0
- package/lib/components/Calendar/Calendar.stories.tsx +12 -32
- package/lib/components/Calendar/__tests__/Calendar.test.tsx +23 -15
- package/lib/components/Calendar/index.ts +1 -5
- package/lib/components/Calendar/subcomponents/AcademicWeeks.tsx +2 -1
- package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +5 -1
- package/lib/components/Calendar/subcomponents/EventDot.tsx +2 -1
- package/lib/components/Calendar/subcomponents/index.ts +1 -1
- package/lib/components/Calendar/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.ts +43 -11
- package/lib/components/Calendar/utils/normaliseMonth/normaliseMonth.test.ts +5 -5
- package/lib/components/Datepicker/Datepicker.lld.md +108 -0
- package/lib/components/Datepicker/Datepicker.stories.tsx +44 -5
- package/lib/components/Datepicker/Datepicker.tsx +14 -36
- package/lib/components/Datepicker/Datepicker.types.ts +5 -14
- package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +150 -8
- package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +10 -4
- package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +39 -5
- package/lib/components/Datepicker/subcomponents/DatepickerInput.tsx +30 -17
- package/lib/components/Datepicker/subcomponents/Panel.tsx +6 -2
- package/lib/components/Datepicker/subcomponents/VisibleField.tsx +40 -3
- package/lib/components/Datepicker/subcomponents/index.ts +0 -1
- package/lib/components/Datepicker/utils/index.ts +0 -1
- package/lib/components/Dialog/BaseDialog.tsx +11 -0
- package/lib/components/Dialog/Dialog.tsx +8 -1
- package/lib/components/Dialog/DialogBody.tsx +5 -1
- package/lib/components/Dialog/DialogHeader.tsx +2 -1
- package/lib/components/Divider/Divider.stories.tsx +1 -1
- package/lib/components/Field/ErrorText.tsx +1 -0
- package/lib/components/Field/Field.stories.tsx +1 -1
- package/lib/components/Field/__tests__/Field.test.tsx +13 -0
- package/lib/components/FileInput/FileInput.stories.tsx +1 -1
- package/lib/components/Footer/Footer.stories.tsx +1 -1
- package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +3 -3
- package/lib/components/Header/Header.mdx +52 -0
- package/lib/components/Header/Header.stories.tsx +98 -0
- package/lib/components/Header/Header.tsx +51 -6
- package/lib/components/Header/__tests__/Header.test.tsx +17 -1
- package/lib/components/Heading/Heading.stories.tsx +1 -1
- package/lib/components/Icon/Icon.stories.tsx +1 -1
- package/lib/components/IconButton/IconButton.stories.tsx +1 -1
- package/lib/components/Input/Input.stories.tsx +1 -1
- package/lib/components/Label/Label.stories.tsx +1 -1
- package/lib/components/Main/Main.stories.tsx +36 -0
- package/lib/components/Main/Main.tsx +46 -0
- package/lib/components/Main/__tests__/Main.test.tsx +80 -0
- package/lib/components/Main/__tests__/__snapshots__/Main.test.tsx.snap +33 -0
- package/lib/components/Main/index.ts +2 -0
- package/lib/components/NativeDatepicker/NativeDatepicker.stories.tsx +100 -0
- package/lib/components/{Datepicker/subcomponents → NativeDatepicker}/NativeDatepicker.tsx +14 -15
- package/lib/components/NativeDatepicker/NativeDatepicker.types.ts +19 -0
- package/lib/components/NativeDatepicker/index.ts +2 -0
- package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.ts +1 -1
- package/lib/components/NativeDatepicker/utils/index.ts +1 -0
- package/lib/components/Pagination/PaginationControls.tsx +55 -12
- package/lib/components/Pagination/PaginationInfo.tsx +5 -1
- package/lib/components/Paragraph/Paragraph.stories.tsx +1 -1
- package/lib/components/Search/Search.stories.tsx +1 -1
- package/lib/components/Search/Search.tsx +4 -1
- package/lib/components/Search/__tests__/Search.test.tsx +19 -1
- package/lib/components/Select/Select.mdx +169 -0
- package/lib/components/Select/Select.stories.tsx +191 -43
- package/lib/components/Select/Select.tsx +36 -12
- package/lib/components/Select/Select.types.ts +66 -48
- package/lib/components/Select/__tests__/Select.test.tsx +448 -7
- package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
- package/lib/components/Select/subcomponents/CustomOption.tsx +2 -1
- package/lib/components/Select/subcomponents/CustomSelect.tsx +303 -33
- package/lib/components/Select/subcomponents/FilterInput.tsx +80 -0
- package/lib/components/Select/subcomponents/NativeSelect.tsx +13 -1
- package/lib/components/Select/subcomponents/VisibleField.tsx +11 -3
- package/lib/components/Select/subcomponents/index.tsx +1 -0
- package/lib/components/Snackbar/Snackbar.stories.tsx +1 -1
- package/lib/components/Spinner/Spinner.stories.tsx +1 -1
- package/lib/components/Textarea/Textarea.stories.tsx +1 -1
- package/lib/components/Timepicker/Timepicker.tsx +4 -0
- package/lib/components/Timepicker/__tests__/__snapshots__/Timepicker.test.tsx.snap +2 -2
- package/lib/components/Toggle/Toggle.stories.tsx +1 -1
- package/lib/components/Tooltip/Tooltip.stories.tsx +1 -1
- package/lib/components/WeekPicker/WeekPicker.stories.tsx +147 -0
- package/lib/components/WeekPicker/WeekPicker.tsx +2 -2
- package/lib/components/WeekPicker/WeekPicker.types.ts +21 -0
- package/lib/components/WeekPicker/index.ts +1 -0
- package/lib/components/WeekPicker/subcomponents/CustomDatepicker.tsx +1 -1
- package/lib/components/common/Common.mdx +1 -1
- package/lib/components/index.ts +11 -2
- package/lib/hooks/useFocusTrap.ts +40 -4
- package/lib/index.ts +1 -0
- package/lib/utils/__tests__/announce.test.ts +121 -0
- package/lib/utils/announce.ts +134 -0
- package/lib/utils/index.ts +1 -0
- package/package.json +3 -6
- package/dist/components/Datepicker/subcomponents/NativeDatepicker.d.ts +0 -6
- package/lib/components/Accordion/Accordion.stories.tsx.NOT_READY +0 -93
- /package/dist/components/{Datepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts → Main/__tests__/Main.test.d.ts} +0 -0
- /package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.test.ts +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useArgs } from '@storybook/preview-api';
|
|
3
|
+
import WeekPicker from './WeekPicker';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/Work in progress/WeekPicker',
|
|
7
|
+
component: WeekPicker,
|
|
8
|
+
parameters: { layout: 'padded' },
|
|
9
|
+
argTypes: {
|
|
10
|
+
value: { control: { type: 'date' } },
|
|
11
|
+
minDate: { control: { type: 'date' } },
|
|
12
|
+
maxDate: { control: { type: 'date' } },
|
|
13
|
+
disabled: { control: { type: 'boolean' } },
|
|
14
|
+
showAcademicWeeks: { control: { type: 'boolean' } },
|
|
15
|
+
},
|
|
16
|
+
tags: ['autodocs'],
|
|
17
|
+
} satisfies Meta<typeof WeekPicker>;
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj<typeof meta>;
|
|
21
|
+
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
render: () => {
|
|
24
|
+
const [args, updateArgs] = useArgs();
|
|
25
|
+
|
|
26
|
+
// Storybook controls provide UNIX timestamps for dates, need to convert
|
|
27
|
+
// https://storybook.js.org/docs/essentials/controls#annotation
|
|
28
|
+
args.value = args.value ? new Date(args.value) : null;
|
|
29
|
+
args.minDate = args.minDate
|
|
30
|
+
? new Date(args.minDate).toLocaleDateString('sv-SE')
|
|
31
|
+
: null;
|
|
32
|
+
args.maxDate = args.maxDate
|
|
33
|
+
? new Date(args.maxDate).toLocaleDateString('sv-SE')
|
|
34
|
+
: null;
|
|
35
|
+
|
|
36
|
+
const onValueChange = (value: Date | null) =>
|
|
37
|
+
updateArgs({ value: value });
|
|
38
|
+
return (
|
|
39
|
+
<WeekPicker
|
|
40
|
+
{...args}
|
|
41
|
+
onValueChange={onValueChange}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const academicWeeks = [
|
|
48
|
+
{ start: '2025-08-25', number: 1 },
|
|
49
|
+
{ start: '2025-09-01', number: 2 },
|
|
50
|
+
{ start: '2025-09-08', number: 3 },
|
|
51
|
+
{ start: '2025-09-15', number: 4 },
|
|
52
|
+
{ start: '2025-09-22', number: 5 },
|
|
53
|
+
{ start: '2025-09-29', number: 6 },
|
|
54
|
+
{ start: '2025-10-06', number: 7 },
|
|
55
|
+
{ start: '2025-10-13', number: 8 },
|
|
56
|
+
{ start: '2025-10-20', number: 9 },
|
|
57
|
+
{ start: '2025-10-27', number: 10 },
|
|
58
|
+
{ start: '2025-11-03', number: 11 },
|
|
59
|
+
{ start: '2025-11-10', number: 12 },
|
|
60
|
+
{ start: '2025-11-17', number: 13 },
|
|
61
|
+
{ start: '2025-11-24', number: 14 },
|
|
62
|
+
{ start: '2025-12-01', number: 15 },
|
|
63
|
+
{ start: '2025-12-08', number: 16 },
|
|
64
|
+
{ start: '2025-12-15', number: 17 },
|
|
65
|
+
{ start: '2025-12-22', number: 18 },
|
|
66
|
+
{ start: '2025-12-29', number: 19 },
|
|
67
|
+
{ start: '2026-01-05', number: 20 },
|
|
68
|
+
{ start: '2026-01-12', number: 21 },
|
|
69
|
+
{ start: '2026-01-19', number: 22 },
|
|
70
|
+
{ start: '2026-01-26', number: 23 },
|
|
71
|
+
{ start: '2026-02-02', number: 24 },
|
|
72
|
+
{ start: '2026-02-09', number: 25 },
|
|
73
|
+
{ start: '2026-02-16', number: 26 },
|
|
74
|
+
{ start: '2026-02-23', number: 27 },
|
|
75
|
+
{ start: '2026-03-02', number: 28 },
|
|
76
|
+
{ start: '2026-03-09', number: 29 },
|
|
77
|
+
{ start: '2026-03-16', number: 30 },
|
|
78
|
+
{ start: '2026-03-23', number: 31 },
|
|
79
|
+
{ start: '2026-03-30', number: 32 },
|
|
80
|
+
{ start: '2026-04-06', number: 33 },
|
|
81
|
+
{ start: '2026-04-13', number: 34 },
|
|
82
|
+
{ start: '2026-04-20', number: 35 },
|
|
83
|
+
{ start: '2026-04-27', number: 36 },
|
|
84
|
+
{ start: '2026-05-04', number: 37 },
|
|
85
|
+
{ start: '2026-05-11', number: 38 },
|
|
86
|
+
{ start: '2026-05-18', number: 39 },
|
|
87
|
+
{ start: '2026-05-25', number: 40 },
|
|
88
|
+
{ start: '2026-06-01', number: 41 },
|
|
89
|
+
{ start: '2026-06-08', number: 42 },
|
|
90
|
+
{ start: '2026-06-15', number: 43 },
|
|
91
|
+
{ start: '2026-06-22', number: 44 },
|
|
92
|
+
{ start: '2026-06-29', number: 45 },
|
|
93
|
+
{ start: '2026-07-06', number: 46 },
|
|
94
|
+
{ start: '2026-07-13', number: 47 },
|
|
95
|
+
{ start: '2026-07-20', number: 48 },
|
|
96
|
+
{ start: '2026-07-27', number: 49 },
|
|
97
|
+
{ start: '2026-08-03', number: 50 },
|
|
98
|
+
{ start: '2026-08-10', number: 51 },
|
|
99
|
+
{ start: '2026-08-17', number: 52 },
|
|
100
|
+
{ start: '2026-08-24', number: 53 },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
export const WithAcademicWeeks: Story = {
|
|
104
|
+
name: 'With academic weeks',
|
|
105
|
+
args: {
|
|
106
|
+
showAcademicWeeks: true,
|
|
107
|
+
academicWeeks: academicWeeks,
|
|
108
|
+
value: new Date(academicWeeks[0].start), // So the week numbers appear on story mount
|
|
109
|
+
},
|
|
110
|
+
render: () => {
|
|
111
|
+
const [args, updateArgs] = useArgs();
|
|
112
|
+
|
|
113
|
+
// Storybook controls provide UNIX timestamps for dates, need to convert
|
|
114
|
+
// https://storybook.js.org/docs/essentials/controls#annotation
|
|
115
|
+
args.value = args.value ? new Date(args.value) : null;
|
|
116
|
+
args.minDate = args.minDate
|
|
117
|
+
? new Date(args.minDate).toLocaleDateString('sv-SE')
|
|
118
|
+
: null;
|
|
119
|
+
args.maxDate = args.maxDate
|
|
120
|
+
? new Date(args.maxDate).toLocaleDateString('sv-SE')
|
|
121
|
+
: null;
|
|
122
|
+
|
|
123
|
+
const onValueChange = (value: Date | null) =>
|
|
124
|
+
updateArgs({ value: value });
|
|
125
|
+
return (
|
|
126
|
+
<WeekPicker
|
|
127
|
+
{...args}
|
|
128
|
+
onValueChange={onValueChange}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const Disabled: Story = {
|
|
135
|
+
name: 'Disabled',
|
|
136
|
+
args: {
|
|
137
|
+
disabled: true,
|
|
138
|
+
value: new Date(),
|
|
139
|
+
},
|
|
140
|
+
render: () => {
|
|
141
|
+
const [args] = useArgs();
|
|
142
|
+
|
|
143
|
+
args.value = args.value ? new Date(args.value) : null;
|
|
144
|
+
|
|
145
|
+
return <WeekPicker {...args} />;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CustomDatepicker } from './subcomponents';
|
|
2
|
-
import type {
|
|
2
|
+
import type { WeekPickerProps } from './WeekPicker.types';
|
|
3
3
|
|
|
4
4
|
const WeekPicker = ({
|
|
5
5
|
value,
|
|
@@ -9,7 +9,7 @@ const WeekPicker = ({
|
|
|
9
9
|
disabled,
|
|
10
10
|
className,
|
|
11
11
|
...props
|
|
12
|
-
}:
|
|
12
|
+
}: WeekPickerProps) => {
|
|
13
13
|
return (
|
|
14
14
|
<CustomDatepicker
|
|
15
15
|
value={value}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HTMLAttributes, RefObject } from 'react';
|
|
2
|
+
import type { CalendarEvent, AcademicWeek } from '../Calendar';
|
|
3
|
+
|
|
4
|
+
export type DatepickerValue = Date | null;
|
|
5
|
+
|
|
6
|
+
export interface WeekPickerProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
value?: DatepickerValue;
|
|
8
|
+
onValueChange?: (
|
|
9
|
+
value: DatepickerValue,
|
|
10
|
+
event?: React.SyntheticEvent
|
|
11
|
+
) => void;
|
|
12
|
+
minDate?: string | null;
|
|
13
|
+
maxDate?: string | null;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
events?: CalendarEvent[];
|
|
16
|
+
showAcademicWeeks?: boolean;
|
|
17
|
+
academicWeeks?: AcademicWeek[];
|
|
18
|
+
testId?: string;
|
|
19
|
+
ref?: RefObject<HTMLDivElement>;
|
|
20
|
+
inputRef?: RefObject<HTMLInputElement>;
|
|
21
|
+
}
|
|
@@ -4,7 +4,7 @@ import { VisibleField } from './';
|
|
|
4
4
|
import { Panel } from '../../Datepicker/subcomponents';
|
|
5
5
|
import { Calendar, Icon, IconButton } from '../..';
|
|
6
6
|
import { parseInputValue } from '../../Datepicker/utils';
|
|
7
|
-
import type { DatepickerValue } from '
|
|
7
|
+
import type { DatepickerValue } from '../WeekPicker.types';
|
|
8
8
|
import type { CalendarEvent, AcademicWeek } from '../../Calendar';
|
|
9
9
|
|
|
10
10
|
interface CustomDatepickerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -76,7 +76,7 @@ The available margin props are:
|
|
|
76
76
|
include={['m', 'm', 'mv', 'mh', 'mt', 'mb', 'ml', 'mr', 'noMargins']}
|
|
77
77
|
/>
|
|
78
78
|
|
|
79
|
-
[A live example of common margin props can be found in this `<Button>` story.](?path=/story/components-
|
|
79
|
+
[A live example of common margin props can be found in this `<Button>` story.](?path=/story/components-button--common-margins)
|
|
80
80
|
|
|
81
81
|
Implementation details can be found in <a href='https://github.com/ucl-isd/uikit-react/blob/main/lib/components/common/marginsStyle.ts' target='_blank'>uikit-react/components/common/MarginsStyle.tsx</a>
|
|
82
82
|
|
package/lib/components/index.ts
CHANGED
|
@@ -103,8 +103,8 @@ export type { TabsProps } from './Tabs';
|
|
|
103
103
|
export type { TabProps } from './Tabs/Tab';
|
|
104
104
|
|
|
105
105
|
// todo:
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
export { default as Accordion } from './Accordion';
|
|
107
|
+
export type { AccordionProps } from './Accordion';
|
|
108
108
|
|
|
109
109
|
export { default as Field } from './Field';
|
|
110
110
|
export type { FieldProps } from './Field';
|
|
@@ -134,6 +134,9 @@ export type { RadioProps, LabelledRadioProps } from './Radio';
|
|
|
134
134
|
export { default as Datepicker } from './Datepicker';
|
|
135
135
|
export type { DatepickerProps } from './Datepicker';
|
|
136
136
|
|
|
137
|
+
export { default as NativeDatepicker } from './NativeDatepicker';
|
|
138
|
+
export type { NativeDatepickerProps } from './NativeDatepicker';
|
|
139
|
+
|
|
137
140
|
export { default as Calendar } from './Calendar';
|
|
138
141
|
export type { CalendarProps } from './Calendar';
|
|
139
142
|
|
|
@@ -150,3 +153,9 @@ export type { CookieNoticeProps } from './CookieNotice';
|
|
|
150
153
|
|
|
151
154
|
export { default as Search } from './Search';
|
|
152
155
|
export type { SearchProps } from './Search';
|
|
156
|
+
|
|
157
|
+
export { default as Layout } from './Layout';
|
|
158
|
+
export type { LayoutProps } from './Layout';
|
|
159
|
+
|
|
160
|
+
export { default as Main } from './Main';
|
|
161
|
+
export type { MainProps } from './Main';
|
|
@@ -22,6 +22,7 @@ interface UseFocusTrapOptions {
|
|
|
22
22
|
initialFocusRef?: React.RefObject<HTMLElement>;
|
|
23
23
|
finalFocusRef?: React.RefObject<HTMLElement>;
|
|
24
24
|
restoreFocus?: boolean;
|
|
25
|
+
skipFirstFocusable?: boolean;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export const useFocusTrap = ({
|
|
@@ -30,6 +31,7 @@ export const useFocusTrap = ({
|
|
|
30
31
|
initialFocusRef,
|
|
31
32
|
finalFocusRef,
|
|
32
33
|
restoreFocus = true,
|
|
34
|
+
skipFirstFocusable = false,
|
|
33
35
|
}: UseFocusTrapOptions) => {
|
|
34
36
|
const previousActiveElement = useRef<HTMLElement | null>(null);
|
|
35
37
|
|
|
@@ -87,10 +89,26 @@ export const useFocusTrap = ({
|
|
|
87
89
|
previousActiveElement.current = document.activeElement as HTMLElement;
|
|
88
90
|
|
|
89
91
|
// Focus the specified initial element or the first focusable element
|
|
90
|
-
const
|
|
92
|
+
const focusableElements = getFocusableElements();
|
|
93
|
+
const defaultIndex =
|
|
94
|
+
skipFirstFocusable && focusableElements.length > 1 ? 1 : 0;
|
|
95
|
+
const focusElement =
|
|
96
|
+
initialFocusRef?.current || focusableElements[defaultIndex];
|
|
97
|
+
|
|
98
|
+
let requestAnimationFrameId: number | null = null;
|
|
99
|
+
let setTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
100
|
+
|
|
91
101
|
if (focusElement) {
|
|
92
|
-
// Use
|
|
93
|
-
|
|
102
|
+
// Use requestAnimationFrame if available, otherwise setTimeout
|
|
103
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
104
|
+
requestAnimationFrameId = requestAnimationFrame(() => {
|
|
105
|
+
focusElement.focus();
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
setTimeoutId = setTimeout(() => {
|
|
109
|
+
focusElement.focus();
|
|
110
|
+
}, 0);
|
|
111
|
+
}
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
// Add event listener for keyboard navigation
|
|
@@ -98,8 +116,26 @@ export const useFocusTrap = ({
|
|
|
98
116
|
|
|
99
117
|
return () => {
|
|
100
118
|
document.removeEventListener('keydown', handleKeyDown);
|
|
119
|
+
if (
|
|
120
|
+
typeof requestAnimationFrame === 'function' &&
|
|
121
|
+
requestAnimationFrameId !== null
|
|
122
|
+
) {
|
|
123
|
+
cancelAnimationFrame(requestAnimationFrameId);
|
|
124
|
+
}
|
|
125
|
+
if (
|
|
126
|
+
typeof requestAnimationFrame !== 'function' &&
|
|
127
|
+
setTimeoutId !== undefined
|
|
128
|
+
) {
|
|
129
|
+
clearTimeout(setTimeoutId);
|
|
130
|
+
}
|
|
101
131
|
};
|
|
102
|
-
}, [
|
|
132
|
+
}, [
|
|
133
|
+
isActive,
|
|
134
|
+
initialFocusRef,
|
|
135
|
+
getFocusableElements,
|
|
136
|
+
handleKeyDown,
|
|
137
|
+
skipFirstFocusable,
|
|
138
|
+
]);
|
|
103
139
|
|
|
104
140
|
// Restore focus when trap becomes inactive
|
|
105
141
|
useEffect(() => {
|
package/lib/index.ts
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import announce, { __resetForTesting } from '../announce';
|
|
4
|
+
|
|
5
|
+
describe('announce (ARIA live region)', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// Reset DOM before each test
|
|
8
|
+
document.body.innerHTML = '';
|
|
9
|
+
__resetForTesting();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.useRealTimers();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('creates a live region on first announce', async () => {
|
|
17
|
+
announce('Hello world');
|
|
18
|
+
|
|
19
|
+
const region = await screen.findByTestId('aria-live-region');
|
|
20
|
+
|
|
21
|
+
expect(region).toBeTruthy();
|
|
22
|
+
expect(region.getAttribute('aria-live')).toBe('polite');
|
|
23
|
+
expect(region.getAttribute('aria-atomic')).toBe('true');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('visually hides the live region but keeps it accessible', async () => {
|
|
27
|
+
announce('Hidden message');
|
|
28
|
+
|
|
29
|
+
const region = await screen.findByTestId('aria-live-region');
|
|
30
|
+
|
|
31
|
+
expect(region.style.position).toBe('absolute');
|
|
32
|
+
expect(region.style.left).toBe('-9999px');
|
|
33
|
+
expect(region.style.width).toBe('1px');
|
|
34
|
+
expect(region.style.height).toBe('1px');
|
|
35
|
+
expect(region.style.overflow).toBe('hidden');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('clears text and sets message after timeout', async () => {
|
|
39
|
+
announce('This is an announcement');
|
|
40
|
+
|
|
41
|
+
const region = await screen.findByTestId('aria-live-region');
|
|
42
|
+
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(region.textContent).toBe('This is an announcement');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('reuses the same live region on multiple announcements', async () => {
|
|
49
|
+
announce('First message');
|
|
50
|
+
|
|
51
|
+
const region = await screen.findByTestId('aria-live-region');
|
|
52
|
+
|
|
53
|
+
expect(region).toBeTruthy();
|
|
54
|
+
|
|
55
|
+
announce('Second message');
|
|
56
|
+
|
|
57
|
+
// Query all instances
|
|
58
|
+
const allRegions = await screen.findAllByTestId('aria-live-region');
|
|
59
|
+
|
|
60
|
+
expect(allRegions.length).toBe(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('queues multiple announcements and processes sequentially', async () => {
|
|
64
|
+
vi.useFakeTimers();
|
|
65
|
+
|
|
66
|
+
announce('First message');
|
|
67
|
+
announce('Second message');
|
|
68
|
+
|
|
69
|
+
// Query the live region
|
|
70
|
+
const region = document.querySelector(
|
|
71
|
+
'[data-testid="aria-live-region"]'
|
|
72
|
+
) as HTMLElement;
|
|
73
|
+
|
|
74
|
+
// Initially empty
|
|
75
|
+
expect(region.textContent).toBe('');
|
|
76
|
+
|
|
77
|
+
// After 100ms, first message appears
|
|
78
|
+
vi.advanceTimersByTime(100);
|
|
79
|
+
expect(region.textContent).toBe('First message');
|
|
80
|
+
|
|
81
|
+
// Second message should NOT appear yet (need to wait 1000ms + 100ms more)
|
|
82
|
+
vi.advanceTimersByTime(500);
|
|
83
|
+
expect(region.textContent).toBe('First message');
|
|
84
|
+
|
|
85
|
+
// After remaining 500ms of the 1000ms delay, queue processes but text is cleared
|
|
86
|
+
vi.advanceTimersByTime(500);
|
|
87
|
+
expect(region.textContent).toBe('');
|
|
88
|
+
|
|
89
|
+
// After another 100ms, second message appears
|
|
90
|
+
vi.advanceTimersByTime(100);
|
|
91
|
+
expect(region.textContent).toBe('Second message');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('duplicate prevention while currently announcing', async () => {
|
|
95
|
+
vi.useFakeTimers();
|
|
96
|
+
|
|
97
|
+
announce('Processing');
|
|
98
|
+
announce('Processing'); // Duplicate - should be ignored
|
|
99
|
+
announce('Processing'); // Duplicate - should be ignored
|
|
100
|
+
announce('Done'); // Different message - should be queued
|
|
101
|
+
|
|
102
|
+
const region = document.querySelector(
|
|
103
|
+
'[data-testid="aria-live-region"]'
|
|
104
|
+
) as HTMLElement;
|
|
105
|
+
|
|
106
|
+
// First message appears
|
|
107
|
+
vi.advanceTimersByTime(100);
|
|
108
|
+
expect(region.textContent).toBe('Processing');
|
|
109
|
+
|
|
110
|
+
// Complete first message, next should be "Done" (duplicates were ignored)
|
|
111
|
+
vi.advanceTimersByTime(1000);
|
|
112
|
+
vi.advanceTimersByTime(100);
|
|
113
|
+
expect(region.textContent).toBe('Done');
|
|
114
|
+
|
|
115
|
+
// No more messages in queue
|
|
116
|
+
vi.advanceTimersByTime(1000);
|
|
117
|
+
vi.advanceTimersByTime(100);
|
|
118
|
+
// Still "Done" - nothing else was queued
|
|
119
|
+
expect(region.textContent).toBe('Done');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sends a message to screen readers using an ARIA live region.
|
|
3
|
+
*
|
|
4
|
+
* Automatically initializes a hidden live region the first time it is used.
|
|
5
|
+
* This allows screen readers (NVDA, JAWS, VoiceOver) to announce dynamic
|
|
6
|
+
* updates such as "Item removed", "Saved", etc.
|
|
7
|
+
*
|
|
8
|
+
* The live region is visually hidden but remains accessible.
|
|
9
|
+
*
|
|
10
|
+
* @param message - The message to announce to screen readers.
|
|
11
|
+
*
|
|
12
|
+
* @param force - If true, forces the announcement even if it's the same as the last one.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* announce("Item removed");
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* announce("Form submitted successfully");
|
|
19
|
+
*/
|
|
20
|
+
let liveRegion: HTMLDivElement | null = null;
|
|
21
|
+
let announcementQueue: string[] = [];
|
|
22
|
+
let isAnnouncing = false;
|
|
23
|
+
let lastAnnouncement = '';
|
|
24
|
+
|
|
25
|
+
const SR_DETECTION_DELAY = 100; // Time for SR to detect change
|
|
26
|
+
const SR_PROCESSING_DELAY = 1000; // Time for SR to finish announcement
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates the live region if it does not already exist.
|
|
30
|
+
*/
|
|
31
|
+
const initLiveRegion = (): void => {
|
|
32
|
+
if (typeof document === 'undefined' || liveRegion) return;
|
|
33
|
+
|
|
34
|
+
// Guard: body may not exist yet (e.g., script in <head>)
|
|
35
|
+
const container = document.body || document.documentElement;
|
|
36
|
+
if (!container) return;
|
|
37
|
+
|
|
38
|
+
const region = document.createElement('div');
|
|
39
|
+
region.setAttribute('aria-live', 'polite');
|
|
40
|
+
region.setAttribute('aria-atomic', 'true');
|
|
41
|
+
region.setAttribute('role', 'status');
|
|
42
|
+
region.setAttribute('data-testid', 'aria-live-region');
|
|
43
|
+
|
|
44
|
+
// Visually hide but keep accessible
|
|
45
|
+
region.style.position = 'absolute';
|
|
46
|
+
region.style.left = '-9999px';
|
|
47
|
+
region.style.width = '1px';
|
|
48
|
+
region.style.height = '1px';
|
|
49
|
+
region.style.overflow = 'hidden';
|
|
50
|
+
|
|
51
|
+
document.body.appendChild(region);
|
|
52
|
+
liveRegion = region;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Processes the announcement queue sequentially.
|
|
57
|
+
*/
|
|
58
|
+
const processQueue = (): void => {
|
|
59
|
+
if (isAnnouncing || announcementQueue.length === 0) return;
|
|
60
|
+
|
|
61
|
+
if (!liveRegion) {
|
|
62
|
+
// If no live region, do not drop the message or update lastAnnouncement
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Only shift and update lastAnnouncement if liveRegion exists
|
|
67
|
+
const message = announcementQueue.shift()!;
|
|
68
|
+
lastAnnouncement = message;
|
|
69
|
+
isAnnouncing = true;
|
|
70
|
+
// Clear previous message to force announcement
|
|
71
|
+
liveRegion.textContent = '';
|
|
72
|
+
|
|
73
|
+
// brief delay ensures SRs detect the change
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
if (liveRegion) {
|
|
76
|
+
liveRegion.textContent = message;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Wait for screen reader to process, then announce next
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
isAnnouncing = false;
|
|
82
|
+
processQueue();
|
|
83
|
+
}, SR_PROCESSING_DELAY);
|
|
84
|
+
}, SR_DETECTION_DELAY);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Announces a message to screen readers.
|
|
89
|
+
*/
|
|
90
|
+
const announce = (message: string, force = false): void => {
|
|
91
|
+
// SSR guard: do nothing if document is unavailable
|
|
92
|
+
if (typeof document === 'undefined') return;
|
|
93
|
+
|
|
94
|
+
// Initialize live region on first use
|
|
95
|
+
if (!liveRegion) initLiveRegion();
|
|
96
|
+
|
|
97
|
+
// If forcing, clear the queue to prevent old messages
|
|
98
|
+
if (force) {
|
|
99
|
+
announcementQueue.length = 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Skip if the same message was just announced or is already queued
|
|
103
|
+
if (
|
|
104
|
+
!force &&
|
|
105
|
+
message === lastAnnouncement &&
|
|
106
|
+
announcementQueue.length === 0
|
|
107
|
+
) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Prevent adding duplicate messages to the queue (including if currently announcing)
|
|
112
|
+
if (
|
|
113
|
+
announcementQueue.includes(message) ||
|
|
114
|
+
(isAnnouncing && lastAnnouncement === message)
|
|
115
|
+
) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
announcementQueue.push(message);
|
|
120
|
+
processQueue();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Export for testing purposes
|
|
124
|
+
export const __resetForTesting = (): void => {
|
|
125
|
+
if (liveRegion && liveRegion.parentNode) {
|
|
126
|
+
liveRegion.parentNode.removeChild(liveRegion);
|
|
127
|
+
}
|
|
128
|
+
liveRegion = null;
|
|
129
|
+
announcementQueue = [];
|
|
130
|
+
isAnnouncing = false;
|
|
131
|
+
lastAnnouncement = '';
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export default announce;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as announce } from './announce';
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "uikit-react-public",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.17.4",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
"dist",
|
|
11
11
|
"lib"
|
|
12
12
|
],
|
|
13
|
-
"publishConfig": {
|
|
14
|
-
"registry": "https://registry.npmjs.org/"
|
|
15
|
-
},
|
|
16
13
|
"scripts": {
|
|
17
14
|
"dev": "vite",
|
|
18
15
|
"dev_expose": "vite --host",
|
|
@@ -45,8 +42,8 @@
|
|
|
45
42
|
"@floating-ui/react-dom": "^2.1.2"
|
|
46
43
|
},
|
|
47
44
|
"peerDependencies": {
|
|
48
|
-
"react": "^
|
|
49
|
-
"react-dom": "^
|
|
45
|
+
"react": "^19.0.0",
|
|
46
|
+
"react-dom": "^19.0.0"
|
|
50
47
|
},
|
|
51
48
|
"devDependencies": {
|
|
52
49
|
"@azure/msal-browser": "^4.7.0",
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
interface NativeDatepickerProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
2
|
-
testId?: string;
|
|
3
|
-
ref?: React.RefObject<HTMLInputElement>;
|
|
4
|
-
}
|
|
5
|
-
declare const NativeDatepicker: ({ value, onChange, min, max, className, disabled, testId, ref, ...props }: NativeDatepickerProps) => import("react/jsx-runtime").JSX.Element;
|
|
6
|
-
export default NativeDatepicker;
|