uikit-react-public 0.17.4 → 0.21.9

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 (282) hide show
  1. package/README.md +2 -4
  2. package/dist/components/Accordion/Accordion.Heading.d.ts +1 -0
  3. package/dist/components/AppHeader/AppHeader.d.ts +1 -1
  4. package/dist/components/AppHeader/AppHeaderBottom.d.ts +1 -1
  5. package/dist/components/AppHeader/AppHeaderNav.d.ts +1 -1
  6. package/dist/components/AppHeader/AppHeaderTop.d.ts +1 -1
  7. package/dist/components/Breadcrumbs/Breadcrumb.d.ts +3 -4
  8. package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +1 -1
  9. package/dist/components/Breadcrumbs/Breadcrumbs.stories.d.ts +1 -1
  10. package/dist/components/Button/Button.d.ts +7 -3
  11. package/dist/components/Button/Button.stories.d.ts +17 -7
  12. package/dist/components/Button/style/buttonAccentStyle.d.ts +4 -0
  13. package/dist/components/Button/style/buttonPrimaryDestructiveStyle.d.ts +4 -0
  14. package/dist/components/Button/style/buttonPrimaryStyle.d.ts +4 -0
  15. package/dist/components/Button/style/buttonPrimarySubtleStyle.d.ts +4 -0
  16. package/dist/components/Button/style/buttonPrimaryWarningStyle.d.ts +4 -0
  17. package/dist/components/Button/style/buttonSecondaryDestructiveStyle.d.ts +4 -0
  18. package/dist/components/Button/style/buttonSecondaryStyle.d.ts +4 -0
  19. package/dist/components/Button/style/buttonSecondarySubtleStyle.d.ts +4 -0
  20. package/dist/components/Button/style/buttonTertiaryDestructiveStyle.d.ts +4 -0
  21. package/dist/components/Button/style/buttonTertiaryNoPaddingStyle.d.ts +4 -0
  22. package/dist/components/Button/style/buttonTertiaryStyle.d.ts +4 -0
  23. package/dist/components/Checkbox/Checkbox.d.ts +1 -0
  24. package/dist/components/FooterNew/BackToTop.d.ts +8 -0
  25. package/dist/components/FooterNew/Footer.d.ts +23 -0
  26. package/dist/components/FooterNew/FooterColumn.d.ts +8 -0
  27. package/dist/components/FooterNew/FooterLinks.d.ts +7 -0
  28. package/dist/components/FooterNew/FooterNavLink.d.ts +8 -0
  29. package/dist/components/FooterNew/LegalAndCopyright.d.ts +14 -0
  30. package/dist/components/FooterNew/LogoAddressAndSocial.d.ts +10 -0
  31. package/dist/components/FooterNew/SocialLink.d.ts +8 -0
  32. package/dist/components/FooterNew/__tests__/Footer.test.d.ts +1 -0
  33. package/dist/components/FooterNew/index.d.ts +2 -0
  34. package/dist/components/HeaderNew/Header.d.ts +18 -0
  35. package/dist/components/HeaderNew/HeaderBorder.d.ts +7 -0
  36. package/dist/components/HeaderNew/HeaderLogo.d.ts +9 -0
  37. package/dist/components/HeaderNew/HeaderMenuContainer.d.ts +7 -0
  38. package/dist/components/HeaderNew/HeaderTitle.d.ts +9 -0
  39. package/dist/components/HeaderNew/__tests__/Header.test.d.ts +1 -0
  40. package/dist/components/HeaderNew/constants.d.ts +3 -0
  41. package/dist/components/HeaderNew/index.d.ts +3 -0
  42. package/dist/components/Heading/Heading.d.ts +6 -4
  43. package/dist/components/Heading/Heading.stories.d.ts +8 -6
  44. package/dist/components/Icon/svgImports.d.ts +7 -881
  45. package/dist/components/Link/BaseLink.d.ts +14 -5
  46. package/dist/components/Link/Link.d.ts +8 -3
  47. package/dist/components/Link/Link.stories.d.ts +3 -1
  48. package/dist/components/MenuNew/Menu.context.d.ts +14 -0
  49. package/dist/components/MenuNew/Menu.d.ts +20 -0
  50. package/dist/components/MenuNew/MenuContent.d.ts +9 -0
  51. package/dist/components/MenuNew/MenuItem.d.ts +10 -0
  52. package/dist/components/MenuNew/MenuSection.d.ts +7 -0
  53. package/dist/components/MenuNew/index.d.ts +6 -0
  54. package/dist/components/MenuNew/trigger/ButtonMenuTrigger.d.ts +8 -0
  55. package/dist/components/MenuNew/trigger/IconMenuTrigger.d.ts +8 -0
  56. package/dist/components/Overlay/Overlay.stories.d.ts +12 -12
  57. package/dist/components/Paragraph/Paragraph.d.ts +5 -2
  58. package/dist/components/Paragraph/Paragraph.stories.d.ts +6 -3
  59. package/dist/components/Select/Select.d.ts +2 -1
  60. package/dist/components/Select/Select.stories.d.ts +13 -1
  61. package/dist/components/Select/Select.types.d.ts +40 -13
  62. package/dist/components/Select/subcomponents/CustomSelect.d.ts +3 -3
  63. package/dist/components/Select/subcomponents/FilterInput.d.ts +3 -1
  64. package/dist/components/Select/subcomponents/NativeSelect.d.ts +2 -2
  65. package/dist/components/Select/subcomponents/VisibleField.d.ts +4 -1
  66. package/dist/components/Spinner/Spinner.d.ts +2 -0
  67. package/dist/components/StandaloneLink/StandaloneLink.d.ts +8 -5
  68. package/dist/components/StandaloneLink/StandaloneLink.stories.d.ts +3 -1
  69. package/dist/components/Table/Table.d.ts +3 -3
  70. package/dist/components/Table/Table.stories.d.ts +3 -3
  71. package/dist/components/Table/Table.types.d.ts +1 -0
  72. package/dist/components/Table/subcomponents/Cell/Cell.d.ts +5 -1
  73. package/dist/components/Table/subcomponents/Cell/Cell.stories.d.ts +15 -13
  74. package/dist/components/Table/subcomponents/Cell/CellContent.d.ts +5 -1
  75. package/dist/components/Table/subcomponents/HeadCell/HeadCell.d.ts +2 -1
  76. package/dist/components/Table/subcomponents/HeadCell/HeadCell.stories.d.ts +14 -13
  77. package/dist/components/Table/subcomponents/HeadCell/HeadCellContent.d.ts +2 -1
  78. package/dist/components/Table/subcomponents/__tests__/Row.test.d.ts +1 -0
  79. package/dist/components/UclLogoNew/UclLogo.d.ts +8 -0
  80. package/dist/components/UclLogoNew/index.d.ts +2 -0
  81. package/dist/components/index.d.ts +12 -0
  82. package/dist/index.js +20104 -15317
  83. package/dist/theme/__tests__/fonts.test.d.ts +1 -0
  84. package/dist/theme/common/themeCommon.d.ts +904 -0
  85. package/dist/theme/fonts.d.ts +18 -0
  86. package/dist/theme/index.d.ts +6 -3
  87. package/dist/theme/light/lightColour.d.ts +126 -0
  88. package/dist/theme/light/lightTheme.d.ts +3 -0
  89. package/dist/theme/original/color.d.ts +166 -0
  90. package/dist/theme/original/defaultTheme.d.ts +1340 -0
  91. package/dist/theme/original/originalColourNewStructure.d.ts +126 -0
  92. package/dist/theme/useTheme.d.ts +2174 -0
  93. package/dist/utils/addAlphaToHex.d.ts +5 -0
  94. package/dist/utils/scrollToTop.d.ts +2 -0
  95. package/lib/components/Accordion/Accordion.Heading.tsx +51 -39
  96. package/lib/components/Accordion/Accordion.Panel.tsx +0 -4
  97. package/lib/components/Accordion/Accordion.tsx +34 -28
  98. package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +12 -10
  99. package/lib/components/Alert/Alert.tsx +12 -12
  100. package/lib/components/Alert/__tests__/__snapshots__/Alert.test.tsx.snap +13 -39
  101. package/lib/components/AppHeader/AppHeader.tsx +6 -11
  102. package/lib/components/AppHeader/AppHeaderBottom.tsx +2 -3
  103. package/lib/components/AppHeader/AppHeaderNav.tsx +2 -3
  104. package/lib/components/AppHeader/AppHeaderTop.tsx +1 -1
  105. package/lib/components/AppHeader/__tests__/__snapshots__/AppHeader.test.tsx.snap +2 -2
  106. package/lib/components/AppMenu/__tests__/__snapshots__/AppMenu.test.tsx.snap +6 -19
  107. package/lib/components/Badge/Badge.stories.tsx +1 -1
  108. package/lib/components/Breadcrumbs/Breadcrumb.tsx +26 -12
  109. package/lib/components/Breadcrumbs/Breadcrumbs.tsx +1 -1
  110. package/lib/components/Breadcrumbs/__tests__/Breadcrumbs.test.tsx +9 -27
  111. package/lib/components/Breadcrumbs/__tests__/__snapshots__/Breadcrumbs.test.tsx.snap +24 -20
  112. package/lib/components/Button/Button.mdx +32 -279
  113. package/lib/components/Button/Button.stories.tsx +43 -50
  114. package/lib/components/Button/Button.tsx +165 -25
  115. package/lib/components/Button/__tests__/Button.test.tsx +49 -15
  116. package/lib/components/Button/__tests__/__snapshots__/Button.test.tsx.snap +80 -73
  117. package/lib/components/Button/style/buttonAccentStyle.ts +53 -0
  118. package/lib/components/Button/style/buttonPrimaryDestructiveStyle.ts +55 -0
  119. package/lib/components/Button/style/buttonPrimaryStyle.ts +53 -0
  120. package/lib/components/Button/style/buttonPrimarySubtleStyle.ts +64 -0
  121. package/lib/components/Button/style/buttonPrimaryWarningStyle.ts +56 -0
  122. package/lib/components/Button/style/buttonSecondaryDestructiveStyle.ts +63 -0
  123. package/lib/components/Button/style/buttonSecondaryStyle.ts +62 -0
  124. package/lib/components/Button/style/buttonSecondarySubtleStyle.ts +72 -0
  125. package/lib/components/Button/style/buttonTertiaryDestructiveStyle.ts +65 -0
  126. package/lib/components/Button/style/buttonTertiaryNoPaddingStyle.ts +52 -0
  127. package/lib/components/Button/style/buttonTertiaryStyle.ts +62 -0
  128. package/lib/components/Calendar/Calendar.stories.tsx +33 -13
  129. package/lib/components/Calendar/Calendar.tsx +2 -2
  130. package/lib/components/Calendar/__tests__/__snapshots__/Calendar.test.tsx.snap +99 -95
  131. package/lib/components/Calendar/subcomponents/AcademicWeek.tsx +2 -1
  132. package/lib/components/Calendar/subcomponents/AcademicWeeks.tsx +2 -3
  133. package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +3 -7
  134. package/lib/components/Calendar/subcomponents/Controls.tsx +1 -1
  135. package/lib/components/Calendar/subcomponents/Day.stories.tsx +1 -1
  136. package/lib/components/Calendar/subcomponents/Day.tsx +7 -9
  137. package/lib/components/Calendar/subcomponents/EventDot.tsx +4 -8
  138. package/lib/components/Checkbox/Checkbox.stories.tsx +1 -1
  139. package/lib/components/Checkbox/Checkbox.tsx +12 -10
  140. package/lib/components/Checkbox/__tests__/Checkbox.test.tsx +29 -0
  141. package/lib/components/Checkbox/__tests__/__snapshots__/Checkbox.test.tsx.snap +4 -4
  142. package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +117 -0
  143. package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +16 -44
  144. package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +10 -1
  145. package/lib/components/Datepicker/subcomponents/VisibleField.tsx +24 -23
  146. package/lib/components/Dialog/BaseDialog.tsx +2 -2
  147. package/lib/components/Dialog/Dialog.stories.tsx +1 -1
  148. package/lib/components/Divider/__tests__/__snapshots__/Breadcrumbs.test.tsx.snap +12 -12
  149. package/lib/components/FeedbackDialog/FeedbackDialog.stories.tsx +1 -1
  150. package/lib/components/FeedbackDialog/FeedbackDialog.tsx +4 -6
  151. package/lib/components/Field/CharacterCount.tsx +2 -2
  152. package/lib/components/Field/ErrorText.tsx +1 -1
  153. package/lib/components/Field/Field.tsx +1 -1
  154. package/lib/components/Field/HelperText.tsx +3 -1
  155. package/lib/components/FileInput/FileInput.stories.tsx +1 -1
  156. package/lib/components/FileInput/__tests__/__snapshots__/FileInput.test.tsx.snap +4 -20
  157. package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +70 -79
  158. package/lib/components/FooterNew/BackToTop.tsx +83 -0
  159. package/lib/components/FooterNew/Footer.tsx +110 -0
  160. package/lib/components/FooterNew/FooterColumn.tsx +79 -0
  161. package/lib/components/FooterNew/FooterLinks.tsx +44 -0
  162. package/lib/components/FooterNew/FooterNavLink.tsx +63 -0
  163. package/lib/components/FooterNew/LegalAndCopyright.tsx +150 -0
  164. package/lib/components/FooterNew/LogoAddressAndSocial.tsx +154 -0
  165. package/lib/components/FooterNew/SocialLink.tsx +108 -0
  166. package/lib/components/FooterNew/__tests__/Footer.test.tsx +51 -0
  167. package/lib/components/FooterNew/__tests__/__snapshots__/Footer.test.tsx.snap +1107 -0
  168. package/lib/components/FooterNew/index.ts +2 -0
  169. package/lib/components/HeaderDraft/__tests__/__snapshots__/Header.test.tsx.snap +3 -2
  170. package/lib/components/HeaderNew/Header.tsx +93 -0
  171. package/lib/components/HeaderNew/HeaderBorder.tsx +55 -0
  172. package/lib/components/HeaderNew/HeaderLogo.tsx +70 -0
  173. package/lib/components/HeaderNew/HeaderMenuContainer.tsx +35 -0
  174. package/lib/components/HeaderNew/HeaderTitle.tsx +53 -0
  175. package/lib/components/HeaderNew/__tests__/Header.test.tsx +42 -0
  176. package/lib/components/HeaderNew/__tests__/__snapshots__/Header.test.tsx.snap +79 -0
  177. package/lib/components/HeaderNew/constants.ts +3 -0
  178. package/lib/components/HeaderNew/index.ts +7 -0
  179. package/lib/components/Heading/Heading.stories.tsx +34 -41
  180. package/lib/components/Heading/Heading.tsx +180 -49
  181. package/lib/components/Heading/__tests__/__snapshots__/Heading.test.tsx.snap +4 -4
  182. package/lib/components/Icon/__tests__/__snapshots__/Icon.test.tsx.snap +16 -12
  183. package/lib/components/Icon/svgImports.ts +318 -296
  184. package/lib/components/IconButton/IconButton.tsx +3 -4
  185. package/lib/components/IconButton/__tests__/__snapshots__/IconButton.test.tsx.snap +12 -9
  186. package/lib/components/Link/BaseLink.tsx +114 -71
  187. package/lib/components/Link/Link.stories.tsx +1 -1
  188. package/lib/components/Link/Link.tsx +120 -109
  189. package/lib/components/Link/__tests__/__snapshots__/link.test.tsx.snap +2 -2
  190. package/lib/components/MenuNew/Menu.context.tsx +149 -0
  191. package/lib/components/MenuNew/Menu.tsx +75 -0
  192. package/lib/components/MenuNew/MenuContent.tsx +140 -0
  193. package/lib/components/MenuNew/MenuItem.tsx +101 -0
  194. package/lib/components/MenuNew/MenuSection.tsx +47 -0
  195. package/lib/components/MenuNew/index.ts +8 -0
  196. package/lib/components/MenuNew/trigger/ButtonMenuTrigger.tsx +42 -0
  197. package/lib/components/MenuNew/trigger/IconMenuTrigger.tsx +40 -0
  198. package/lib/components/Pagination/Pagination.stories.tsx +1 -1
  199. package/lib/components/Pagination/PaginationControls.tsx +4 -5
  200. package/lib/components/Pagination/PaginationInfo.tsx +2 -3
  201. package/lib/components/Paragraph/Paragraph.stories.tsx +29 -27
  202. package/lib/components/Paragraph/Paragraph.tsx +212 -81
  203. package/lib/components/Paragraph/__tests__/__snapshots__/Paragraph.test.tsx.snap +5 -5
  204. package/lib/components/Radio/Radio.stories.tsx +1 -1
  205. package/lib/components/Radio/Radio.tsx +8 -8
  206. package/lib/components/Radio/__tests__/__snapshots__/Radio.test.tsx.snap +4 -4
  207. package/lib/components/Search/__tests__/__snapshots__/Search.test.tsx.snap +12 -32
  208. package/lib/components/Select/Select.mdx +23 -0
  209. package/lib/components/Select/Select.stories.tsx +43 -10
  210. package/lib/components/Select/Select.tsx +14 -3
  211. package/lib/components/Select/Select.types.ts +53 -16
  212. package/lib/components/Select/__tests__/Select.test.tsx +250 -1
  213. package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +5 -4
  214. package/lib/components/Select/subcomponents/CustomOption.tsx +10 -3
  215. package/lib/components/Select/subcomponents/CustomSelect.tsx +110 -10
  216. package/lib/components/Select/subcomponents/FilterInput.tsx +13 -3
  217. package/lib/components/Select/subcomponents/NativeSelect.tsx +10 -18
  218. package/lib/components/Select/subcomponents/Panel.tsx +2 -2
  219. package/lib/components/Select/subcomponents/VisibleField.tsx +48 -3
  220. package/lib/components/Snackbar/__tests__/__snapshots__/Snackbar.test.tsx.snap +9 -15
  221. package/lib/components/Spinner/Spinner.tsx +24 -5
  222. package/lib/components/Spinner/__tests__/Spinner.test.tsx +35 -5
  223. package/lib/components/Spinner/__tests__/__snapshots__/Spinner.test.tsx.snap +40 -16
  224. package/lib/components/StandaloneLink/StandaloneLink.stories.tsx +1 -1
  225. package/lib/components/StandaloneLink/StandaloneLink.tsx +180 -163
  226. package/lib/components/StandaloneLink/__tests__/__snapshots__/StandaloneLink.test.tsx.snap +2 -2
  227. package/lib/components/Table/Table.stories.tsx +1 -1
  228. package/lib/components/Table/Table.tsx +2 -0
  229. package/lib/components/Table/Table.types.ts +1 -0
  230. package/lib/components/Table/__tests__/Table.test.tsx +19 -0
  231. package/lib/components/Table/__tests__/__snapshots__/Table.test.tsx.snap +7 -3
  232. package/lib/components/Table/subcomponents/Cell/Cell.stories.tsx +1 -1
  233. package/lib/components/Table/subcomponents/Cell/Cell.tsx +23 -2
  234. package/lib/components/Table/subcomponents/Cell/CellContent.tsx +12 -1
  235. package/lib/components/Table/subcomponents/Cell/__tests__/Cell.test.tsx +106 -0
  236. package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +4 -3
  237. package/lib/components/Table/subcomponents/HeadCell/HeadCell.stories.tsx +1 -1
  238. package/lib/components/Table/subcomponents/HeadCell/HeadCell.tsx +28 -6
  239. package/lib/components/Table/subcomponents/HeadCell/HeadCellContent.tsx +3 -0
  240. package/lib/components/Table/subcomponents/HeadCell/__tests__/HeadCell.test.tsx +221 -2
  241. package/lib/components/Table/subcomponents/HeadCell/__tests__/__snapshots__/HeadCell.test.tsx.snap +6 -4
  242. package/lib/components/Table/subcomponents/Row.tsx +2 -2
  243. package/lib/components/Table/subcomponents/SortIcon.tsx +1 -0
  244. package/lib/components/Table/subcomponents/__tests__/Row.test.tsx +59 -0
  245. package/lib/components/Tabs/Tab.tsx +3 -3
  246. package/lib/components/Tabs/Tabs.stories.tsx +1 -1
  247. package/lib/components/Tabs/Tabs.tsx +5 -3
  248. package/lib/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap +4 -4
  249. package/lib/components/Timepicker/Timepicker.stories.tsx +1 -1
  250. package/lib/components/Toggle/Toggle.tsx +5 -5
  251. package/lib/components/Toggle/ToggleHandle.tsx +2 -3
  252. package/lib/components/Tooltip/Tooltip.tsx +2 -2
  253. package/lib/components/Tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap +2 -2
  254. package/lib/components/UclLogoNew/UclLogo.tsx +42 -0
  255. package/lib/components/UclLogoNew/index.ts +2 -0
  256. package/lib/components/WeekPicker/WeekPicker.stories.tsx +3 -5
  257. package/lib/components/common/Common.mdx +0 -1
  258. package/lib/components/index.ts +19 -1
  259. package/lib/theme/Colours.mdx +1 -1
  260. package/lib/theme/Theme.mdx +1 -1
  261. package/lib/theme/Typography.mdx +1 -1
  262. package/lib/theme/__tests__/fonts.test.ts +37 -0
  263. package/lib/theme/common/themeCommon.ts +515 -0
  264. package/lib/theme/fonts.ts +110 -0
  265. package/lib/theme/index.ts +6 -6
  266. package/lib/theme/light/lightColour.ts +232 -0
  267. package/lib/theme/light/lightTheme.ts +37 -0
  268. package/lib/theme/{defaultTheme.ts → original/color.ts} +17 -199
  269. package/lib/theme/original/defaultTheme.ts +207 -0
  270. package/lib/theme/original/originalColourNewStructure.ts +185 -0
  271. package/lib/theme/useTheme.tsx +72 -15
  272. package/lib/types/assets.d.ts +10 -0
  273. package/lib/utils/addAlphaToHex.ts +29 -0
  274. package/lib/utils/scrollToTop.ts +5 -0
  275. package/package.json +11 -6
  276. package/dist/components/Button/buttonPrimaryStyle.d.ts +0 -4
  277. package/dist/components/Button/buttonSecondaryStyle.d.ts +0 -4
  278. package/dist/components/Button/buttonTertiaryStyle.d.ts +0 -4
  279. package/dist/theme/defaultTheme.d.ts +0 -274
  280. package/lib/components/Button/buttonPrimaryStyle.ts +0 -62
  281. package/lib/components/Button/buttonSecondaryStyle.ts +0 -65
  282. package/lib/components/Button/buttonTertiaryStyle.ts +0 -54
