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.
Files changed (158) hide show
  1. package/README.md +4 -2
  2. package/dist/components/Accordion/Accordion.Heading.d.ts +4 -4
  3. package/dist/components/Accordion/Accordion.Panel.d.ts +2 -2
  4. package/dist/components/Accordion/Accordion.d.ts +1 -1
  5. package/dist/components/Accordion/Accordion.stories.d.ts +57 -0
  6. package/dist/components/Accordion/index.d.ts +2 -0
  7. package/dist/components/Avatar/Avatar.stories.d.ts +107 -1
  8. package/dist/components/Button/Button.d.ts +1 -0
  9. package/dist/components/Calendar/index.d.ts +1 -1
  10. package/dist/components/Datepicker/Datepicker.d.ts +1 -1
  11. package/dist/components/Datepicker/Datepicker.stories.d.ts +4 -3
  12. package/dist/components/Datepicker/Datepicker.types.d.ts +4 -5
  13. package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +4 -1
  14. package/dist/components/Datepicker/subcomponents/DatepickerInput.d.ts +15 -2
  15. package/dist/components/Datepicker/subcomponents/Panel.d.ts +1 -1
  16. package/dist/components/Datepicker/subcomponents/VisibleField.d.ts +6 -1
  17. package/dist/components/Datepicker/subcomponents/index.d.ts +0 -1
  18. package/dist/components/Datepicker/utils/index.d.ts +0 -1
  19. package/dist/components/Dialog/BaseDialog.d.ts +2 -1
  20. package/dist/components/Dialog/Dialog.d.ts +2 -0
  21. package/dist/components/Header/Header.d.ts +4 -1
  22. package/dist/components/Header/Header.stories.d.ts +40 -0
  23. package/dist/components/Main/Main.d.ts +21 -0
  24. package/dist/components/Main/Main.stories.d.ts +15 -0
  25. package/dist/components/Main/index.d.ts +2 -0
  26. package/dist/components/NativeDatepicker/NativeDatepicker.d.ts +3 -0
  27. package/dist/components/NativeDatepicker/NativeDatepicker.stories.d.ts +36 -0
  28. package/dist/components/NativeDatepicker/NativeDatepicker.types.d.ts +10 -0
  29. package/dist/components/NativeDatepicker/index.d.ts +2 -0
  30. package/dist/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.d.ts +1 -1
  31. package/dist/components/NativeDatepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts +1 -0
  32. package/dist/components/NativeDatepicker/utils/index.d.ts +1 -0
  33. package/dist/components/Select/Select.stories.d.ts +154 -2
  34. package/dist/components/Select/Select.types.d.ts +51 -22
  35. package/dist/components/Select/subcomponents/CustomOption.d.ts +1 -1
  36. package/dist/components/Select/subcomponents/CustomSelect.d.ts +3 -2
  37. package/dist/components/Select/subcomponents/FilterInput.d.ts +14 -0
  38. package/dist/components/Select/subcomponents/NativeSelect.d.ts +5 -1
  39. package/dist/components/Select/subcomponents/VisibleField.d.ts +3 -1
  40. package/dist/components/Select/subcomponents/index.d.ts +1 -0
  41. package/dist/components/WeekPicker/WeekPicker.d.ts +2 -2
  42. package/dist/components/WeekPicker/WeekPicker.stories.d.ts +41 -0
  43. package/dist/components/WeekPicker/WeekPicker.types.d.ts +16 -0
  44. package/dist/components/WeekPicker/index.d.ts +1 -0
  45. package/dist/components/WeekPicker/subcomponents/CustomDatepicker.d.ts +1 -1
  46. package/dist/components/index.d.ts +8 -0
  47. package/dist/hooks/useFocusTrap.d.ts +2 -1
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.js +4366 -3768
  50. package/dist/utils/__tests__/announce.test.d.ts +1 -0
  51. package/dist/utils/announce.d.ts +6 -0
  52. package/dist/utils/index.d.ts +1 -0
  53. package/lib/components/Accordion/Accordion.Heading.tsx +27 -8
  54. package/lib/components/Accordion/Accordion.Panel.tsx +11 -3
  55. package/lib/components/Accordion/Accordion.stories.tsx +139 -0
  56. package/lib/components/Accordion/Accordion.tsx +10 -8
  57. package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +7 -7
  58. package/lib/components/Accordion/index.ts +2 -0
  59. package/lib/components/Alert/Alert.stories.tsx +1 -1
  60. package/lib/components/Avatar/Avatar.mdx +117 -0
  61. package/lib/components/Avatar/Avatar.stories.tsx +110 -2
  62. package/lib/components/Blanket/Blanket.stories.tsx +1 -1
  63. package/lib/components/Button/Button.stories.tsx +1 -1
  64. package/lib/components/Button/Button.tsx +1 -0
  65. package/lib/components/Calendar/Calendar.stories.tsx +12 -32
  66. package/lib/components/Calendar/__tests__/Calendar.test.tsx +23 -15
  67. package/lib/components/Calendar/index.ts +1 -5
  68. package/lib/components/Calendar/subcomponents/AcademicWeeks.tsx +2 -1
  69. package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +5 -1
  70. package/lib/components/Calendar/subcomponents/EventDot.tsx +2 -1
  71. package/lib/components/Calendar/subcomponents/index.ts +1 -1
  72. package/lib/components/Calendar/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.ts +43 -11
  73. package/lib/components/Calendar/utils/normaliseMonth/normaliseMonth.test.ts +5 -5
  74. package/lib/components/Datepicker/Datepicker.lld.md +108 -0
  75. package/lib/components/Datepicker/Datepicker.stories.tsx +44 -5
  76. package/lib/components/Datepicker/Datepicker.tsx +14 -36
  77. package/lib/components/Datepicker/Datepicker.types.ts +5 -14
  78. package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +150 -8
  79. package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +10 -4
  80. package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +39 -5
  81. package/lib/components/Datepicker/subcomponents/DatepickerInput.tsx +30 -17
  82. package/lib/components/Datepicker/subcomponents/Panel.tsx +6 -2
  83. package/lib/components/Datepicker/subcomponents/VisibleField.tsx +40 -3
  84. package/lib/components/Datepicker/subcomponents/index.ts +0 -1
  85. package/lib/components/Datepicker/utils/index.ts +0 -1
  86. package/lib/components/Dialog/BaseDialog.tsx +11 -0
  87. package/lib/components/Dialog/Dialog.tsx +8 -1
  88. package/lib/components/Dialog/DialogBody.tsx +5 -1
  89. package/lib/components/Dialog/DialogHeader.tsx +2 -1
  90. package/lib/components/Divider/Divider.stories.tsx +1 -1
  91. package/lib/components/Field/ErrorText.tsx +1 -0
  92. package/lib/components/Field/Field.stories.tsx +1 -1
  93. package/lib/components/Field/__tests__/Field.test.tsx +13 -0
  94. package/lib/components/FileInput/FileInput.stories.tsx +1 -1
  95. package/lib/components/Footer/Footer.stories.tsx +1 -1
  96. package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +3 -3
  97. package/lib/components/Header/Header.mdx +52 -0
  98. package/lib/components/Header/Header.stories.tsx +98 -0
  99. package/lib/components/Header/Header.tsx +51 -6
  100. package/lib/components/Header/__tests__/Header.test.tsx +17 -1
  101. package/lib/components/Heading/Heading.stories.tsx +1 -1
  102. package/lib/components/Icon/Icon.stories.tsx +1 -1
  103. package/lib/components/IconButton/IconButton.stories.tsx +1 -1
  104. package/lib/components/Input/Input.stories.tsx +1 -1
  105. package/lib/components/Label/Label.stories.tsx +1 -1
  106. package/lib/components/Main/Main.stories.tsx +36 -0
  107. package/lib/components/Main/Main.tsx +46 -0
  108. package/lib/components/Main/__tests__/Main.test.tsx +80 -0
  109. package/lib/components/Main/__tests__/__snapshots__/Main.test.tsx.snap +33 -0
  110. package/lib/components/Main/index.ts +2 -0
  111. package/lib/components/NativeDatepicker/NativeDatepicker.stories.tsx +100 -0
  112. package/lib/components/{Datepicker/subcomponents → NativeDatepicker}/NativeDatepicker.tsx +14 -15
  113. package/lib/components/NativeDatepicker/NativeDatepicker.types.ts +19 -0
  114. package/lib/components/NativeDatepicker/index.ts +2 -0
  115. package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.ts +1 -1
  116. package/lib/components/NativeDatepicker/utils/index.ts +1 -0
  117. package/lib/components/Pagination/PaginationControls.tsx +55 -12
  118. package/lib/components/Pagination/PaginationInfo.tsx +5 -1
  119. package/lib/components/Paragraph/Paragraph.stories.tsx +1 -1
  120. package/lib/components/Search/Search.stories.tsx +1 -1
  121. package/lib/components/Search/Search.tsx +4 -1
  122. package/lib/components/Search/__tests__/Search.test.tsx +19 -1
  123. package/lib/components/Select/Select.mdx +169 -0
  124. package/lib/components/Select/Select.stories.tsx +191 -43
  125. package/lib/components/Select/Select.tsx +36 -12
  126. package/lib/components/Select/Select.types.ts +66 -48
  127. package/lib/components/Select/__tests__/Select.test.tsx +448 -7
  128. package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
  129. package/lib/components/Select/subcomponents/CustomOption.tsx +2 -1
  130. package/lib/components/Select/subcomponents/CustomSelect.tsx +303 -33
  131. package/lib/components/Select/subcomponents/FilterInput.tsx +80 -0
  132. package/lib/components/Select/subcomponents/NativeSelect.tsx +13 -1
  133. package/lib/components/Select/subcomponents/VisibleField.tsx +11 -3
  134. package/lib/components/Select/subcomponents/index.tsx +1 -0
  135. package/lib/components/Snackbar/Snackbar.stories.tsx +1 -1
  136. package/lib/components/Spinner/Spinner.stories.tsx +1 -1
  137. package/lib/components/Textarea/Textarea.stories.tsx +1 -1
  138. package/lib/components/Timepicker/Timepicker.tsx +4 -0
  139. package/lib/components/Timepicker/__tests__/__snapshots__/Timepicker.test.tsx.snap +2 -2
  140. package/lib/components/Toggle/Toggle.stories.tsx +1 -1
  141. package/lib/components/Tooltip/Tooltip.stories.tsx +1 -1
  142. package/lib/components/WeekPicker/WeekPicker.stories.tsx +147 -0
  143. package/lib/components/WeekPicker/WeekPicker.tsx +2 -2
  144. package/lib/components/WeekPicker/WeekPicker.types.ts +21 -0
  145. package/lib/components/WeekPicker/index.ts +1 -0
  146. package/lib/components/WeekPicker/subcomponents/CustomDatepicker.tsx +1 -1
  147. package/lib/components/common/Common.mdx +1 -1
  148. package/lib/components/index.ts +11 -2
  149. package/lib/hooks/useFocusTrap.ts +40 -4
  150. package/lib/index.ts +1 -0
  151. package/lib/utils/__tests__/announce.test.ts +121 -0
  152. package/lib/utils/announce.ts +134 -0
  153. package/lib/utils/index.ts +1 -0
  154. package/package.json +3 -6
  155. package/dist/components/Datepicker/subcomponents/NativeDatepicker.d.ts +0 -6
  156. package/lib/components/Accordion/Accordion.stories.tsx.NOT_READY +0 -93
  157. /package/dist/components/{Datepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts → Main/__tests__/Main.test.d.ts} +0 -0
  158. /package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.test.ts +0 -0
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
3
3
  import Tooltip from './Tooltip';
