uikit-react-public 0.17.4 → 0.21.8

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 (280) 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/HeadingNew/Heading.d.ts +13 -0
  43. package/dist/components/HeadingNew/index.d.ts +2 -0
  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/ParagraphNew/Paragraph.d.ts +13 -0
  58. package/dist/components/ParagraphNew/index.d.ts +4 -0
  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 +20909 -16022
  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/HeadingNew/Heading.tsx +208 -0
  180. package/lib/components/HeadingNew/index.ts +2 -0
  181. package/lib/components/Icon/__tests__/__snapshots__/Icon.test.tsx.snap +16 -12
  182. package/lib/components/Icon/svgImports.ts +318 -296
  183. package/lib/components/IconButton/IconButton.tsx +3 -4
  184. package/lib/components/IconButton/__tests__/__snapshots__/IconButton.test.tsx.snap +12 -9
  185. package/lib/components/Link/BaseLink.tsx +114 -71
  186. package/lib/components/Link/Link.stories.tsx +1 -1
  187. package/lib/components/Link/Link.tsx +120 -109
  188. package/lib/components/Link/__tests__/__snapshots__/link.test.tsx.snap +2 -2
  189. package/lib/components/MenuNew/Menu.context.tsx +149 -0
  190. package/lib/components/MenuNew/Menu.tsx +75 -0
  191. package/lib/components/MenuNew/MenuContent.tsx +140 -0
  192. package/lib/components/MenuNew/MenuItem.tsx +101 -0
  193. package/lib/components/MenuNew/MenuSection.tsx +47 -0
  194. package/lib/components/MenuNew/index.ts +8 -0
  195. package/lib/components/MenuNew/trigger/ButtonMenuTrigger.tsx +42 -0
  196. package/lib/components/MenuNew/trigger/IconMenuTrigger.tsx +40 -0
  197. package/lib/components/Pagination/Pagination.stories.tsx +1 -1
  198. package/lib/components/Pagination/PaginationControls.tsx +4 -5
  199. package/lib/components/Pagination/PaginationInfo.tsx +2 -3
  200. package/lib/components/ParagraphNew/Paragraph.tsx +200 -0
  201. package/lib/components/ParagraphNew/index.ts +6 -0
  202. package/lib/components/Radio/Radio.stories.tsx +1 -1
  203. package/lib/components/Radio/Radio.tsx +8 -8
  204. package/lib/components/Radio/__tests__/__snapshots__/Radio.test.tsx.snap +4 -4
  205. package/lib/components/Search/__tests__/__snapshots__/Search.test.tsx.snap +12 -32
  206. package/lib/components/Select/Select.mdx +23 -0
  207. package/lib/components/Select/Select.stories.tsx +43 -10
  208. package/lib/components/Select/Select.tsx +14 -3
  209. package/lib/components/Select/Select.types.ts +53 -16
  210. package/lib/components/Select/__tests__/Select.test.tsx +250 -1
  211. package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +5 -4
  212. package/lib/components/Select/subcomponents/CustomOption.tsx +10 -3
  213. package/lib/components/Select/subcomponents/CustomSelect.tsx +110 -10
  214. package/lib/components/Select/subcomponents/FilterInput.tsx +13 -3
  215. package/lib/components/Select/subcomponents/NativeSelect.tsx +10 -18
  216. package/lib/components/Select/subcomponents/Panel.tsx +2 -2
  217. package/lib/components/Select/subcomponents/VisibleField.tsx +48 -3
  218. package/lib/components/Snackbar/__tests__/__snapshots__/Snackbar.test.tsx.snap +9 -15
  219. package/lib/components/Spinner/Spinner.tsx +24 -5
  220. package/lib/components/Spinner/__tests__/Spinner.test.tsx +35 -5
  221. package/lib/components/Spinner/__tests__/__snapshots__/Spinner.test.tsx.snap +40 -16
  222. package/lib/components/StandaloneLink/StandaloneLink.stories.tsx +1 -1
  223. package/lib/components/StandaloneLink/StandaloneLink.tsx +180 -163
  224. package/lib/components/StandaloneLink/__tests__/__snapshots__/StandaloneLink.test.tsx.snap +2 -2
  225. package/lib/components/Table/Table.stories.tsx +1 -1
  226. package/lib/components/Table/Table.tsx +2 -0
  227. package/lib/components/Table/Table.types.ts +1 -0
  228. package/lib/components/Table/__tests__/Table.test.tsx +19 -0
  229. package/lib/components/Table/__tests__/__snapshots__/Table.test.tsx.snap +7 -3
  230. package/lib/components/Table/subcomponents/Cell/Cell.stories.tsx +1 -1
  231. package/lib/components/Table/subcomponents/Cell/Cell.tsx +23 -2
  232. package/lib/components/Table/subcomponents/Cell/CellContent.tsx +12 -1
  233. package/lib/components/Table/subcomponents/Cell/__tests__/Cell.test.tsx +106 -0
  234. package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +4 -3
  235. package/lib/components/Table/subcomponents/HeadCell/HeadCell.stories.tsx +1 -1
  236. package/lib/components/Table/subcomponents/HeadCell/HeadCell.tsx +28 -6
  237. package/lib/components/Table/subcomponents/HeadCell/HeadCellContent.tsx +3 -0
  238. package/lib/components/Table/subcomponents/HeadCell/__tests__/HeadCell.test.tsx +221 -2
  239. package/lib/components/Table/subcomponents/HeadCell/__tests__/__snapshots__/HeadCell.test.tsx.snap +6 -4
  240. package/lib/components/Table/subcomponents/Row.tsx +2 -2
  241. package/lib/components/Table/subcomponents/SortIcon.tsx +1 -0
  242. package/lib/components/Table/subcomponents/__tests__/Row.test.tsx +59 -0
  243. package/lib/components/Tabs/Tab.tsx +3 -3
  244. package/lib/components/Tabs/Tabs.stories.tsx +1 -1
  245. package/lib/components/Tabs/Tabs.tsx +5 -3
  246. package/lib/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap +4 -4
  247. package/lib/components/Timepicker/Timepicker.stories.tsx +1 -1
  248. package/lib/components/Toggle/Toggle.tsx +5 -5
  249. package/lib/components/Toggle/ToggleHandle.tsx +2 -3
  250. package/lib/components/Tooltip/Tooltip.tsx +2 -2
  251. package/lib/components/Tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap +2 -2
  252. package/lib/components/UclLogoNew/UclLogo.tsx +42 -0
  253. package/lib/components/UclLogoNew/index.ts +2 -0
  254. package/lib/components/WeekPicker/WeekPicker.stories.tsx +3 -5
  255. package/lib/components/common/Common.mdx +0 -1
  256. package/lib/components/index.ts +19 -1
  257. package/lib/theme/Colours.mdx +1 -1
  258. package/lib/theme/Theme.mdx +1 -1
  259. package/lib/theme/Typography.mdx +1 -1
  260. package/lib/theme/__tests__/fonts.test.ts +37 -0
  261. package/lib/theme/common/themeCommon.ts +515 -0
  262. package/lib/theme/fonts.ts +110 -0
  263. package/lib/theme/index.ts +6 -6
  264. package/lib/theme/light/lightColour.ts +232 -0
  265. package/lib/theme/light/lightTheme.ts +37 -0
  266. package/lib/theme/{defaultTheme.ts → original/color.ts} +17 -199
  267. package/lib/theme/original/defaultTheme.ts +207 -0
  268. package/lib/theme/original/originalColourNewStructure.ts +185 -0
  269. package/lib/theme/useTheme.tsx +72 -15
  270. package/lib/types/assets.d.ts +10 -0
  271. package/lib/utils/addAlphaToHex.ts +29 -0
  272. package/lib/utils/scrollToTop.ts +5 -0
  273. package/package.json +11 -6
  274. package/dist/components/Button/buttonPrimaryStyle.d.ts +0 -4
  275. package/dist/components/Button/buttonSecondaryStyle.d.ts +0 -4
  276. package/dist/components/Button/buttonTertiaryStyle.d.ts +0 -4
  277. package/dist/theme/defaultTheme.d.ts +0 -274
  278. package/lib/components/Button/buttonPrimaryStyle.ts +0 -62
  279. package/lib/components/Button/buttonSecondaryStyle.ts +0 -65
  280. package/lib/components/Button/buttonTertiaryStyle.ts +0 -54
