uikit-react-public 0.11.24 → 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 (301) 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/Badge/Badge.d.ts +6 -0
  9. package/dist/components/Badge/Badge.stories.d.ts +15 -0
  10. package/dist/components/Badge/index.d.ts +2 -0
  11. package/dist/components/Button/Button.d.ts +3 -1
  12. package/dist/components/Calendar/index.d.ts +1 -1
  13. package/dist/components/CookieNotice/CookieNotice.d.ts +16 -0
  14. package/dist/components/CookieNotice/index.d.ts +2 -0
  15. package/dist/components/Datepicker/Datepicker.d.ts +1 -1
  16. package/dist/components/Datepicker/Datepicker.stories.d.ts +4 -3
  17. package/dist/components/Datepicker/Datepicker.types.d.ts +4 -5
  18. package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +4 -1
  19. package/dist/components/Datepicker/subcomponents/DatepickerInput.d.ts +15 -2
  20. package/dist/components/Datepicker/subcomponents/Panel.d.ts +1 -1
  21. package/dist/components/Datepicker/subcomponents/VisibleField.d.ts +6 -1
  22. package/dist/components/Datepicker/subcomponents/index.d.ts +0 -1
  23. package/dist/components/Datepicker/utils/index.d.ts +0 -1
  24. package/dist/components/Dialog/BaseDialog.d.ts +8 -2
  25. package/dist/components/Dialog/Dialog.d.ts +2 -0
  26. package/dist/components/FileInput/FileInput.d.ts +8 -0
  27. package/dist/components/FileInput/FileInput.stories.d.ts +16 -0
  28. package/dist/components/FileInput/index.d.ts +2 -0
  29. package/dist/components/Header/Header.d.ts +7 -1
  30. package/dist/components/Header/Header.stories.d.ts +40 -0
  31. package/dist/components/Heading/Heading.d.ts +1 -1
  32. package/dist/components/Link/BaseLink.d.ts +10 -0
  33. package/dist/components/Link/Link.d.ts +5 -10
  34. package/dist/components/Link/Link.stories.d.ts +1 -1
  35. package/dist/components/Link/index.d.ts +1 -1
  36. package/dist/components/Main/Main.d.ts +21 -0
  37. package/dist/components/Main/Main.stories.d.ts +15 -0
  38. package/dist/components/Main/__tests__/Main.test.d.ts +1 -0
  39. package/dist/components/Main/index.d.ts +2 -0
  40. package/dist/components/Menu/MenuContent.d.ts +1 -1
  41. package/dist/components/Menu/MenuItem.d.ts +2 -0
  42. package/dist/components/Menu/MenuSection.d.ts +1 -1
  43. package/dist/components/NativeDatepicker/NativeDatepicker.d.ts +3 -0
  44. package/dist/components/NativeDatepicker/NativeDatepicker.stories.d.ts +36 -0
  45. package/dist/components/NativeDatepicker/NativeDatepicker.types.d.ts +10 -0
  46. package/dist/components/NativeDatepicker/index.d.ts +2 -0
  47. package/dist/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.d.ts +1 -1
  48. package/dist/components/NativeDatepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts +1 -0
  49. package/dist/components/NativeDatepicker/utils/index.d.ts +1 -0
  50. package/dist/components/Search/Search.d.ts +16 -0
  51. package/dist/components/Search/Search.stories.d.ts +34 -0
  52. package/dist/components/Search/__tests__/Search.test.d.ts +1 -0
  53. package/dist/components/Search/index.d.ts +2 -0
  54. package/dist/components/Select/Select.d.ts +1 -1
  55. package/dist/components/Select/Select.stories.d.ts +157 -9
  56. package/dist/components/Select/Select.types.d.ts +66 -32
  57. package/dist/components/Select/subcomponents/CustomOption.d.ts +1 -1
  58. package/dist/components/Select/subcomponents/CustomSelect.d.ts +3 -3
  59. package/dist/components/Select/subcomponents/FilterInput.d.ts +14 -0
  60. package/dist/components/Select/subcomponents/NativeSelect.d.ts +5 -1
  61. package/dist/components/Select/subcomponents/Panel.d.ts +1 -1
  62. package/dist/components/Select/subcomponents/VisibleField.d.ts +6 -4
  63. package/dist/components/Select/subcomponents/index.d.ts +1 -0
  64. package/dist/components/StandaloneLink/StandaloneLink.d.ts +12 -0
  65. package/dist/components/StandaloneLink/StandaloneLink.stories.d.ts +13 -0
  66. package/dist/components/StandaloneLink/__tests__/StandaloneLink.test.d.ts +1 -0
  67. package/dist/components/StandaloneLink/index.d.ts +2 -0
  68. package/dist/components/Table/Table.d.ts +10 -8
  69. package/dist/components/Table/Table.stories.d.ts +21 -0
  70. package/dist/components/Table/Table.types.d.ts +11 -0
  71. package/dist/components/Table/__tests__/Table.test.d.ts +1 -0
  72. package/dist/components/Table/index.d.ts +2 -1
  73. package/dist/components/Table/subcomponents/Body.d.ts +4 -0
  74. package/dist/components/Table/subcomponents/Cell/Cell.d.ts +12 -0
  75. package/dist/components/Table/subcomponents/Cell/Cell.stories.d.ts +313 -0
  76. package/dist/components/Table/subcomponents/Cell/CellContent.d.ts +10 -0
  77. package/dist/components/Table/subcomponents/Cell/__tests__/Cell.test.d.ts +1 -0
  78. package/dist/components/Table/subcomponents/Head.d.ts +4 -0
  79. package/dist/components/Table/subcomponents/HeadCell/HeadCell.d.ts +13 -0
  80. package/dist/components/Table/subcomponents/HeadCell/HeadCell.stories.d.ts +312 -0
  81. package/dist/components/Table/subcomponents/HeadCell/HeadCellContent.d.ts +10 -0
  82. package/dist/components/Table/subcomponents/HeadCell/__tests__/HeadCell.test.d.ts +1 -0
  83. package/dist/components/Table/subcomponents/Row.d.ts +5 -0
  84. package/dist/components/Table/subcomponents/SortIcon.d.ts +7 -0
  85. package/dist/components/Table/subcomponents/index.d.ts +10 -0
  86. package/dist/components/Tabs/Tab.d.ts +1 -1
  87. package/dist/components/Tabs/TabContext.d.ts +1 -0
  88. package/dist/components/Tabs/Tabs.d.ts +3 -1
  89. package/dist/components/Tabs/Tabs.stories.d.ts +3 -0
  90. package/dist/components/Timepicker/Timepicker.d.ts +10 -0
  91. package/dist/components/Timepicker/Timepicker.stories.d.ts +7 -0
  92. package/dist/components/Timepicker/__tests__/Timepicker.test.d.ts +1 -0
  93. package/dist/components/Timepicker/index.d.ts +2 -0
  94. package/dist/components/Timepicker/utils/convertDateToTimeString.d.ts +2 -0
  95. package/dist/components/Timepicker/utils/convertDateToTimeString.test.d.ts +1 -0
  96. package/dist/components/Timepicker/utils/index.d.ts +1 -0
  97. package/dist/components/WeekPicker/WeekPicker.d.ts +3 -0
  98. package/dist/components/WeekPicker/WeekPicker.stories.d.ts +41 -0
  99. package/dist/components/WeekPicker/WeekPicker.types.d.ts +16 -0
  100. package/dist/components/WeekPicker/index.d.ts +2 -0
  101. package/dist/components/WeekPicker/subcomponents/CustomDatepicker.d.ts +17 -0
  102. package/dist/components/WeekPicker/subcomponents/DatepickerInput.d.ts +13 -0
  103. package/dist/components/WeekPicker/subcomponents/VisibleField.d.ts +15 -0
  104. package/dist/components/WeekPicker/subcomponents/index.d.ts +3 -0
  105. package/dist/components/index.d.ts +19 -0
  106. package/dist/hooks/index.d.ts +2 -0
  107. package/dist/hooks/useFocusTrap.d.ts +10 -0
  108. package/dist/index.d.ts +2 -0
  109. package/dist/index.js +6460 -4607
  110. package/dist/theme/defaultTheme.d.ts +7 -0
  111. package/dist/theme/useTheme.d.ts +14 -0
  112. package/dist/utils/__tests__/announce.test.d.ts +1 -0
  113. package/dist/utils/__tests__/capitalise.test.d.ts +1 -0
  114. package/dist/utils/announce.d.ts +6 -0
  115. package/dist/utils/capitalise.d.ts +2 -0
  116. package/dist/utils/index.d.ts +1 -0
  117. package/lib/components/Accordion/Accordion.Heading.tsx +27 -8
  118. package/lib/components/Accordion/Accordion.Panel.tsx +11 -3
  119. package/lib/components/Accordion/Accordion.stories.tsx +139 -0
  120. package/lib/components/Accordion/Accordion.tsx +10 -8
  121. package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +7 -7
  122. package/lib/components/Accordion/index.ts +2 -0
  123. package/lib/components/Alert/Alert.stories.tsx +1 -1
  124. package/lib/components/Alert/Alert.tsx +7 -1
  125. package/lib/components/Alert/__tests__/__snapshots__/Alert.test.tsx.snap +4 -0
  126. package/lib/components/Avatar/Avatar.mdx +117 -0
  127. package/lib/components/Avatar/Avatar.stories.tsx +110 -2
  128. package/lib/components/Badge/Badge.stories.tsx +19 -0
  129. package/lib/components/Badge/Badge.tsx +48 -0
  130. package/lib/components/Badge/index.ts +2 -0
  131. package/lib/components/Blanket/Blanket.stories.tsx +1 -1
  132. package/lib/components/Breadcrumbs/__tests__/__snapshots__/Breadcrumbs.test.tsx.snap +4 -4
  133. package/lib/components/Button/Button.stories.tsx +1 -1
  134. package/lib/components/Button/Button.tsx +6 -2
  135. package/lib/components/Calendar/Calendar.stories.tsx +12 -32
  136. package/lib/components/Calendar/__tests__/Calendar.test.tsx +23 -15
  137. package/lib/components/Calendar/index.ts +1 -5
  138. package/lib/components/Calendar/subcomponents/AcademicWeeks.tsx +2 -1
  139. package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +5 -1
  140. package/lib/components/Calendar/subcomponents/EventDot.tsx +2 -1
  141. package/lib/components/Calendar/subcomponents/Grid.tsx +0 -1
  142. package/lib/components/Calendar/subcomponents/index.ts +1 -1
  143. package/lib/components/Calendar/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.ts +43 -11
  144. package/lib/components/Calendar/utils/normaliseMonth/normaliseMonth.test.ts +5 -5
  145. package/lib/components/CookieNotice/CookieNotice.tsx +114 -0
  146. package/lib/components/CookieNotice/index.ts +2 -0
  147. package/lib/components/Datepicker/Datepicker.lld.md +108 -0
  148. package/lib/components/Datepicker/Datepicker.stories.tsx +44 -5
  149. package/lib/components/Datepicker/Datepicker.tsx +14 -36
  150. package/lib/components/Datepicker/Datepicker.types.ts +5 -14
  151. package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +150 -8
  152. package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +10 -4
  153. package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +39 -5
  154. package/lib/components/Datepicker/subcomponents/DatepickerInput.tsx +30 -17
  155. package/lib/components/Datepicker/subcomponents/Panel.tsx +6 -2
  156. package/lib/components/Datepicker/subcomponents/VisibleField.tsx +40 -3
  157. package/lib/components/Datepicker/subcomponents/index.ts +0 -1
  158. package/lib/components/Datepicker/utils/index.ts +0 -1
  159. package/lib/components/Dialog/BaseDialog.tsx +55 -4
  160. package/lib/components/Dialog/Dialog.tsx +8 -1
  161. package/lib/components/Dialog/DialogBody.tsx +5 -1
  162. package/lib/components/Dialog/DialogHeader.tsx +2 -1
  163. package/lib/components/Divider/Divider.stories.tsx +1 -1
  164. package/lib/components/Field/ErrorText.tsx +1 -0
  165. package/lib/components/Field/Field.stories.tsx +1 -1
  166. package/lib/components/Field/__tests__/Field.test.tsx +161 -148
  167. package/lib/components/FileInput/FileInput.stories.tsx +70 -0
  168. package/lib/components/FileInput/FileInput.tsx +68 -0
  169. package/lib/components/FileInput/__tests__/FileInput.test.tsx +99 -0
  170. package/lib/components/FileInput/__tests__/__snapshots__/FileInput.test.tsx.snap +91 -0
  171. package/lib/components/FileInput/index.ts +2 -0
  172. package/lib/components/Footer/Footer.stories.tsx +1 -1
  173. package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +28 -28
  174. package/lib/components/Header/Header.mdx +52 -0
  175. package/lib/components/Header/Header.stories.tsx +98 -0
  176. package/lib/components/Header/Header.tsx +65 -3
  177. package/lib/components/Header/__tests__/Header.test.tsx +17 -1
  178. package/lib/components/Header/__tests__/__snapshots__/Header.test.tsx.snap +4 -4
  179. package/lib/components/Heading/Documentation.mdx +1 -1
  180. package/lib/components/Heading/Heading.stories.tsx +1 -1
  181. package/lib/components/Heading/Heading.tsx +1 -1
  182. package/lib/components/Heading/__tests__/Heading.test.tsx +7 -19
  183. package/lib/components/Heading/__tests__/__snapshots__/Heading.test.tsx.snap +7 -7
  184. package/lib/components/Icon/Icon.stories.tsx +1 -1
  185. package/lib/components/IconButton/IconButton.stories.tsx +1 -1
  186. package/lib/components/Input/Input.stories.tsx +1 -1
  187. package/lib/components/Label/Label.stories.tsx +1 -1
  188. package/lib/components/Label/Label.tsx +0 -2
  189. package/lib/components/Label/__tests__/__snapshots__/Label.test.tsx.snap +7 -7
  190. package/lib/components/Link/BaseLink.tsx +84 -0
  191. package/lib/components/Link/Link.tsx +72 -32
  192. package/lib/components/Link/__tests__/__snapshots__/link.test.tsx.snap +3 -3
  193. package/lib/components/Link/__tests__/link.test.tsx +6 -13
  194. package/lib/components/Link/index.ts +1 -1
  195. package/lib/components/Main/Main.stories.tsx +36 -0
  196. package/lib/components/Main/Main.tsx +46 -0
  197. package/lib/components/Main/__tests__/Main.test.tsx +80 -0
  198. package/lib/components/Main/__tests__/__snapshots__/Main.test.tsx.snap +33 -0
  199. package/lib/components/Main/index.ts +2 -0
  200. package/lib/components/Menu/Menu.context.tsx +3 -1
  201. package/lib/components/Menu/Menu.tsx +2 -2
  202. package/lib/components/Menu/MenuContent.tsx +5 -5
  203. package/lib/components/Menu/MenuItem.tsx +20 -3
  204. package/lib/components/Menu/MenuSection.tsx +4 -3
  205. package/lib/components/NativeDatepicker/NativeDatepicker.stories.tsx +100 -0
  206. package/lib/components/{Datepicker/subcomponents → NativeDatepicker}/NativeDatepicker.tsx +14 -15
  207. package/lib/components/NativeDatepicker/NativeDatepicker.types.ts +19 -0
  208. package/lib/components/NativeDatepicker/index.ts +2 -0
  209. package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.ts +1 -1
  210. package/lib/components/NativeDatepicker/utils/index.ts +1 -0
  211. package/lib/components/Pagination/PaginationControls.tsx +56 -15
  212. package/lib/components/Pagination/PaginationInfo.tsx +5 -1
  213. package/lib/components/Paragraph/Paragraph.stories.tsx +1 -1
  214. package/lib/components/Search/Search.stories.tsx +41 -0
  215. package/lib/components/Search/Search.tsx +170 -0
  216. package/lib/components/Search/__tests__/Search.test.tsx +112 -0
  217. package/lib/components/Search/__tests__/__snapshots__/Search.test.tsx.snap +179 -0
  218. package/lib/components/Search/index.ts +2 -0
  219. package/lib/components/Select/Select.mdx +169 -0
  220. package/lib/components/Select/Select.stories.tsx +198 -77
  221. package/lib/components/Select/Select.tsx +37 -13
  222. package/lib/components/Select/Select.types.ts +77 -54
  223. package/lib/components/Select/__tests__/Select.test.tsx +448 -7
  224. package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +3 -3
  225. package/lib/components/Select/subcomponents/CustomOption.tsx +24 -10
  226. package/lib/components/Select/subcomponents/CustomSelect.tsx +333 -52
  227. package/lib/components/Select/subcomponents/FilterInput.tsx +80 -0
  228. package/lib/components/Select/subcomponents/NativeSelect.tsx +13 -1
  229. package/lib/components/Select/subcomponents/Panel.tsx +4 -5
  230. package/lib/components/Select/subcomponents/VisibleField.tsx +36 -24
  231. package/lib/components/Select/subcomponents/index.tsx +1 -0
  232. package/lib/components/Snackbar/Snackbar.stories.tsx +1 -1
  233. package/lib/components/Spinner/Spinner.stories.tsx +1 -1
  234. package/lib/components/StandaloneLink/StandaloneLink.stories.tsx +32 -0
  235. package/lib/components/StandaloneLink/StandaloneLink.tsx +183 -0
  236. package/lib/components/StandaloneLink/__tests__/StandaloneLink.test.tsx +57 -0
  237. package/lib/components/StandaloneLink/__tests__/__snapshots__/StandaloneLink.test.tsx.snap +19 -0
  238. package/lib/components/StandaloneLink/index.ts +2 -0
  239. package/lib/components/Table/Table.stories.tsx +337 -0
  240. package/lib/components/Table/Table.tsx +42 -67
  241. package/lib/components/Table/Table.types.ts +14 -0
  242. package/lib/components/Table/__tests__/Table.test.tsx +121 -0
  243. package/lib/components/Table/__tests__/__snapshots__/Table.test.tsx.snap +210 -0
  244. package/lib/components/Table/index.ts +8 -1
  245. package/lib/components/Table/subcomponents/Body.tsx +18 -0
  246. package/lib/components/Table/subcomponents/Cell/Cell.stories.tsx +151 -0
  247. package/lib/components/Table/subcomponents/Cell/Cell.tsx +72 -0
  248. package/lib/components/Table/subcomponents/Cell/CellContent.tsx +91 -0
  249. package/lib/components/Table/subcomponents/Cell/__tests__/Cell.test.tsx +115 -0
  250. package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +107 -0
  251. package/lib/components/Table/subcomponents/Head.tsx +34 -0
  252. package/lib/components/Table/subcomponents/HeadCell/HeadCell.stories.tsx +85 -0
  253. package/lib/components/Table/subcomponents/HeadCell/HeadCell.tsx +99 -0
  254. package/lib/components/Table/subcomponents/HeadCell/HeadCellContent.tsx +61 -0
  255. package/lib/components/Table/subcomponents/HeadCell/__tests__/HeadCell.test.tsx +137 -0
  256. package/lib/components/Table/subcomponents/HeadCell/__tests__/__snapshots__/HeadCell.test.tsx.snap +110 -0
  257. package/lib/components/Table/subcomponents/Row.tsx +49 -0
  258. package/lib/components/Table/subcomponents/SortIcon.tsx +63 -0
  259. package/lib/components/Table/subcomponents/index.ts +14 -0
  260. package/lib/components/Tabs/Tab.tsx +3 -3
  261. package/lib/components/Tabs/TabContext.tsx +1 -0
  262. package/lib/components/Tabs/Tabs.stories.tsx +9 -3
  263. package/lib/components/Tabs/Tabs.tsx +10 -32
  264. package/lib/components/Tabs/__tests__/Tabs.test.tsx +10 -4
  265. package/lib/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap +32 -32
  266. package/lib/components/Textarea/Textarea.stories.tsx +1 -1
  267. package/lib/components/Timepicker/Timepicker.stories.tsx +43 -0
  268. package/lib/components/Timepicker/Timepicker.tsx +100 -0
  269. package/lib/components/Timepicker/__tests__/Timepicker.test.tsx +55 -0
  270. package/lib/components/Timepicker/__tests__/__snapshots__/Timepicker.test.tsx.snap +19 -0
  271. package/lib/components/Timepicker/index.tsx +2 -0
  272. package/lib/components/Timepicker/utils/convertDateToTimeString.test.ts +54 -0
  273. package/lib/components/Timepicker/utils/convertDateToTimeString.ts +10 -0
  274. package/lib/components/Timepicker/utils/index.ts +1 -0
  275. package/lib/components/Toggle/Toggle.stories.tsx +1 -1
  276. package/lib/components/Tooltip/Tooltip.stories.tsx +1 -1
  277. package/lib/components/WeekPicker/WeekPicker.stories.tsx +147 -0
  278. package/lib/components/WeekPicker/WeekPicker.tsx +26 -0
  279. package/lib/components/WeekPicker/WeekPicker.types.ts +21 -0
  280. package/lib/components/WeekPicker/index.ts +2 -0
  281. package/lib/components/WeekPicker/subcomponents/CustomDatepicker.tsx +298 -0
  282. package/lib/components/WeekPicker/subcomponents/DatepickerInput.tsx +111 -0
  283. package/lib/components/WeekPicker/subcomponents/VisibleField.tsx +126 -0
  284. package/lib/components/WeekPicker/subcomponents/index.ts +3 -0
  285. package/lib/components/common/Common.mdx +1 -1
  286. package/lib/components/index.ts +28 -2
  287. package/lib/hooks/index.ts +2 -0
  288. package/lib/hooks/useFocusTrap.ts +159 -0
  289. package/lib/index.ts +2 -0
  290. package/lib/theme/defaultTheme.ts +7 -0
  291. package/lib/utils/__tests__/announce.test.ts +121 -0
  292. package/lib/utils/__tests__/capitalise.test.ts +40 -0
  293. package/lib/utils/announce.ts +134 -0
  294. package/lib/utils/capitalise.ts +4 -0
  295. package/lib/utils/index.ts +1 -0
  296. package/package.json +3 -6
  297. package/dist/components/Datepicker/subcomponents/NativeDatepicker.d.ts +0 -6
  298. package/lib/components/Accordion/Accordion.stories.tsx.NOT_READY +0 -93
  299. package/lib/components/Field/__tests__/__snapshots__/Field.test.tsx.snap +0 -300
  300. /package/dist/components/{Datepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts → FileInput/__tests__/FileInput.test.d.ts} +0 -0
  301. /package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.test.ts +0 -0
