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
@@ -25,9 +25,9 @@ const meta = {
25
25
  table: { type: { summary: 'OptionData[]' } },
26
26
  },
27
27
  value: {
28
- description: 'Selected value (string | number)',
28
+ description: 'Selected value (string | number | null)',
29
29
  control: { type: 'text' },
30
- table: { type: { summary: 'string | number' } },
30
+ table: { type: { summary: 'string | number | null' } },
31
31
  },
32
32
  onValueChange: {
33
33
  description:
@@ -35,7 +35,8 @@ const meta = {
35
35
  control: false,
36
36
  table: {
37
37
  type: {
38
- summary: '(value: string | number, ev: React.UIEvent) => void',
38
+ summary:
39
+ '(value: string | number | null, ev: React.SyntheticEvent) => void',
39
40
  },
40
41
  },
41
42
  },
@@ -67,6 +68,12 @@ const meta = {
67
68
  control: { type: 'boolean' },
68
69
  table: { type: { summary: 'boolean' } },
69
70
  },
71
+ clearable: {
72
+ description:
73
+ 'Show a clear action in the custom variant when a value is selected',
74
+ control: { type: 'boolean' },
75
+ table: { type: { summary: 'boolean' } },
76
+ },
70
77
  filterInputProps: {
71
78
  description:
72
79
  'Additional props to spread onto the filter input (when filterable)',
@@ -131,7 +138,7 @@ type Story = StoryObj<typeof meta>;
131
138
  export const Default: Story = {
132
139
  render: () => {
133
140
  const [args, updateArgs] = useArgs();
134
- const onValueChange = (value: string | number) => {
141
+ const onValueChange = (value: string | number | null) => {
135
142
  updateArgs({ value });
136
143
  };
137
144
  return (
@@ -176,7 +183,7 @@ export const Disabled: Story = {
176
183
  },
177
184
  render: () => {
178
185
  const [args, updateArgs] = useArgs();
179
- const onValueChange = (value: string | number) => {
186
+ const onValueChange = (value: string | number | null) => {
180
187
  updateArgs({ value });
181
188
  };
182
189
 
@@ -198,7 +205,7 @@ export const WithPlaceholder: Story = {
198
205
  args: { placeholder: 'Please select an option' },
199
206
  render: () => {
200
207
  const [args, updateArgs] = useArgs();
201
- const onValueChange = (value: string | number) => {
208
+ const onValueChange = (value: string | number | null) => {
202
209
  updateArgs({ value });
203
210
  };
204
211
 
@@ -227,7 +234,7 @@ export const SingleLongOption: Story = {
227
234
  },
228
235
  render: () => {
229
236
  const [args, updateArgs] = useArgs();
230
- const onValueChange = (value: string | number) => {
237
+ const onValueChange = (value: string | number | null) => {
231
238
  updateArgs({ value });
232
239
  };
233
240
 
@@ -268,7 +275,7 @@ export const ManyOptions: Story = {
268
275
  },
269
276
  render: () => {
270
277
  const [args, updateArgs] = useArgs();
271
- const onValueChange = (value: string | number) => {
278
+ const onValueChange = (value: string | number | null) => {
272
279
  updateArgs({ value });
273
280
  };
274
281
  return (
@@ -289,7 +296,8 @@ export const filterable: Story = {
289
296
  args: { filterable: true },
290
297
  render: () => {
291
298
  const [args, updateArgs] = useArgs();
292
- const onValueChange = (value: string | number) => updateArgs({ value });
299
+ const onValueChange = (value: string | number | null) =>
300
+ updateArgs({ value });
293
301
  return (
294
302
  <div style={storyWrapperStyle}>
295
303
  <Select
@@ -305,12 +313,37 @@ export const filterable: Story = {
305
313
  },
306
314
  };
307
315
 
316
+ export const Clearable: Story = {
317
+ args: {
318
+ clearable: true,
319
+ value: '2',
320
+ placeholder: 'Clear selection',
321
+ },
322
+ render: () => {
323
+ const [args, updateArgs] = useArgs();
324
+ const onValueChange = (value: string | number | null) =>
325
+ updateArgs({ value });
326
+ return (
327
+ <div style={storyWrapperStyle}>
328
+ <Select
329
+ {...args}
330
+ className={select400pxWidthClass}
331
+ options={args.options}
332
+ value={args.value}
333
+ onValueChange={onValueChange}
334
+ />
335
+ </div>
336
+ );
337
+ },
338
+ };
339
+
308
340
  export const SelectionBehaviourCommit: Story = {
309
341
  name: 'Selection behaviour: commit',
310
342
  args: { selectionBehaviour: 'commit' },
311
343
  render: () => {
312
344
  const [args, updateArgs] = useArgs();
313
- const onValueChange = (value: string | number) => updateArgs({ value });
345
+ const onValueChange = (value: string | number | null) =>
346
+ updateArgs({ value });
314
347
  return (
315
348
  <div style={storyWrapperStyle}>
316
349
  <Select
@@ -1,11 +1,18 @@
1
1
  import { NativeSelect, CustomSelect } from './subcomponents';
2
- import { SelectProps } from './Select.types';
2
+ import type { InternalSelectProps, SelectProps } from './Select.types';
3
3
 
4
- const Select = <T extends string | number = string>(props: SelectProps<T>) => {
4
+ type SelectComponent = <T extends string | number = string>(
5
+ props: SelectProps<T>
6
+ ) => React.JSX.Element;
7
+
8
+ const Select = (<T extends string | number = string>(
9
+ props: InternalSelectProps<T>
10
+ ) => {
5
11
  const {
6
12
  native,
7
13
  nativeHtmlAttributes,
8
14
  filterable,
15
+ clearable,
9
16
  onValueChange,
10
17
  ref,
11
18
  ...rest
@@ -15,6 +22,9 @@ const Select = <T extends string | number = string>(props: SelectProps<T>) => {
15
22
  if (filterable) {
16
23
  console.warn('filterable is not supported on native Select; ignoring.');
17
24
  }
25
+ if (clearable) {
26
+ console.warn('clearable is not supported on native Select; ignoring.');
27
+ }
18
28
  const { value, ...nativeRest } = rest;
19
29
  const {
20
30
  value: nativeAttrValue,
@@ -40,10 +50,11 @@ const Select = <T extends string | number = string>(props: SelectProps<T>) => {
40
50
  <CustomSelect<T>
41
51
  onValueChange={onValueChange}
42
52
  filterable={filterable}
53
+ clearable={clearable}
43
54
  ref={ref as React.Ref<HTMLDivElement>}
44
55
  {...rest}
45
56
  />
46
57
  );
47
- };
58
+ }) as SelectComponent;
48
59
 
49
60
  export default Select;
@@ -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}