@@ -37,11 +37,10 @@ export type FilterInputProps = Omit<
37
37
  | 'aria-label'
38
38
  >;
39
39
 
40
- /**
41
- * Public props for <Select>, used by both custom and native render paths.
42
- */
43
- export interface SelectProps<T = string | number>
44
- extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
40
+ type SelectBaseProps<T = string | number> = Omit<
41
+ React.HTMLAttributes<HTMLElement>,
42
+ 'onChange'
43
+ > & {
45
44
  /**
46
45
  * Controls keyboard selection behaviour in the custom select variant.
47
46
  * - `focus` (default): arrow keys move focus and commit value immediately.
@@ -84,10 +83,6 @@ export interface SelectProps<T = string | number>
84
83
  * Extra attributes forwarded to the native <select> when `native` is true
85
84
  */
86
85
  nativeHtmlAttributes?: React.SelectHTMLAttributes<HTMLSelectElement>;
87
- /**
88
- * Current value (controlled)
89
- */
90
- value?: T;
91
86
  /**
92
87
  * Disable interaction
93
88
  */
@@ -104,23 +99,65 @@ export interface SelectProps<T = string | number>
104
99
  * Custom className for the options panel
105
100
  */
106
101
  panelClassName?: string;
107
- /**
108
- * Change handler for the custom variant
109
- */
110
- onValueChange?: (value: T, ev: React.UIEvent) => void;
111
102
  /**
112
103
  * Ref forwarded to the rendered element
113
104
  * (div for custom, select for native)
114
105
  */
115
106
  ref?: React.Ref<HTMLDivElement | HTMLSelectElement | null>;
116
- }
107
+ };
108
+
109
+ export type NonClearableSelectProps<T = string | number> =
110
+ SelectBaseProps<T> & {
111
+ /**
112
+ * Current value (controlled)
113
+ */
114
+ value?: T;
115
+ /**
116
+ * Show a clear action in the custom variant when a value is selected
117
+ */
118
+ clearable?: false | undefined;
119
+ /**
120
+ * Change handler for the custom variant
121
+ */
122
+ onValueChange?: (value: T, ev: React.SyntheticEvent) => void;
123
+ };
124
+
125
+ export type ClearableSelectProps<T = string | number> = SelectBaseProps<T> & {
126
+ /**
127
+ * Current value (controlled)
128
+ */
129
+ value?: T | null;
130
+ /**
131
+ * Show a clear action in the custom variant when a value is selected
132
+ */
133
+ clearable: true;
134
+ /**
135
+ * Change handler for the custom variant
136
+ */
137
+ onValueChange?: (value: T | null, ev: React.SyntheticEvent) => void;
138
+ };
139
+
140
+ /**
141
+ * Public props for <Select>, used by both custom and native render paths.
142
+ */
143
+ export type SelectProps<T = string | number> =
144
+ | NonClearableSelectProps<T>
145
+ | ClearableSelectProps<T>;
146
+
147
+ export type InternalSelectProps<T = string | number> = SelectBaseProps<T> & {
148
+ value?: T | null;
149
+ clearable?: boolean;
150
+ onValueChange?: (value: T | null, ev: React.SyntheticEvent) => void;
151
+ };
117
152
 