@@ -2,12 +2,24 @@ import { useState, useRef, useEffect, useMemo, useId } from 'react';
2
2
  import { css, cx } from '@emotion/css';
3
3
  import { VisibleField, Panel, CustomOption, FilterInput } from '.';
4
4
  import { useTheme } from '../../../theme';
5
- import type { SelectProps } from '../Select.types';
5
+ import type { InternalSelectProps } from '../Select.types';
6
6
 
7
7
  const NAME = 'ucl-uikit-select';
8
+ const FOCUSABLE_ELEMENTS = [
9
+ 'a[href]',
10
+ 'button:not([disabled])',
11
+ 'textarea:not([disabled])',
12
+ 'input:not([disabled])',
13
+ 'select:not([disabled])',
14
+ '[tabindex]:not([tabindex="-1"])',
15
+ '[contenteditable="true"]',
16
+ 'audio[controls]',
17
+ 'video[controls]',
18
+ 'details > summary:first-of-type',
19
+ ].join(', ');
8
20
 
9
21
  type CustomSelectProps<T> = Omit<
10
- SelectProps<T>,
22
+ InternalSelectProps<T>,
11
23
  'native' | 'nativeHtmlAttributes'
12
24
  >;
13
25
 
@@ -17,6 +29,7 @@ const CustomSelect = <T extends string | number>({
17
29
  options = [],
18
30
  onValueChange,
19
31
  disabled,
32
+ clearable = false,
20
33
  placeholder,
21
34
  lineBreak = false,
22
35
  filterInputProps,
@@ -95,6 +108,7 @@ const CustomSelect = <T extends string | number>({
95
108
  null
96
109
  );
97
110
  const filterInputRef = useRef<HTMLInputElement | null>(null);
111
+ const clearButtonRef = useRef<HTMLButtonElement | null>(null);
98
112
  const reactId = useId();
99
113
  const idBase = props.id ?? `${testId}-${reactId.replace(/[:]/g, '')}`;
100
114
  const listboxId = `${idBase}-listbox`;
@@ -161,6 +175,79 @@ const CustomSelect = <T extends string | number>({
161
175
  };
162
176
  }, [effectiveRef]);
163
177
 
178
+ const handleBlur = (event: React.FocusEvent<HTMLElement>) => {
179
+ const nextFocusedElement = event.relatedTarget as Node | null;
180
+ if (
181
+ effectiveRef.current &&
182
+ nextFocusedElement &&
183
+ effectiveRef.current.contains(nextFocusedElement)
184
+ ) {
185
+ return;
186
+ }
187
+ closePanel();
188
+ };
189
+
190
+ const isFocusableElement = (element: HTMLElement) => {
191
+ if (!element.isConnected) return false;
192
+ if (element instanceof HTMLInputElement && element.type === 'hidden') {
193
+ return false;
194
+ }
195
+ if (element.tabIndex < 0) return false;
196
+ if ('disabled' in element && element.disabled) return false;
197
+
198
+ const style = window.getComputedStyle(element);
199
+ if (style.display === 'none' || style.visibility === 'hidden') {
200
+ return false;
201
+ }
202
+
203
+ return true;
204
+ };
205
+
206
+ const moveFocusOutsideSelect = (isReverse: boolean) => {
207
+ if (!effectiveRef.current) return false;
208
+
209
+ const focusableElements = Array.from(
210
+ document.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENTS)
211
+ ).filter(isFocusableElement);
212
+
213
+ const currentElement = filterInputRef.current;
214
+ if (!currentElement) return false;
215
+
216
+ const currentIndex = focusableElements.indexOf(currentElement);
217
+ if (currentIndex === -1) return false;
218
+
219
+ const step = isReverse ? -1 : 1;
220
+ let nextIndex = currentIndex + step;
221
+
222
+ while (nextIndex >= 0 && nextIndex < focusableElements.length) {
223
+ const candidate = focusableElements[nextIndex];
224
+ if (!effectiveRef.current.contains(candidate)) {
225
+ candidate.focus();
226
+ if (document.activeElement === candidate) {
227
+ return true;
228
+ }
229
+ }
230
+ nextIndex += step;
231
+ }
232
+
233
+ return false;
234
+ };
235
+
236
+ const handleFilterTabOut = (event: React.KeyboardEvent<HTMLInputElement>) => {
237
+ closePanel();
238
+ // If this widget gains more tabbable descendants, revisit this handoff.
239
+ if (!event.shiftKey && clearButtonRef.current) {
240
+ clearButtonRef.current.focus();
241
+ if (document.activeElement === clearButtonRef.current) {
242
+ event.preventDefault();
243
+ return;
244
+ }
245
+ }
246
+ if (moveFocusOutsideSelect(event.shiftKey)) {
247
+ event.preventDefault();
248
+ }
249
+ };
250
+
164
251
  useEffect(() => {
165
252
  // Close the dropdown if it becomes disabled
166
253
  if (disabled && isOpen) setIsOpen(false);
@@ -211,6 +298,17 @@ const CustomSelect = <T extends string | number>({
211
298
  closePanel();
212
299
  };
213
300
 
301
+ const handleClear = (event: React.MouseEvent | React.KeyboardEvent) => {
302
+ if (disabled) return;
303
+ event.stopPropagation();
304
+ setSelectedOptionIndex(null);
305
+ setActiveOptionIndex(null);
306
+ setFilterText('');
307
+ closePanel();
308
+ effectiveRef.current?.focus();
309
+ onValueChange?.(null, event);
310
+ };
311
+
214
312
  // Used by <CustomOption> and passed as prop
215
313
  const handleSelect = (
216
314
  event: React.MouseEvent,
@@ -393,18 +491,14 @@ const CustomSelect = <T extends string | number>({
393
491
  height: 48px;
394
492
  box-sizing: border-box;
395
493
  padding: 0 16px;
396
- background-color: ${theme.color.neutral.white};
397
- color: ${theme.color.text.primary};
494
+ background-color: ${theme.colour.fill.inverse};
495
+ color: ${theme.colour.text.default};
398
496
  font-family: ${theme.font.family.primary};
399
497
  font-size: ${theme.font.size.f16};
400
- border: ${theme.border.b1} solid ${theme.color.neutral.grey60};
498
+ border: ${theme.border.b1} solid ${theme.colour.speciality.inputDefault};
401
499
  cursor: pointer;
402
500
  user-select: none;
403
501
 
404
- &:hover {
405
- ${!isOpen && `background-color: ${theme.color.neutral.grey5};`}
406
- }
407
-
408
502
  &:focus-visible,
409
503
  &:focus-within {
410
504
  outline: none;
@@ -434,7 +528,8 @@ const CustomSelect = <T extends string | number>({
434
528
  onClick={handleClick}
435
529
  onKeyDown={handleKeyDown}
436
530
  onFocus={handleFocus}
437
- tabIndex={disabled ? -1 : 0}
531
+ onBlur={handleBlur}
532
+ tabIndex={disabled || (filterable && isOpen) ? -1 : 0}
438
533
  className={style}
439
534
  data-testid={testId}
440
535
  ref={setRefs}
@@ -451,6 +546,9 @@ const CustomSelect = <T extends string | number>({
451
546
  placeholder={placeholder}
452
547
  disabled={disabled}
453
548
  filterable={filterable}
549
+ clearable={clearable}
550
+ onClear={handleClear}
551
+ clearButtonRef={clearButtonRef}
454
552
  >
455
553
  {filterable && (
456
554
  <FilterInput
@@ -459,6 +557,8 @@ const CustomSelect = <T extends string | number>({
459
557
  placeholder={placeholder}
460
558
  disabled={disabled}
461
559
  inputRef={filterInputRef}
560
+ onBlur={handleBlur}
561
+ onTabOut={handleFilterTabOut}
462
562
  ariaControls={listboxId}
463
563
  ariaExpanded={isOpen}
464
564
  ariaActiveDescendant={activeDescendantId}
@@ -5,6 +5,8 @@ import type { FilterInputProps } from '../Select.types';
5
5
  type FilterInputComponentProps = {
6
6
  value: string;
7
7
  onChange: (value: string) => void;
8
+ onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
9
+ onTabOut?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
8
10
  placeholder?: string;
9
11
  disabled?: boolean;
10
12
  inputRef?: React.RefObject<HTMLInputElement | null>;
@@ -17,6 +19,8 @@ type FilterInputComponentProps = {
17
19
  const FilterInput = ({
18
20
  value,
19
21
  onChange,
22
+ onBlur,
23
+ onTabOut,
20
24
  placeholder,
21
25
  disabled,
22
26
  inputRef,
@@ -29,6 +33,11 @@ const FilterInput = ({
29
33
  const [theme] = useTheme();
30
34
 
31
35
  const handleOnKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
36
+ if (e.key === 'Tab') {
37
+ onTabOut?.(e);
38
+ return;
39
+ }
40
+
32
41
  // Let parent handle key navigation
33
42
  if (
34
43
  e.key === 'Escape' ||
@@ -46,12 +55,12 @@ const FilterInput = ({
46
55
  outline: none;
47
56
  background: transparent;
48
57
  font: inherit;
49
- color: ${theme.color.text.primary};
58
+ color: ${theme.colour.text.default};
50
59
  &::placeholder {
51
- color: ${theme.color.text.secondary};
60
+ color: ${theme.colour.text.tertiary};
52
61
  }
53
62
  &:disabled {
54
- color: ${theme.color.text.disabled};
63
+ color: ${theme.colour.text.disabled};
55
64
  cursor: not-allowed;
56
65
  }
57
66
  `;
@@ -63,6 +72,7 @@ const FilterInput = ({
63
72
  placeholder={placeholder}
64
73
  disabled={disabled}
65
74
  onChange={(e) => onChange(e.target.value)}
75
+ onBlur={onBlur}
66
76
  onClick={(e) => e.stopPropagation()}
67
77
  onKeyDown={(e) => handleOnKeyDown(e)}
68
78
  aria-label='Filter options'
@@ -1,14 +1,14 @@
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 type { SelectProps } from '../Select.types';
4
+ import type { InternalSelectProps } from '../Select.types';
5
5
 
6
6
  type NativeSelectProps = Omit<
7
7
  React.SelectHTMLAttributes<HTMLSelectElement>,
8
8
  'value' | 'defaultValue'
9
9
  > &
10
10
  Omit<
11
- SelectProps,
11
+ InternalSelectProps,
12
12
  'native' | 'filterable' | 'nativeHtmlAttributes' | 'onValueChange' | 'ref'
13
13
  > & {
14
14
  value?: string | number;
@@ -29,8 +29,8 @@ const NativeSelect = ({
29
29
  const [theme] = useTheme();
30
30
 
31
31
  const chevronColour = disabled
32
- ? theme.color.neutral.grey20
33
- : theme.color.interaction.blue70;
32
+ ? theme.colour.icon.disabled
33
+ : theme.colour.icon.default;
34
34
  const chevronDownSvg = chevronDownSvgDataUri(chevronColour);
35
35
 
36
36
  const baseStyle = css`
@@ -39,9 +39,9 @@ const NativeSelect = ({
39
39
  line-height: ${theme.font.lineHeight.h150};
40
40
  font-family: ${theme.font.family.primary};
41
41
  font-size: ${theme.font.size.f16};
42
- background-color: ${theme.color.neutral.white};
43
- border: ${theme.border.b1} solid ${theme.color.neutral.grey60};
44
- color: ${theme.color.text.primary};
42
+ background-color: ${theme.colour.fill.inverse};
43
+ border: ${theme.border.b1} solid ${theme.colour.border.secondary};
44
+ color: ${theme.colour.text.default};
45
45
  appearance: none;
46
46
  -webkit-appearance: none;
47
47
  -moz-appearance: none;
@@ -58,13 +58,6 @@ const NativeSelect = ({
58
58
  width: ${width}px;
59
59
  `;
60
60
 
61
- const hoverStyle = css`
62
- &:hover {
63
- border-color: ${theme.color.neutral.grey60};
64
- background-color: ${theme.color.neutral.grey5};
65
- }
66
- `;
67
-
68
61
  const focusStyle = css`
69
62
  &:focus-visible {
70
63
  box-shadow: ${theme.boxShadow.focus};
@@ -72,13 +65,13 @@ const NativeSelect = ({
72
65
  `;
73
66
 
74
67
  const disabledStyle = css`
75
- color: ${theme.color.text.disabled};
76
- border: ${theme.border.b1} solid ${theme.color.neutral.grey20};
68
+ color: ${theme.colour.text.disabled};
69
+ border: ${theme.border.b1} solid ${theme.colour.border.disabled};
77
70
  opacity: 1; // Override user-agent default
78
71
  cursor: not-allowed;
79
72
 
80
73
  &:hover {
81
- background-color: ${theme.color.neutral.white};
74
+ background-color: ${theme.colour.fill.inverse};
82
75
  }
83
76
  `;
84
77
 
@@ -86,7 +79,6 @@ const NativeSelect = ({
86
79
  NAME,
87
80
  baseStyle,
88
81
  !!width && widthStyle,
89
- !disabled && hoverStyle,
90
82
  !disabled && focusStyle,
91
83
  disabled && disabledStyle,
92
84
  className
@@ -26,8 +26,8 @@ const Panel = ({ className, ...props }: PanelProps) => {
26
26
  overflow-x: hidden;
27
27
  box-sizing: content-box;
28
28
  padding: ${theme.padding.p8} 0;
29
- border: ${theme.border.b1} solid ${theme.color.neutral.grey20};
30
- background-color: ${theme.color.neutral.white};
29
+ border: ${theme.border.b1} solid ${theme.colour.border.subtle};
30
+ background-color: ${theme.colour.fill.inverse};
31
31
  `;
32
32
 
33
33
  const style = cx(NAME, baseStyle, className);
@@ -11,6 +11,9 @@ interface VisibleFieldProps<T> {
11
11
  selectedOption: OptionData<T> | null | undefined;
12
12
  placeholder?: string;
13
13
  filterable?: boolean;
14
+ clearable?: boolean;
15
+ onClear?: (event: React.MouseEvent | React.KeyboardEvent) => void;
16
+ clearButtonRef?: React.RefObject<HTMLButtonElement | null>;
14
17
  children?: React.ReactNode;
15
18
  }
16
19
 
@@ -20,6 +23,9 @@ const VisibleField = <T extends string | number>({
20
23
  placeholder,
21
24
  disabled,
22
25
  filterable,
26
+ clearable = false,
27
+ onClear,
28
+ clearButtonRef,
23
29
  children,
24
30
  }: VisibleFieldProps<T>) => {
25
31
  const [theme] = useTheme();
@@ -44,7 +50,7 @@ const VisibleField = <T extends string | number>({
44
50
  const chevronIconBaseStyle = css`
45
51
  flex-shrink: 0;
46
52
  margin-left: auto;
47
- color: ${theme.color.interaction.blue70};
53
+ color: ${theme.colour.icon.default};
48
54
  `;
49
55
 
50
56
  const chevronIconOpenStyle = css`
@@ -52,7 +58,7 @@ const VisibleField = <T extends string | number>({
52
58
  `;
53
59
 
54
60
  const chevronIconDisabledStyle = css`
55
- color: ${theme.color.neutral.grey20};
61
+ color: ${theme.colour.icon.disabled};
56
62
  `;
57
63
 
58
64
  const chevronIconStyle = cx(
@@ -61,6 +67,26 @@ const VisibleField = <T extends string | number>({
61
67
  disabled && chevronIconDisabledStyle
62
68
  );
63
69
 
70
+ const clearButtonStyle = css`
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ flex-shrink: 0;
75
+ margin-left: auto;
76
+ padding: 0;
77
+ border: none;
78
+ background: transparent;
79
+ color: ${theme.colour.icon.default};
80
+ cursor: pointer;
81
+
82
+ &:focus-visible {
83
+ outline: none;
84
+ box-shadow: ${theme.boxShadow.focus};
85
+ }
86
+ `;
87
+
88
+ const showClearButton = clearable && !!selectedOption && !disabled;
89
+
64
90
  return (
65
91
  <div
66
92
  className={style}
@@ -73,7 +99,26 @@ const VisibleField = <T extends string | number>({
73
99
  {selectedOption ? selectedOption.label : placeholder || ''}
74
100
  </span>
75
101
  )}
76
- <Icon.ChevronDown className={chevronIconStyle} />
102
+ {showClearButton ? (
103
+ <button
104
+ ref={clearButtonRef}
105
+ type='button'
106
+ className={clearButtonStyle}
107
+ aria-label='Clear selection'
108
+ data-testid={`${NAME}-clear-button`}
109
+ onClick={(event) => {
110
+ event.stopPropagation();
111
+ onClear?.(event);
112
+ }}
113
+ onKeyDown={(event) => {
114
+ event.stopPropagation();
115
+ }}
116
+ >
117
+ <Icon.X />
118
+ </button>
119
+ ) : (
120
+ <Icon.ChevronDown className={chevronIconStyle} />
121
+ )}
77
122
  </div>
78
123
  );
79
124
  };
@@ -9,6 +9,7 @@ exports[`Snackbar > snapshot: custom test id 1`] = `
9
9
  class="ucl-uikit-icon css-1uzxprk"
10
10
  data-testid="ucl-uikit-icon"
11
11
  fill="none"
12
+ focusable="false"
12
13
  height="24"
13
14
  stroke="currentColor"
14
15
  stroke-linecap="round"
@@ -21,8 +22,8 @@ exports[`Snackbar > snapshot: custom test id 1`] = `
21
22
  <path
22
23
  d="M22 11.08V12a10 10 0 1 1-5.93-9.14"
23
24
  />
24
- <polyline
25
- points="22 4 12 14.01 9 11.01"
25
+ <path
26
+ d="M22 4 12 14.01l-3-3"
26
27
  />
27
28
  </svg>
28
29
  <span>
@@ -50,6 +51,7 @@ exports[`Snackbar > snapshot: minimal props 1`] = `
50
51
  class="ucl-uikit-icon css-1uzxprk"
51
52
  data-testid="ucl-uikit-icon"
52
53
  fill="none"
54
+ focusable="false"
53
55
  height="24"
54
56
  stroke="currentColor"
55
57
  stroke-linecap="round"
@@ -62,8 +64,8 @@ exports[`Snackbar > snapshot: minimal props 1`] = `
62
64
  <path
63
65
  d="M22 11.08V12a10 10 0 1 1-5.93-9.14"
64
66
  />
65
- <polyline
66
- points="22 4 12 14.01 9 11.01"
67
+ <path
68
+ d="M22 4 12 14.01l-3-3"
67
69
  />
68
70
  </svg>
69
71
  <span>
@@ -80,6 +82,7 @@ exports[`Snackbar > snapshot: minimal props 1`] = `
80
82
  class="ucl-uikit-icon css-148hpxb"
81
83
  data-testid="ucl-uikit-icon"
82
84
  fill="none"
85
+ focusable="false"
83
86
  height="24"
84
87
  stroke="currentColor"
85
88
  stroke-linecap="round"
@@ -89,17 +92,8 @@ exports[`Snackbar > snapshot: minimal props 1`] = `
89
92
  width="24"
90
93
  xmlns="http://www.w3.org/2000/svg"
91
94
  >
92
- <line
93
- x1="18"
94
- x2="6"
95
- y1="6"
96
- y2="18"
97
- />
98
- <line
99
- x1="6"
100
- x2="18"
101
- y1="6"
102
- y2="18"
95
+ <path
96
+ d="M18 6 6 18M6 6l12 12"
103
97
  />
104
98
  </svg>
105
99
  </button>
@@ -1,17 +1,36 @@
1
1
  import { SVGAttributes, forwardRef, memo } from 'react';
2
2
  import { css, cx } from '@emotion/css';
3
+ import { useTheme } from '../../theme';
3
4
 
4
5
  export const NAME = 'ucl-uikit-spinner';
5
6
 
6
7
  export interface SpinnerProps extends SVGAttributes<SVGSVGElement> {
7
8
  size?: number;
9
+ strokeWidth?: number;
8
10
  testId?: string;
11
+ inheritColour?: boolean;
9
12
  }
10
13
 
11
14
  const Spinner = forwardRef<SVGSVGElement, SpinnerProps>(
12
- ({ size = 24, className, testId = NAME, ...rest }: SpinnerProps, ref) => {
13
- const rotationDuration = `2s`; // rotation duration
14
- const dashDuration = `1.5s`; // dash duration
15
+ (
16
+ {
17
+ size = 72,
18
+ strokeWidth = 4,
19
+ className,
20
+ testId = NAME,
21
+ inheritColour = false,
22
+ ...rest
23
+ }: SpinnerProps,
24
+ ref
25
+ ) => {
26
+ const [theme] = useTheme();
27
+
28
+ const rotationDuration = `3s`; // rotation duration
29
+ const dashDuration = `2s`; // dash duration
30
+
31
+ const baseStyle = css`
32
+ color: ${inheritColour ? 'inherit' : theme.colour.icon.brand};
33
+ `;
15
34
 
16
35
  const gStyle = css`
17
36
  transform-origin: 50% 50%;
@@ -44,7 +63,7 @@ const Spinner = forwardRef<SVGSVGElement, SpinnerProps>(
44
63
  }
45
64
  `;
46
65
 
47
- const style = cx(NAME, className);
66
+ const style = cx(NAME, baseStyle, className);
48
67
 
49
68
  return (
50
69
  <svg
@@ -64,7 +83,7 @@ const Spinner = forwardRef<SVGSVGElement, SpinnerProps>(
64
83
  r='20'
65
84
  fill='none'
66
85
  stroke='currentColor'
67
- strokeWidth={8}
86
+ strokeWidth={strokeWidth}
68
87
  />
69
88
  </g>
70
89
  </svg>
@@ -2,17 +2,26 @@ import { describe, expect, test } from 'vitest';
2
2
  import { render } from '@testing-library/react';
3
3
  import { css } from '@emotion/css';
4
4
  import SpinnerSvg from '../Spinner';
5
+ import { ThemeContextProvider } from '../../..';
5
6
 
6
7
  describe('SpinnerSvg', () => {
7
8
  // Snapshot tests
8
9
 
9
10
  test('snapshot: default props', () => {
10
- const renderResult = render(<SpinnerSvg />);
11
+ const renderResult = render(
12
+ <ThemeContextProvider>
13
+ <SpinnerSvg />
14
+ </ThemeContextProvider>
15
+ );
11
16
  expect(renderResult.container.firstChild).toMatchSnapshot();
12
17
  });
13
18
 
14
19
  test('snapshot: custom size', () => {
15
- const renderResult = render(<SpinnerSvg size={32} />);
20
+ const renderResult = render(
21
+ <ThemeContextProvider>
22
+ <SpinnerSvg size={32} />
23
+ </ThemeContextProvider>
24
+ );
16
25
  expect(renderResult.container.firstChild).toMatchSnapshot();
17
26
  });
18
27
 
@@ -20,7 +29,20 @@ describe('SpinnerSvg', () => {
20
29
  const style = css`
21
30
  color: #0d68cf;
22
31
  `;
23
- const renderResult = render(<SpinnerSvg className={style} />);
32
+ const renderResult = render(
33
+ <ThemeContextProvider>
34
+ <SpinnerSvg className={style} />
35
+ </ThemeContextProvider>
36
+ );
37
+ expect(renderResult.container.firstChild).toMatchSnapshot();
38
+ });
39
+
40
+ test('snapshot: inherits parent colour when inheritColour is true', () => {
41
+ const renderResult = render(
42
+ <ThemeContextProvider>
43
+ <SpinnerSvg inheritColour />
44
+ </ThemeContextProvider>
45
+ );
24
46
  expect(renderResult.container.firstChild).toMatchSnapshot();
25
47
  });
26
48
 
@@ -28,13 +50,21 @@ describe('SpinnerSvg', () => {
28
50
 
29
51
  test('Can be found with default testId', () => {
30
52
  const defaultTestId = 'ucl-uikit-spinner';
31
- const { getByTestId } = render(<SpinnerSvg testId={defaultTestId} />);
53
+ const { getByTestId } = render(
54
+ <ThemeContextProvider>
55
+ <SpinnerSvg testId={defaultTestId} />
56
+ </ThemeContextProvider>
57
+ );
32
58
  const svg = getByTestId(defaultTestId);
33
59
  expect(svg).toBeDefined();
34
60
  });
35
61
 
36
62
  test('test ID: custom', () => {
37
- const { getByTestId } = render(<SpinnerSvg testId='custom-test-id' />);
63
+ const { getByTestId } = render(
64
+ <ThemeContextProvider>
65
+ <SpinnerSvg testId='custom-test-id' />
66
+ </ThemeContextProvider>
67
+ );
38
68
  const svg = getByTestId('custom-test-id');
39
69
  expect(svg).toBeDefined();
40
70
  });