@@ -1,29 +1,149 @@
1
- import { useState, useRef, useEffect } from 'react';
1
+ import { useState, useRef, useEffect, useMemo, useId } from 'react';
2
2
  import { css, cx } from '@emotion/css';
3
- import { VisibleField, Panel, CustomOption } from '.';
3
+ import { VisibleField, Panel, CustomOption, FilterInput } from '.';
4
4
  import { useTheme } from '../../../theme';
5
- import type { CustomSelectProps } from '../Select.types';
5
+ import type { SelectProps } from '../Select.types';
6
6
 
7
7
  const NAME = 'ucl-uikit-select';
8
- export const DEFAULT_WIDTH_PX = 200;
9
8
 
10
- const CustomSelect = ({
9
+ type CustomSelectProps<T> = Omit<
10
+ SelectProps<T>,
11
+ 'native' | 'nativeHtmlAttributes'
12
+ >;
13
+
14
+ const CustomSelect = <T extends string | number>({
15
+ selectionBehaviour = 'focus',
11
16
  value,
12
17
  options = [],
13
18
  onValueChange,
14
19
  disabled,
15
20
  placeholder,
16
- width = DEFAULT_WIDTH_PX,
21
+ lineBreak = false,
22
+ filterInputProps,
23
+ width,
17
24
  testId = NAME,
18
25
  className,
26
+ panelClassName,
27
+ filterable = false,
19
28
  ref,
20
29
  ...props
21
- }: CustomSelectProps) => {
30
+ }: CustomSelectProps<T>) => {
31
+ // todo: remove width prop (deprecated)
32
+ if (width !== undefined) {
33
+ console.warn(
34
+ 'Select width prop is deprecated; it has no effect. Use className instead.'
35
+ );
36
+ }
37
+
38
+ const atLeastOneOptionHasIcon = !!options.find(
39
+ (opt) => opt.icon !== undefined
40
+ );
41
+ // todo: remove option icon prop (deprecated)
42
+ if (atLeastOneOptionHasIcon) {
43
+ console.warn('Select option icon prop is deprecated; it has no effect.');
44
+ }
45
+
46
+ const duplicateOptionValues = useMemo(() => {
47
+ const seen = new Set<T>();
48
+ const duplicates = new Set<T>();
49
+
50
+ for (const option of options) {
51
+ if (seen.has(option.value)) duplicates.add(option.value);
52
+ else seen.add(option.value);
53
+ }
54
+
55
+ return Array.from(duplicates);
56
+ }, [options]);
57
+
58
+ useEffect(() => {
59
+ if (duplicateOptionValues.length > 0) {
60
+ console.warn(
61
+ `Select options contain non-unique values: ${duplicateOptionValues
62
+ .map(String)
63
+ .join(', ')}`
64
+ );
65
+ }
66
+ }, [duplicateOptionValues]);
67
+
22
68
  const internalRef = useRef<HTMLDivElement>(null);
23
- const effectiveRef = ref || internalRef;
69
+ const effectiveRef = internalRef;
70
+ const openedViaFocusRef = useRef(false);
71
+ const skipOpenOnFocusRef = useRef(false);
72
+
73
+ const setRefs = (node: HTMLDivElement | null) => {
74
+ internalRef.current = node;
75
+ if (typeof ref === 'function') {
76
+ ref(node);
77
+ } else if (
78
+ ref &&
79
+ 'current' in
80
+ (ref as React.RefObject<HTMLDivElement | HTMLSelectElement | null>)
81
+ ) {
82
+ (
83
+ ref as React.RefObject<HTMLDivElement | HTMLSelectElement | null>
84
+ ).current = node;
85
+ }
86
+ };
24
87
 
25
88
  const [theme] = useTheme();
26
89
  const [isOpen, setIsOpen] = useState(false);
90
+ const [filterText, setFilterText] = useState('');
91
+ const [activeOptionIndex, setActiveOptionIndex] = useState<number | null>(
92
+ null
93
+ );
94
+ const [selectedOptionIndex, setSelectedOptionIndex] = useState<number | null>(
95
+ null
96
+ );
97
+ const filterInputRef = useRef<HTMLInputElement | null>(null);
98
+ const reactId = useId();
99
+ const idBase = props.id ?? `${testId}-${reactId.replace(/[:]/g, '')}`;
100
+ const listboxId = `${idBase}-listbox`;
101
+
102
+ // Returns a list of indexes of options that are currently visible based on the filter text
103
+ const visibleOptionIndexes = useMemo(() => {
104
+ const normalizedFilterText = filterText.toLowerCase();
105
+ return filterable
106
+ ? options.reduce<number[]>((visibleIndexes, option, index) => {
107
+ if (option.label.toLowerCase().includes(normalizedFilterText)) {
108
+ visibleIndexes.push(index);
109
+ }
110
+ return visibleIndexes;
111
+ }, [])
112
+ : options.map((_, index) => index);
113
+ }, [filterable, options, filterText]);
114
+ const visibleOptions = useMemo(
115
+ () => visibleOptionIndexes.map((index) => options[index]),
116
+ [visibleOptionIndexes, options]
117
+ );
118
+
119
+ useEffect(() => {
120
+ if (!isOpen && filterText) setFilterText('');
121
+ }, [isOpen, filterText]);
122
+
123
+ useEffect(() => {
124
+ const matchingIndexes = options.reduce<number[]>(
125
+ (matches, option, index) => {
126
+ if (option.value === value) matches.push(index);
127
+ return matches;
128
+ },
129
+ []
130
+ );
131
+
132
+ if (matchingIndexes.length === 0) {
133
+ if (selectedOptionIndex !== null) setSelectedOptionIndex(null);
134
+ return;
135
+ }
136
+
137
+ // If the currently selected option is among the matches, keep it selected
138
+ if (
139
+ selectedOptionIndex !== null &&
140
+ matchingIndexes.includes(selectedOptionIndex)
141
+ ) {
142
+ return;
143
+ }
144
+ // Otherwise, select the first matching option
145
+ setSelectedOptionIndex(matchingIndexes[0]);
146
+ }, [options, selectedOptionIndex, value]);
27
147
 
28
148
  useEffect(() => {
29
149
  const handleClickOutside = (event: MouseEvent) => {
@@ -34,6 +154,7 @@ const CustomSelect = ({
34
154
  setIsOpen(false);
35
155
  }
36
156
  };
157
+
37
158
  document.addEventListener('mousedown', handleClickOutside);
38
159
  return () => {
39
160
  document.removeEventListener('mousedown', handleClickOutside);
@@ -45,6 +166,12 @@ const CustomSelect = ({
45
166
  if (disabled && isOpen) setIsOpen(false);
46
167
  }, [disabled, isOpen]);
47
168
 
169
+ useEffect(() => {
170
+ if (filterable && isOpen && filterInputRef.current) {
171
+ filterInputRef.current.focus();
172
+ }
173
+ }, [filterable, isOpen]);
174
+
48
175
  const togglePanel = () => {
49
176
  if (!disabled) setIsOpen((prev) => !prev);
50
177
  };
@@ -54,18 +181,82 @@ const CustomSelect = ({
54
181
  };
55
182
 
56
183
  const closePanel = () => {
57
- if (!disabled) setIsOpen(false);
184
+ if (!disabled) {
185
+ setIsOpen(false);
186
+ setActiveOptionIndex(null);
187
+ }
188
+ };
189
+
190
+ const handleClick = (event: React.MouseEvent) => {
191
+ if (disabled) return;
192
+ if (openedViaFocusRef.current) {
193
+ openedViaFocusRef.current = false;
194
+ return;
195
+ }
196
+ if (!isOpen) {
197
+ openPanel();
198
+ if (filterable && filterInputRef.current) {
199
+ filterInputRef.current.focus();
200
+ }
201
+ return;
202
+ }
203
+ // If filter is enabled and the click was on the input, keep it open
204
+ if (
205
+ filterable &&
206
+ filterInputRef.current &&
207
+ filterInputRef.current.contains(event.target as Node)
208
+ ) {
209
+ return;
210
+ }
211
+ closePanel();
58
212
  };
59
213
 
60
214
  // Used by <CustomOption> and passed as prop
61
- const handleSelect = (event: React.MouseEvent, optionValue: string) => {
62
- if (onValueChange) onValueChange(optionValue, event);
215
+ const handleSelect = (
216
+ event: React.MouseEvent,
217
+ optionValue: T,
218
+ optionIndex?: number
219
+ ) => {
220
+ if (typeof optionIndex === 'number') {
221
+ setSelectedOptionIndex(optionIndex);
222
+ }
223
+ onValueChange?.(optionValue, event);
224
+ setFilterText('');
63
225
  closePanel();
64
226
  };
65
227
 
66
- const selectedOption = options.find((option) => option.value === value);
228
+ // Get the currently selected option object from its index among the visible options
229
+ const selectedOption =
230
+ selectedOptionIndex !== null ? options[selectedOptionIndex] : undefined;
231
+ // Get the index of the selected option among the visible options, or -1 if it's not visible
232
+ const selectedVisibleIndex =
233
+ selectedOptionIndex !== null
234
+ ? visibleOptionIndexes.indexOf(selectedOptionIndex)
235
+ : -1;
236
+ // Ensure the active option index is within bounds of the visible options
237
+ const effectiveActiveOptionIndex =
238
+ activeOptionIndex !== null &&
239
+ activeOptionIndex >= 0 &&
240
+ activeOptionIndex < visibleOptions.length
241
+ ? activeOptionIndex
242
+ : null;
243
+ // Get the index of the currently highlighted option among the visible options, or null if none is highlighted
244
+ const highlightedVisibleIndex =
245
+ effectiveActiveOptionIndex !== null
246
+ ? effectiveActiveOptionIndex
247
+ : selectedVisibleIndex >= 0
248
+ ? selectedVisibleIndex
249
+ : null;
250
+ const highlightedOptionSourceIndex =
251
+ highlightedVisibleIndex !== null
252
+ ? visibleOptionIndexes[highlightedVisibleIndex]
253
+ : null;
254
+ const activeDescendantId =
255
+ isOpen && highlightedOptionSourceIndex !== null
256
+ ? `${idBase}-option-${highlightedOptionSourceIndex}`
257
+ : undefined;
67
258
 
68
- const handleKeyDown = (event: React.KeyboardEvent) => {
259
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
69
260
  // Prevent scrolling the page when the select is open
70
261
  if (
71
262
  event.key === 'ArrowUp' ||
@@ -75,9 +266,48 @@ const CustomSelect = ({
75
266
  )
76
267
  event.preventDefault();
77
268
 
269
+ // For current option index, activeOptionIndex > selectedVisibleIndex > value match > null
270
+ const getCurrentOptionIndex = () => {
271
+ if (
272
+ activeOptionIndex !== null &&
273
+ activeOptionIndex >= 0 &&
274
+ activeOptionIndex < visibleOptions.length
275
+ ) {
276
+ return activeOptionIndex;
277
+ }
278
+ if (selectedOptionIndex !== null) {
279
+ const selectedVisibleIndex =
280
+ visibleOptionIndexes.indexOf(selectedOptionIndex);
281
+ if (selectedVisibleIndex >= 0) {
282
+ return selectedVisibleIndex;
283
+ }
284
+ }
285
+ const selectedIndex = visibleOptions.findIndex(
286
+ (option) => option.value === value
287
+ );
288
+ return selectedIndex >= 0 ? selectedIndex : null;
289
+ };
290
+
78
291
  if (disabled) return;
79
292
 
80
293
  if (event.key === 'Enter') {
294
+ if (!isOpen) {
295
+ openPanel();
296
+ return;
297
+ }
298
+
299
+ if (selectionBehaviour === 'commit' && visibleOptions.length > 0) {
300
+ const currentOptionIndex = getCurrentOptionIndex();
301
+ if (currentOptionIndex !== null) {
302
+ const currentSourceIndex = visibleOptionIndexes[currentOptionIndex];
303
+ setSelectedOptionIndex(currentSourceIndex);
304
+ onValueChange?.(visibleOptions[currentOptionIndex].value, event);
305
+ setFilterText('');
306
+ }
307
+ closePanel();
308
+ return;
309
+ }
310
+
81
311
  togglePanel();
82
312
  return;
83
313
  }
@@ -87,6 +317,8 @@ const CustomSelect = ({
87
317
  }
88
318
  if (isOpen && event.key === 'Escape') {
89
319
  closePanel();
320
+ skipOpenOnFocusRef.current = true;
321
+ event.currentTarget.focus();
90
322
  return;
91
323
  }
92
324
  // Select the previous option
@@ -95,17 +327,21 @@ const CustomSelect = ({
95
327
  openPanel();
96
328
  return;
97
329
  }
98
- if (!value) {
99
- // Initialise at the last option if no value provided
100
- onValueChange?.(options[options.length - 1].value, event);
330
+ if (visibleOptions.length === 0) {
101
331
  return;
102
332
  }
103
- const currentOptionIndex = options.findIndex(
104
- (option) => option.value === value
105
- );
333
+ const currentOptionIndex = getCurrentOptionIndex();
106
334
  const previousOptionIndex =
107
- (currentOptionIndex - 1 + options.length) % options.length;
108
- onValueChange?.(options[previousOptionIndex].value, event);
335
+ currentOptionIndex === null
336
+ ? visibleOptions.length - 1
337
+ : (currentOptionIndex - 1 + visibleOptions.length) %
338
+ visibleOptions.length;
339
+ const previousSourceIndex = visibleOptionIndexes[previousOptionIndex];
340
+ setActiveOptionIndex(previousOptionIndex);
341
+ if (selectionBehaviour === 'focus') {
342
+ setSelectedOptionIndex(previousSourceIndex);
343
+ onValueChange?.(visibleOptions[previousOptionIndex].value, event);
344
+ }
109
345
  return;
110
346
  }
111
347
  // Select the next option
@@ -114,18 +350,36 @@ const CustomSelect = ({
114
350
  openPanel();
115
351
  return;
116
352
  }
117
- if (!value) {
118
- // Initialise at the first option if no value provided
119
- onValueChange?.(options[0].value, event);
353
+ if (visibleOptions.length === 0) {
120
354
  return;
121
355
  }
122
- const currentOptionIndex = options.findIndex(
123
- (option) => option.value === value
124
- );
125
- const nextOptionIndex = (currentOptionIndex + 1) % options.length;
126
- onValueChange?.(options[nextOptionIndex].value, event);
356
+ const currentOptionIndex = getCurrentOptionIndex();
357
+ const nextOptionIndex =
358
+ currentOptionIndex === null
359
+ ? 0
360
+ : (currentOptionIndex + 1) % visibleOptions.length;
361
+ const nextSourceIndex = visibleOptionIndexes[nextOptionIndex];
362
+ setActiveOptionIndex(nextOptionIndex);
363
+ if (selectionBehaviour === 'focus') {
364
+ setSelectedOptionIndex(nextSourceIndex);
365
+ onValueChange?.(visibleOptions[nextOptionIndex].value, event);
366
+ }
367
+ return;
368
+ }
369
+ };
370
+
371
+ const handleFocus = (event: React.FocusEvent<HTMLDivElement>) => {
372
+ if (disabled) return;
373
+ if (skipOpenOnFocusRef.current) {
374
+ skipOpenOnFocusRef.current = false;
127
375
  return;
128
376
  }
377
+ const isKeyboardFocus = event.currentTarget.matches(':focus-visible');
378
+ if (filterable && isKeyboardFocus) {
379
+ openedViaFocusRef.current = true;
380
+ openPanel();
381
+ if (filterInputRef.current) filterInputRef.current.focus();
382
+ }
129
383
  };
130
384
 
131
385
  const baseStyle = css`
@@ -134,7 +388,8 @@ const CustomSelect = ({
134
388
  justify-content: space-between;
135
389
  position: relative;
136
390
  min-width: 80px;
137
- width: fit-content;
391
+ width: 100%;
392
+ max-width: calc(100vw - 64px);
138
393
  height: 48px;
139
394
  box-sizing: border-box;
140
395
  padding: 0 16px;
@@ -150,16 +405,13 @@ const CustomSelect = ({
150
405
  ${!isOpen && `background-color: ${theme.color.neutral.grey5};`}
151
406
  }
152
407
 
153
- &:focus-visible {
408
+ &:focus-visible,
409
+ &:focus-within {
154
410
  outline: none;
155
411
  box-shadow: ${theme.boxShadow.focus};
156
412
  }
157
413
  `;
158
414
 
159
- const widthStyle = css`
160
- width: ${width}px;
161
- `;
162
-
163
415
  const disabledStyle = css`
164
416
  color: ${theme.color.text.disabled};
165
417
  border: ${theme.border.b1} solid ${theme.color.neutral.grey20};
@@ -170,25 +422,27 @@ const CustomSelect = ({
170
422
  }
171
423
  `;
172
424
 
173
- const style = cx(
174
- NAME,
175
- baseStyle,
176
- !!width && widthStyle,
177
- disabled && disabledStyle,
178
- className
179
- );
425
+ const noOptionsStyle = css`
426
+ padding: ${theme.padding.p8} ${theme.padding.p16};
427
+ color: ${theme.color.text.secondary};
428
+ `;
429
+
430
+ const style = cx(NAME, baseStyle, disabled && disabledStyle, className);
180
431
 
181
432
  return (
182
433
  <div
183
- onClick={togglePanel}
434
+ onClick={handleClick}
184
435
  onKeyDown={handleKeyDown}
436
+ onFocus={handleFocus}
185
437
  tabIndex={disabled ? -1 : 0}
186
438
  className={style}
187
439
  data-testid={testId}
188
- ref={effectiveRef}
440
+ ref={setRefs}
189
441
  role='combobox'
190
442
  aria-haspopup='listbox'
191
443
  aria-expanded={isOpen}
444
+ aria-controls={isOpen ? listboxId : undefined}
445
+ aria-activedescendant={activeDescendantId}
192
446
  {...props}
193
447
  >
194
448
  <VisibleField
@@ -196,22 +450,49 @@ const CustomSelect = ({
196
450
  selectedOption={selectedOption}
197
451
  placeholder={placeholder}
198
452
  disabled={disabled}
199
- />
453
+ filterable={filterable}
454
+ >
455
+ {filterable && (
456
+ <FilterInput
457
+ value={filterText}
458
+ onChange={setFilterText}
459
+ placeholder={placeholder}
460
+ disabled={disabled}
461
+ inputRef={filterInputRef}
462
+ ariaControls={listboxId}
463
+ ariaExpanded={isOpen}
464
+ ariaActiveDescendant={activeDescendantId}
465
+ {...filterInputProps}
466
+ />
467
+ )}
468
+ </VisibleField>
200
469
  {isOpen && (
201
- <Panel role='listbox'>
202
- {options.map((option) => (
203
- <CustomOption
204
- key={option.value}
470
+ <Panel
471
+ className={panelClassName}
472
+ id={listboxId}
473
+ role='listbox'
474
+ >
475
+ {visibleOptions.map((option, index) => (
476
+ <CustomOption<T>
477
+ key={`${String(option.value)}-${visibleOptionIndexes[index]}`}
478
+ id={`${idBase}-option-${visibleOptionIndexes[index]}`}
205
479
  value={option.value}
206
- isSelected={value === option.value}
480
+ optionIndex={visibleOptionIndexes[index]}
481
+ isSelected={highlightedVisibleIndex === index}
207
482
  onSelect={handleSelect}
483
+ lineBreak={lineBreak}
208
484
  role='option'
209
- aria-selected={value === option.value}
485
+ aria-selected={highlightedVisibleIndex === index}
486
+ aria-posinset={index + 1}
487
+ aria-setsize={visibleOptions.length}
488
+ {...option.optionProps}
210
489
  >
211
- {option.icon}
212
490
  {option.label}
213
491
  </CustomOption>
214
492
  ))}
493
+ {visibleOptions.length === 0 && (
494
+ <div className={noOptionsStyle}>No options</div>
495
+ )}
215
496
  </Panel>
216
497
  )}
217
498
  </div>
@@ -0,0 +1,80 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import { useTheme } from '../../../theme';
3
+ import type { FilterInputProps } from '../Select.types';
4
+
5
+ type FilterInputComponentProps = {
6
+ value: string;
7
+ onChange: (value: string) => void;
8
+ placeholder?: string;
9
+ disabled?: boolean;
10
+ inputRef?: React.RefObject<HTMLInputElement | null>;
11
+ className?: string;
12
+ ariaControls?: string;
13
+ ariaExpanded?: boolean;
14
+ ariaActiveDescendant?: string;
15
+ } & FilterInputProps;
16
+
17
+ const FilterInput = ({
18
+ value,
19
+ onChange,
20
+ placeholder,
21
+ disabled,
22
+ inputRef,
23
+ className,
24
+ ariaControls,
25
+ ariaExpanded,
26
+ ariaActiveDescendant,
27
+ ...rest
28
+ }: FilterInputComponentProps) => {
29
+ const [theme] = useTheme();
30
+
31
+ const handleOnKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
32
+ // Let parent handle key navigation
33
+ if (
34
+ e.key === 'Escape' ||
35
+ e.key === 'Enter' ||
36
+ e.key === 'ArrowUp' ||
37
+ e.key === 'ArrowDown'
38
+ )
39
+ return;
40
+ e.stopPropagation();
41
+ };
42
+
43
+ const style = css`
44
+ flex: 1;
45
+ border: none;
46
+ outline: none;
47
+ background: transparent;
48
+ font: inherit;
49
+ color: ${theme.color.text.primary};
50
+ &::placeholder {
51
+ color: ${theme.color.text.secondary};
52
+ }
53
+ &:disabled {
54
+ color: ${theme.color.text.disabled};
55
+ cursor: not-allowed;
56
+ }
57
+ `;
58
+ return (
59
+ <input
60
+ ref={inputRef}
61
+ className={cx(style, className)}
62
+ value={value}
63
+ placeholder={placeholder}
64
+ disabled={disabled}
65
+ onChange={(e) => onChange(e.target.value)}
66
+ onClick={(e) => e.stopPropagation()}
67
+ onKeyDown={(e) => handleOnKeyDown(e)}
68
+ aria-label='Filter options'
69
+ aria-autocomplete='list'
70
+ role='searchbox'
71
+ aria-haspopup='listbox'
72
+ aria-controls={ariaControls}
73
+ aria-expanded={ariaExpanded}
74
+ aria-activedescendant={ariaActiveDescendant}
75
+ {...rest}
76
+ />
77
+ );
78
+ };
79
+
80
+ export default FilterInput;
@@ -1,7 +1,19 @@
1
1
  import { css, cx } from '@emotion/css';
2
2
  import { useTheme } from '../../../theme';
3
3
  import { dataUri as chevronDownSvgDataUri } from '../../Icon/svgs/ChevronDownSvg';
4
- import { NativeSelectProps } from '../Select.types';
4
+ import type { SelectProps } from '../Select.types';
5
+
6
+ type NativeSelectProps = Omit<
7
+ React.SelectHTMLAttributes<HTMLSelectElement>,
8
+ 'value' | 'defaultValue'
9
+ > &
10
+ Omit<
11
+ SelectProps,
12
+ 'native' | 'filterable' | 'nativeHtmlAttributes' | 'onValueChange' | 'ref'
13
+ > & {
14
+ value?: string | number;
15
+ ref?: React.Ref<HTMLSelectElement>;
16
+ };
5
17
 
6
18
  const NAME = 'ucl-uikit-select--native';
7
19
 
@@ -5,7 +5,7 @@ const NAME = 'ucl-uikit-select__panel';
5
5
 
6
6
  type PanelProps = React.ComponentPropsWithoutRef<'div'>;
7
7
 
8
- const Panel = (props: PanelProps) => {
8
+ const Panel = ({ className, ...props }: PanelProps) => {
9
9
  const [theme] = useTheme();
10
10
 
11
11
  const handleClick = (event: React.MouseEvent) => {
@@ -14,15 +14,14 @@ const Panel = (props: PanelProps) => {
14
14
  };
15
15
 
16
16
  const baseStyle = css`
17
- display: flex;
18
- flex-direction: column;
19
17
  position: absolute;
20
18
  top: 46px;
21
19
  left: -1px; // -1px to align with the border of the field
22
20
  z-index: 10; // Required: panel must be 'above' subsquent DOM elements
23
21
  min-width: 100%;
24
22
  width: fit-content;
25
- max-height: 300px;
23
+ max-width: calc(100vw - 64px);
24
+ max-height: 400px;
26
25
  overflow-y: auto;
27
26
  overflow-x: hidden;
28
27
  box-sizing: content-box;
@@ -31,7 +30,7 @@ const Panel = (props: PanelProps) => {
31
30
  background-color: ${theme.color.neutral.white};
32
31
  `;
33
32
 
34
- const style = cx(NAME, baseStyle);
33
+ const style = cx(NAME, baseStyle, className);
35
34
 
36
35
  return (
37
36
  <div