118
153
  /**
119
154
  * Each option as displayed in the Panel of <CustomSelect>
120
155
  * Roughly equivalent to a custom version of <option>
121
156
  */
122
- export interface CustomOptionProps<T>
123
- extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {
157
+ export interface CustomOptionProps<T> extends Omit<
158
+ React.HTMLAttributes<HTMLDivElement>,
159
+ 'onSelect'
160
+ > {
124
161
  value: T;
125
162
  optionIndex?: number;
126
163
  testId?: string;
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test, vi, beforeAll } from 'vitest';
2
2
  import { useState } from 'react';
3
- import { render } from '@testing-library/react';
3
+ import { createEvent, fireEvent, render } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
5
5
  import Select from '../Select';
6
6
  import { ThemeContextProvider } from '../../../theme/useTheme';
@@ -418,6 +418,41 @@ describe('Select', () => {
418
418
  );
419
419
  });
420
420
 
421
+ test('filterable select keeps option clicks working while the filter input is focused', async () => {
422
+ const user = userEvent.setup();
423
+
424
+ const ControlledSelect = () => {
425
+ const [value, setValue] = useState('');
426
+ return (
427
+ <Select
428
+ filterable
429
+ options={defaultOptions}
430
+ value={value}
431
+ onValueChange={(next) => setValue(next as string)}
432
+ />
433
+ );
434
+ };
435
+
436
+ const result = render(
437
+ <ThemeContextProvider>
438
+ <ControlledSelect />
439
+ </ThemeContextProvider>
440
+ );
441
+
442
+ await user.click(result.getByTestId('ucl-uikit-select'));
443
+ const filterInput = result.getByRole('searchbox');
444
+ expect(filterInput).toHaveFocus();
445
+
446
+ await user.click(result.getByRole('option', { name: 'Option 2' }));
447
+
448
+ expect(
449
+ result.queryByTestId('ucl-uikit-select__panel')
450
+ ).not.toBeInTheDocument();
451
+ expect(result.getByTestId('ucl-uikit-select')).toHaveTextContent(
452
+ 'Option 2'
453
+ );
454
+ });
455
+
421
456
  test('forwards id and title attributes to the combobox', () => {
422
457
  const result = render(
423
458
  <ThemeContextProvider>
@@ -557,4 +592,218 @@ describe('Select', () => {
557
592
  await user.keyboard('{ArrowDown}');
558
593
  expect(filterInput).toHaveAttribute('aria-activedescendant', options[1].id);
559
594
  });
595
+
596
+ test('open dropdown closes when tabbing to the next element', async () => {
597
+ const user = userEvent.setup();
598
+ const result = render(
599
+ <ThemeContextProvider>
600
+ <div>
601
+ <Select
602
+ options={defaultOptions}
603
+ value=''
604
+ onValueChange={() => {}}
605
+ />
606
+ <button type='button'>After</button>
607
+ </div>
608
+ </ThemeContextProvider>
609
+ );
610
+
611
+ await user.click(result.getByRole('combobox'));
612
+ expect(result.getByRole('listbox')).toBeInTheDocument();
613
+
614
+ await user.tab();
615
+
616
+ expect(result.queryByRole('listbox')).not.toBeInTheDocument();
617
+ expect(result.getByRole('button', { name: 'After' })).toHaveFocus();
618
+ });
619
+
620
+ test('open filterable dropdown closes when tabbing to the next element', async () => {
621
+ const user = userEvent.setup();
622
+ const result = render(
623
+ <ThemeContextProvider>
624
+ <div>
625
+ <Select
626
+ filterable
627
+ options={defaultOptions}
628
+ value=''
629
+ onValueChange={() => {}}
630
+ />
631
+ <button type='button'>After</button>
632
+ </div>
633
+ </ThemeContextProvider>
634
+ );
635
+
636
+ await user.click(result.getByRole('combobox'));
637
+ expect(result.getByRole('searchbox')).toHaveFocus();
638
+ expect(result.getByRole('listbox')).toBeInTheDocument();
639
+
640
+ await user.tab();
641
+
642
+ expect(result.queryByRole('listbox')).not.toBeInTheDocument();
643
+ expect(result.getByRole('button', { name: 'After' })).toHaveFocus();
644
+ });
645
+
646
+ test('open dropdown closes when shift-tabbing to the previous element', async () => {
647
+ const user = userEvent.setup();
648
+ const result = render(
649
+ <ThemeContextProvider>
650
+ <div>
651
+ <button type='button'>Before</button>
652
+ <Select
653
+ options={defaultOptions}
654
+ value=''
655
+ onValueChange={() => {}}
656
+ />
657
+ </div>
658
+ </ThemeContextProvider>
659
+ );
660
+
661
+ await user.tab();
662
+ await user.tab();
663
+ expect(result.getByRole('combobox')).toHaveFocus();
664
+
665
+ await user.keyboard('{Enter}');
666
+ expect(result.getByRole('listbox')).toBeInTheDocument();
667
+
668
+ await user.tab({ shift: true });
669
+
670
+ expect(result.queryByRole('listbox')).not.toBeInTheDocument();
671
+ expect(result.getByRole('button', { name: 'Before' })).toHaveFocus();
672
+ });
673
+
674
+ test('open filterable dropdown closes when shift-tabbing to the previous element', async () => {
675
+ const user = userEvent.setup();
676
+ const result = render(
677
+ <ThemeContextProvider>
678
+ <div>
679
+ <button type='button'>Before</button>
680
+ <Select
681
+ filterable
682
+ options={defaultOptions}
683
+ value=''
684
+ onValueChange={() => {}}
685
+ />
686
+ </div>
687
+ </ThemeContextProvider>
688
+ );
689
+
690
+ await user.tab();
691
+ await user.tab();
692
+ expect(result.getByRole('searchbox')).toHaveFocus();
693
+ expect(result.getByRole('listbox')).toBeInTheDocument();
694
+
695
+ await user.tab({ shift: true });
696
+
697
+ expect(result.queryByRole('listbox')).not.toBeInTheDocument();
698
+ expect(result.getByRole('button', { name: 'Before' })).toHaveFocus();
699
+ });
700
+
701
+ test('open filterable dropdown at end of tab order does not trap focus on Tab', async () => {
702
+ const result = render(
703
+ <ThemeContextProvider>
704
+ <Select
705
+ filterable
706
+ options={defaultOptions}
707
+ value=''
708
+ onValueChange={() => {}}
709
+ />
710
+ </ThemeContextProvider>
711
+ );
712
+
713
+ fireEvent.click(result.getByRole('combobox'));
714
+ const searchbox = result.getByRole('searchbox');
715
+ expect(result.getByRole('listbox')).toBeInTheDocument();
716
+
717
+ const tabEvent = createEvent.keyDown(searchbox, { key: 'Tab' });
718
+ const dispatchResult = fireEvent(searchbox, tabEvent);
719
+
720
+ expect(dispatchResult).toBe(true);
721
+ expect(result.queryByRole('listbox')).not.toBeInTheDocument();
722
+ });
723
+
724
+ test('open filterable dropdown at start of tab order does not trap focus on shift-tab', async () => {
725
+ const user = userEvent.setup();
726
+ const result = render(
727
+ <ThemeContextProvider>
728
+ <Select
729
+ filterable
730
+ options={defaultOptions}
731
+ value=''
732
+ onValueChange={() => {}}
733
+ />
734
+ </ThemeContextProvider>
735
+ );
736
+
737
+ await user.tab();
738
+ const searchbox = result.getByRole('searchbox');
739
+ expect(searchbox).toHaveFocus();
740
+ expect(result.getByRole('listbox')).toBeInTheDocument();
741
+
742
+ const shiftTabEvent = createEvent.keyDown(searchbox, {
743
+ key: 'Tab',
744
+ shiftKey: true,
745
+ });
746
+ const dispatchResult = fireEvent(searchbox, shiftTabEvent);
747
+
748
+ expect(dispatchResult).toBe(true);
749
+ expect(result.queryByRole('listbox')).not.toBeInTheDocument();
750
+ });
751
+
752
+ test('clear button appears for clearable select and clears to null', async () => {
753
+ const user = userEvent.setup();
754
+ const changeSpy = vi.fn();
755
+
756
+ const ControlledSelect = () => {
757
+ const [value, setValue] = useState<string | null>('2');
758
+ return (
759
+ <Select
760
+ clearable
761
+ options={defaultOptions}
762
+ value={value}
763
+ placeholder='Select an option'
764
+ onValueChange={(next, ev) => {
765
+ setValue(next as string | null);
766
+ changeSpy(next, ev);
767
+ }}
768
+ />
769
+ );
770
+ };
771
+
772
+ const result = render(
773
+ <ThemeContextProvider>
774
+ <ControlledSelect />
775
+ </ThemeContextProvider>
776
+ );
777
+
778
+ const clearButton = result.getByTestId(
779
+ 'ucl-uikit-select__visible-field-clear-button'
780
+ );
781
+ expect(result.getByRole('combobox')).toHaveTextContent('Option 2');
782
+
783
+ await user.click(clearButton);
784
+
785
+ expect(changeSpy).toHaveBeenCalledWith(null, expect.any(Object));
786
+ expect(result.getByRole('combobox')).toHaveTextContent('Select an option');
787
+ expect(
788
+ result.queryByTestId('ucl-uikit-select__visible-field-clear-button')
789
+ ).not.toBeInTheDocument();
790
+ });
791
+
792
+ test('disabled clearable select does not show clear button', () => {
793
+ const result = render(
794
+ <ThemeContextProvider>
795
+ <Select
796
+ clearable
797
+ disabled
798
+ options={defaultOptions}
799
+ value='1'
800
+ onValueChange={() => {}}
801
+ />
802
+ </ThemeContextProvider>
803
+ );
804
+
805
+ expect(
806
+ result.queryByTestId('ucl-uikit-select__visible-field-clear-button')
807
+ ).not.toBeInTheDocument();
808
+ });
560
809
  });
@@ -4,7 +4,7 @@ exports[`Select > Snapshot: default 1`] = `
4
4
  <div
5
5
  aria-expanded="false"
6
6
  aria-haspopup="listbox"
7
- class="ucl-uikit-select css-1kop2c3"
7
+ class="ucl-uikit-select css-a6n340"
8
8
  data-testid="ucl-uikit-select"
9
9
  role="combobox"
10
10
  tabindex="0"
@@ -17,9 +17,10 @@ exports[`Select > Snapshot: default 1`] = `
17
17
  class="css-16t5nns"
18
18
  />
19
19
  <svg
20
- class="ucl-uikit-icon css-hseitk"
20
+ class="ucl-uikit-icon css-1pg9a4v"
21
21
  data-testid="ucl-uikit-icon"
22
22
  fill="none"
23
+ focusable="false"
23
24
  height="24"
24
25
  stroke="currentColor"
25
26
  stroke-linecap="round"
@@ -29,8 +30,8 @@ exports[`Select > Snapshot: default 1`] = `
29
30
  width="24"
30
31
  xmlns="http://www.w3.org/2000/svg"
31
32
  >
32
- <polyline
33
- points="6 9 12 15 18 9"
33
+ <path
34
+ d="m6 9 6 6 6-6"
34
35
  />
35
36
  </svg>
36
37
  </div>
@@ -34,6 +34,12 @@ const CustomOption = <T extends string | number>({
34
34
  event.stopPropagation(); // Otherwise the panel will open again instantaneously
35
35
  };
36
36
 
37
+ const handleMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
38
+ // Keep focus on the combobox/filter input so blur does not close the panel
39
+ // before the option click handler runs.
40
+ event.preventDefault();
41
+ };
42
+
37
43
  const baseStyle = css`
38
44
  gap: 16px;
39
45
  min-height: 40px;
@@ -42,11 +48,11 @@ const CustomOption = <T extends string | number>({
42
48
  font-family: ${theme.font.family.primary};
43
49
  font-size: ${theme.font.size.f16};
44
50
  line-height: ${theme.font.lineHeight.h140};
45
- background-color: ${theme.color.neutral.white};
51
+ background-color: ${theme.colour.fill.inverse};
46
52
  overflow: hidden;
47
53
 
48
54
  &:hover {
49
- background-color: ${theme.color.neutral.grey5};
55
+ background-color: ${theme.colour.fill.brandSubtleHover};
50
56
  }
51
57
  `;
52
58
 
@@ -60,7 +66,7 @@ const CustomOption = <T extends string | number>({
60
66
  `;
61
67
 
62
68
  const selectedStyle = css`
63
- background-color: ${theme.color.neutral.grey5};
69
+ background-color: ${theme.colour.fill.brandSubtleSelected};
64
70
  `;
65
71
 
66
72
  const style = cx(
@@ -74,6 +80,7 @@ const CustomOption = <T extends string | number>({
74
80
 
75
81
  return (
76
82
  <div
83
+ onMouseDown={handleMouseDown}
77
84
  onClick={handleClick}
78
85
  className={style}
79
86
  data-testid={testId}
@@ -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'