4
4
 
5
5
  const meta = {
6
- title: 'Components/Ready to use/Tooltip',
6
+ title: 'Components/Tooltip',
7
7
  component: Tooltip,
8
8
  args: {
9
9
  children: 'Default tooltip text',
@@ -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 { DatepickerProps } from '../Datepicker';
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
- }: DatepickerProps) => {
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
+ }
@@ -1 +1,2 @@
1
1
  export { default } from './WeekPicker';
2
+ export type { WeekPickerProps, DatepickerValue } from './WeekPicker.types';
@@ -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 '../../Datepicker/Datepicker.types';
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-ready-to-use-button--common-margins)
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
 
@@ -103,8 +103,8 @@ export type { TabsProps } from './Tabs';
103
103
  export type { TabProps } from './Tabs/Tab';
104
104
 
105
105
  // todo:
106
- // export { default as Accordion } from './Accordion';
107
- // export type { AccordionProps } from './Accordion';
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 focusElement = initialFocusRef?.current || getFocusableElements()[0];
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 setTimeout to ensure the element is rendered and focusable
93
- setTimeout(() => focusElement.focus(), 0);
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
- }, [isActive, initialFocusRef, getFocusableElements, handleKeyDown]);
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
@@ -2,3 +2,4 @@ export { cx, css } from '@emotion/css';
2
2
  export * from './components';
3
3
  export * from './theme';
4
4
  export * from './hooks';
5
+ export * from './utils';
@@ -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.14.21",
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": "^18.0.0 || ^19.0.0",
49
- "react-dom": "^18.0.0 || ^19.0.0"
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;