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.
- package/README.md +2 -4
- package/dist/components/Accordion/Accordion.Heading.d.ts +1 -0
- package/dist/components/AppHeader/AppHeader.d.ts +1 -1
- package/dist/components/AppHeader/AppHeaderBottom.d.ts +1 -1
- package/dist/components/AppHeader/AppHeaderNav.d.ts +1 -1
- package/dist/components/AppHeader/AppHeaderTop.d.ts +1 -1
- package/dist/components/Breadcrumbs/Breadcrumb.d.ts +3 -4
- package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.stories.d.ts +1 -1
- package/dist/components/Button/Button.d.ts +7 -3
- package/dist/components/Button/Button.stories.d.ts +17 -7
- package/dist/components/Button/style/buttonAccentStyle.d.ts +4 -0
- package/dist/components/Button/style/buttonPrimaryDestructiveStyle.d.ts +4 -0
- package/dist/components/Button/style/buttonPrimaryStyle.d.ts +4 -0
- package/dist/components/Button/style/buttonPrimarySubtleStyle.d.ts +4 -0
- package/dist/components/Button/style/buttonPrimaryWarningStyle.d.ts +4 -0
- package/dist/components/Button/style/buttonSecondaryDestructiveStyle.d.ts +4 -0
- package/dist/components/Button/style/buttonSecondaryStyle.d.ts +4 -0
- package/dist/components/Button/style/buttonSecondarySubtleStyle.d.ts +4 -0
- package/dist/components/Button/style/buttonTertiaryDestructiveStyle.d.ts +4 -0
- package/dist/components/Button/style/buttonTertiaryNoPaddingStyle.d.ts +4 -0
- package/dist/components/Button/style/buttonTertiaryStyle.d.ts +4 -0
- package/dist/components/Checkbox/Checkbox.d.ts +1 -0
- package/dist/components/FooterNew/BackToTop.d.ts +8 -0
- package/dist/components/FooterNew/Footer.d.ts +23 -0
- package/dist/components/FooterNew/FooterColumn.d.ts +8 -0
- package/dist/components/FooterNew/FooterLinks.d.ts +7 -0
- package/dist/components/FooterNew/FooterNavLink.d.ts +8 -0
- package/dist/components/FooterNew/LegalAndCopyright.d.ts +14 -0
- package/dist/components/FooterNew/LogoAddressAndSocial.d.ts +10 -0
- package/dist/components/FooterNew/SocialLink.d.ts +8 -0
- package/dist/components/FooterNew/__tests__/Footer.test.d.ts +1 -0
- package/dist/components/FooterNew/index.d.ts +2 -0
- package/dist/components/HeaderNew/Header.d.ts +18 -0
- package/dist/components/HeaderNew/HeaderBorder.d.ts +7 -0
- package/dist/components/HeaderNew/HeaderLogo.d.ts +9 -0
- package/dist/components/HeaderNew/HeaderMenuContainer.d.ts +7 -0
- package/dist/components/HeaderNew/HeaderTitle.d.ts +9 -0
- package/dist/components/HeaderNew/__tests__/Header.test.d.ts +1 -0
- package/dist/components/HeaderNew/constants.d.ts +3 -0
- package/dist/components/HeaderNew/index.d.ts +3 -0
- package/dist/components/HeadingNew/Heading.d.ts +13 -0
- package/dist/components/HeadingNew/index.d.ts +2 -0
- package/dist/components/Icon/svgImports.d.ts +7 -881
- package/dist/components/Link/BaseLink.d.ts +14 -5
- package/dist/components/Link/Link.d.ts +8 -3
- package/dist/components/Link/Link.stories.d.ts +3 -1
- package/dist/components/MenuNew/Menu.context.d.ts +14 -0
- package/dist/components/MenuNew/Menu.d.ts +20 -0
- package/dist/components/MenuNew/MenuContent.d.ts +9 -0
- package/dist/components/MenuNew/MenuItem.d.ts +10 -0
- package/dist/components/MenuNew/MenuSection.d.ts +7 -0
- package/dist/components/MenuNew/index.d.ts +6 -0
- package/dist/components/MenuNew/trigger/ButtonMenuTrigger.d.ts +8 -0
- package/dist/components/MenuNew/trigger/IconMenuTrigger.d.ts +8 -0
- package/dist/components/Overlay/Overlay.stories.d.ts +12 -12
- package/dist/components/ParagraphNew/Paragraph.d.ts +13 -0
- package/dist/components/ParagraphNew/index.d.ts +4 -0
- package/dist/components/Select/Select.d.ts +2 -1
- package/dist/components/Select/Select.stories.d.ts +13 -1
- package/dist/components/Select/Select.types.d.ts +40 -13
- package/dist/components/Select/subcomponents/CustomSelect.d.ts +3 -3
- package/dist/components/Select/subcomponents/FilterInput.d.ts +3 -1
- package/dist/components/Select/subcomponents/NativeSelect.d.ts +2 -2
- package/dist/components/Select/subcomponents/VisibleField.d.ts +4 -1
- package/dist/components/Spinner/Spinner.d.ts +2 -0
- package/dist/components/StandaloneLink/StandaloneLink.d.ts +8 -5
- package/dist/components/StandaloneLink/StandaloneLink.stories.d.ts +3 -1
- package/dist/components/Table/Table.d.ts +3 -3
- package/dist/components/Table/Table.stories.d.ts +3 -3
- package/dist/components/Table/Table.types.d.ts +1 -0
- package/dist/components/Table/subcomponents/Cell/Cell.d.ts +5 -1
- package/dist/components/Table/subcomponents/Cell/Cell.stories.d.ts +15 -13
- package/dist/components/Table/subcomponents/Cell/CellContent.d.ts +5 -1
- package/dist/components/Table/subcomponents/HeadCell/HeadCell.d.ts +2 -1
- package/dist/components/Table/subcomponents/HeadCell/HeadCell.stories.d.ts +14 -13
- package/dist/components/Table/subcomponents/HeadCell/HeadCellContent.d.ts +2 -1
- package/dist/components/Table/subcomponents/__tests__/Row.test.d.ts +1 -0
- package/dist/components/UclLogoNew/UclLogo.d.ts +8 -0
- package/dist/components/UclLogoNew/index.d.ts +2 -0
- package/dist/components/index.d.ts +12 -0
- package/dist/index.js +20909 -16022
- package/dist/theme/__tests__/fonts.test.d.ts +1 -0
- package/dist/theme/common/themeCommon.d.ts +904 -0
- package/dist/theme/fonts.d.ts +18 -0
- package/dist/theme/index.d.ts +6 -3
- package/dist/theme/light/lightColour.d.ts +126 -0
- package/dist/theme/light/lightTheme.d.ts +3 -0
- package/dist/theme/original/color.d.ts +166 -0
- package/dist/theme/original/defaultTheme.d.ts +1340 -0
- package/dist/theme/original/originalColourNewStructure.d.ts +126 -0
- package/dist/theme/useTheme.d.ts +2174 -0
- package/dist/utils/addAlphaToHex.d.ts +5 -0
- package/dist/utils/scrollToTop.d.ts +2 -0
- package/lib/components/Accordion/Accordion.Heading.tsx +51 -39
- package/lib/components/Accordion/Accordion.Panel.tsx +0 -4
- package/lib/components/Accordion/Accordion.tsx +34 -28
- package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +12 -10
- package/lib/components/Alert/Alert.tsx +12 -12
- package/lib/components/Alert/__tests__/__snapshots__/Alert.test.tsx.snap +13 -39
- package/lib/components/AppHeader/AppHeader.tsx +6 -11
- package/lib/components/AppHeader/AppHeaderBottom.tsx +2 -3
- package/lib/components/AppHeader/AppHeaderNav.tsx +2 -3
- package/lib/components/AppHeader/AppHeaderTop.tsx +1 -1
- package/lib/components/AppHeader/__tests__/__snapshots__/AppHeader.test.tsx.snap +2 -2
- package/lib/components/AppMenu/__tests__/__snapshots__/AppMenu.test.tsx.snap +6 -19
- package/lib/components/Badge/Badge.stories.tsx +1 -1
- package/lib/components/Breadcrumbs/Breadcrumb.tsx +26 -12
- package/lib/components/Breadcrumbs/Breadcrumbs.tsx +1 -1
- package/lib/components/Breadcrumbs/__tests__/Breadcrumbs.test.tsx +9 -27
- package/lib/components/Breadcrumbs/__tests__/__snapshots__/Breadcrumbs.test.tsx.snap +24 -20
- package/lib/components/Button/Button.mdx +32 -279
- package/lib/components/Button/Button.stories.tsx +43 -50
- package/lib/components/Button/Button.tsx +165 -25
- package/lib/components/Button/__tests__/Button.test.tsx +49 -15
- package/lib/components/Button/__tests__/__snapshots__/Button.test.tsx.snap +80 -73
- package/lib/components/Button/style/buttonAccentStyle.ts +53 -0
- package/lib/components/Button/style/buttonPrimaryDestructiveStyle.ts +55 -0
- package/lib/components/Button/style/buttonPrimaryStyle.ts +53 -0
- package/lib/components/Button/style/buttonPrimarySubtleStyle.ts +64 -0
- package/lib/components/Button/style/buttonPrimaryWarningStyle.ts +56 -0
- package/lib/components/Button/style/buttonSecondaryDestructiveStyle.ts +63 -0
- package/lib/components/Button/style/buttonSecondaryStyle.ts +62 -0
- package/lib/components/Button/style/buttonSecondarySubtleStyle.ts +72 -0
- package/lib/components/Button/style/buttonTertiaryDestructiveStyle.ts +65 -0
- package/lib/components/Button/style/buttonTertiaryNoPaddingStyle.ts +52 -0
- package/lib/components/Button/style/buttonTertiaryStyle.ts +62 -0
- package/lib/components/Calendar/Calendar.stories.tsx +33 -13
- package/lib/components/Calendar/Calendar.tsx +2 -2
- package/lib/components/Calendar/__tests__/__snapshots__/Calendar.test.tsx.snap +99 -95
- package/lib/components/Calendar/subcomponents/AcademicWeek.tsx +2 -1
- package/lib/components/Calendar/subcomponents/AcademicWeeks.tsx +2 -3
- package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +3 -7
- package/lib/components/Calendar/subcomponents/Controls.tsx +1 -1
- package/lib/components/Calendar/subcomponents/Day.stories.tsx +1 -1
- package/lib/components/Calendar/subcomponents/Day.tsx +7 -9
- package/lib/components/Calendar/subcomponents/EventDot.tsx +4 -8
- package/lib/components/Checkbox/Checkbox.stories.tsx +1 -1
- package/lib/components/Checkbox/Checkbox.tsx +12 -10
- package/lib/components/Checkbox/__tests__/Checkbox.test.tsx +29 -0
- package/lib/components/Checkbox/__tests__/__snapshots__/Checkbox.test.tsx.snap +4 -4
- package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +117 -0
- package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +16 -44
- package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +10 -1
- package/lib/components/Datepicker/subcomponents/VisibleField.tsx +24 -23
- package/lib/components/Dialog/BaseDialog.tsx +2 -2
- package/lib/components/Dialog/Dialog.stories.tsx +1 -1
- package/lib/components/Divider/__tests__/__snapshots__/Breadcrumbs.test.tsx.snap +12 -12
- package/lib/components/FeedbackDialog/FeedbackDialog.stories.tsx +1 -1
- package/lib/components/FeedbackDialog/FeedbackDialog.tsx +4 -6
- package/lib/components/Field/CharacterCount.tsx +2 -2
- package/lib/components/Field/ErrorText.tsx +1 -1
- package/lib/components/Field/Field.tsx +1 -1
- package/lib/components/Field/HelperText.tsx +3 -1
- package/lib/components/FileInput/FileInput.stories.tsx +1 -1
- package/lib/components/FileInput/__tests__/__snapshots__/FileInput.test.tsx.snap +4 -20
- package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +70 -79
- package/lib/components/FooterNew/BackToTop.tsx +83 -0
- package/lib/components/FooterNew/Footer.tsx +110 -0
- package/lib/components/FooterNew/FooterColumn.tsx +79 -0
- package/lib/components/FooterNew/FooterLinks.tsx +44 -0
- package/lib/components/FooterNew/FooterNavLink.tsx +63 -0
- package/lib/components/FooterNew/LegalAndCopyright.tsx +150 -0
- package/lib/components/FooterNew/LogoAddressAndSocial.tsx +154 -0
- package/lib/components/FooterNew/SocialLink.tsx +108 -0
- package/lib/components/FooterNew/__tests__/Footer.test.tsx +51 -0
- package/lib/components/FooterNew/__tests__/__snapshots__/Footer.test.tsx.snap +1107 -0
- package/lib/components/FooterNew/index.ts +2 -0
- package/lib/components/HeaderDraft/__tests__/__snapshots__/Header.test.tsx.snap +3 -2
- package/lib/components/HeaderNew/Header.tsx +93 -0
- package/lib/components/HeaderNew/HeaderBorder.tsx +55 -0
- package/lib/components/HeaderNew/HeaderLogo.tsx +70 -0
- package/lib/components/HeaderNew/HeaderMenuContainer.tsx +35 -0
- package/lib/components/HeaderNew/HeaderTitle.tsx +53 -0
- package/lib/components/HeaderNew/__tests__/Header.test.tsx +42 -0
- package/lib/components/HeaderNew/__tests__/__snapshots__/Header.test.tsx.snap +79 -0
- package/lib/components/HeaderNew/constants.ts +3 -0
- package/lib/components/HeaderNew/index.ts +7 -0
- package/lib/components/HeadingNew/Heading.tsx +208 -0
- package/lib/components/HeadingNew/index.ts +2 -0
- package/lib/components/Icon/__tests__/__snapshots__/Icon.test.tsx.snap +16 -12
- package/lib/components/Icon/svgImports.ts +318 -296
- package/lib/components/IconButton/IconButton.tsx +3 -4
- package/lib/components/IconButton/__tests__/__snapshots__/IconButton.test.tsx.snap +12 -9
- package/lib/components/Link/BaseLink.tsx +114 -71
- package/lib/components/Link/Link.stories.tsx +1 -1
- package/lib/components/Link/Link.tsx +120 -109
- package/lib/components/Link/__tests__/__snapshots__/link.test.tsx.snap +2 -2
- package/lib/components/MenuNew/Menu.context.tsx +149 -0
- package/lib/components/MenuNew/Menu.tsx +75 -0
- package/lib/components/MenuNew/MenuContent.tsx +140 -0
- package/lib/components/MenuNew/MenuItem.tsx +101 -0
- package/lib/components/MenuNew/MenuSection.tsx +47 -0
- package/lib/components/MenuNew/index.ts +8 -0
- package/lib/components/MenuNew/trigger/ButtonMenuTrigger.tsx +42 -0
- package/lib/components/MenuNew/trigger/IconMenuTrigger.tsx +40 -0
- package/lib/components/Pagination/Pagination.stories.tsx +1 -1
- package/lib/components/Pagination/PaginationControls.tsx +4 -5
- package/lib/components/Pagination/PaginationInfo.tsx +2 -3
- package/lib/components/ParagraphNew/Paragraph.tsx +200 -0
- package/lib/components/ParagraphNew/index.ts +6 -0
- package/lib/components/Radio/Radio.stories.tsx +1 -1
- package/lib/components/Radio/Radio.tsx +8 -8
- package/lib/components/Radio/__tests__/__snapshots__/Radio.test.tsx.snap +4 -4
- package/lib/components/Search/__tests__/__snapshots__/Search.test.tsx.snap +12 -32
- package/lib/components/Select/Select.mdx +23 -0
- package/lib/components/Select/Select.stories.tsx +43 -10
- package/lib/components/Select/Select.tsx +14 -3
- package/lib/components/Select/Select.types.ts +53 -16
- package/lib/components/Select/__tests__/Select.test.tsx +250 -1
- package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +5 -4
- package/lib/components/Select/subcomponents/CustomOption.tsx +10 -3
- package/lib/components/Select/subcomponents/CustomSelect.tsx +110 -10
- package/lib/components/Select/subcomponents/FilterInput.tsx +13 -3
- package/lib/components/Select/subcomponents/NativeSelect.tsx +10 -18
- package/lib/components/Select/subcomponents/Panel.tsx +2 -2
- package/lib/components/Select/subcomponents/VisibleField.tsx +48 -3
- package/lib/components/Snackbar/__tests__/__snapshots__/Snackbar.test.tsx.snap +9 -15
- package/lib/components/Spinner/Spinner.tsx +24 -5
- package/lib/components/Spinner/__tests__/Spinner.test.tsx +35 -5
- package/lib/components/Spinner/__tests__/__snapshots__/Spinner.test.tsx.snap +40 -16
- package/lib/components/StandaloneLink/StandaloneLink.stories.tsx +1 -1
- package/lib/components/StandaloneLink/StandaloneLink.tsx +180 -163
- package/lib/components/StandaloneLink/__tests__/__snapshots__/StandaloneLink.test.tsx.snap +2 -2
- package/lib/components/Table/Table.stories.tsx +1 -1
- package/lib/components/Table/Table.tsx +2 -0
- package/lib/components/Table/Table.types.ts +1 -0
- package/lib/components/Table/__tests__/Table.test.tsx +19 -0
- package/lib/components/Table/__tests__/__snapshots__/Table.test.tsx.snap +7 -3
- package/lib/components/Table/subcomponents/Cell/Cell.stories.tsx +1 -1
- package/lib/components/Table/subcomponents/Cell/Cell.tsx +23 -2
- package/lib/components/Table/subcomponents/Cell/CellContent.tsx +12 -1
- package/lib/components/Table/subcomponents/Cell/__tests__/Cell.test.tsx +106 -0
- package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +4 -3
- package/lib/components/Table/subcomponents/HeadCell/HeadCell.stories.tsx +1 -1
- package/lib/components/Table/subcomponents/HeadCell/HeadCell.tsx +28 -6
- package/lib/components/Table/subcomponents/HeadCell/HeadCellContent.tsx +3 -0
- package/lib/components/Table/subcomponents/HeadCell/__tests__/HeadCell.test.tsx +221 -2
- package/lib/components/Table/subcomponents/HeadCell/__tests__/__snapshots__/HeadCell.test.tsx.snap +6 -4
- package/lib/components/Table/subcomponents/Row.tsx +2 -2
- package/lib/components/Table/subcomponents/SortIcon.tsx +1 -0
- package/lib/components/Table/subcomponents/__tests__/Row.test.tsx +59 -0
- package/lib/components/Tabs/Tab.tsx +3 -3
- package/lib/components/Tabs/Tabs.stories.tsx +1 -1
- package/lib/components/Tabs/Tabs.tsx +5 -3
- package/lib/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap +4 -4
- package/lib/components/Timepicker/Timepicker.stories.tsx +1 -1
- package/lib/components/Toggle/Toggle.tsx +5 -5
- package/lib/components/Toggle/ToggleHandle.tsx +2 -3
- package/lib/components/Tooltip/Tooltip.tsx +2 -2
- package/lib/components/Tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap +2 -2
- package/lib/components/UclLogoNew/UclLogo.tsx +42 -0
- package/lib/components/UclLogoNew/index.ts +2 -0
- package/lib/components/WeekPicker/WeekPicker.stories.tsx +3 -5
- package/lib/components/common/Common.mdx +0 -1
- package/lib/components/index.ts +19 -1
- package/lib/theme/Colours.mdx +1 -1
- package/lib/theme/Theme.mdx +1 -1
- package/lib/theme/Typography.mdx +1 -1
- package/lib/theme/__tests__/fonts.test.ts +37 -0
- package/lib/theme/common/themeCommon.ts +515 -0
- package/lib/theme/fonts.ts +110 -0
- package/lib/theme/index.ts +6 -6
- package/lib/theme/light/lightColour.ts +232 -0
- package/lib/theme/light/lightTheme.ts +37 -0
- package/lib/theme/{defaultTheme.ts → original/color.ts} +17 -199
- package/lib/theme/original/defaultTheme.ts +207 -0
- package/lib/theme/original/originalColourNewStructure.ts +185 -0
- package/lib/theme/useTheme.tsx +72 -15
- package/lib/types/assets.d.ts +10 -0
- package/lib/utils/addAlphaToHex.ts +29 -0
- package/lib/utils/scrollToTop.ts +5 -0
- package/package.json +11 -6
- package/dist/components/Button/buttonPrimaryStyle.d.ts +0 -4
- package/dist/components/Button/buttonSecondaryStyle.d.ts +0 -4
- package/dist/components/Button/buttonTertiaryStyle.d.ts +0 -4
- package/dist/theme/defaultTheme.d.ts +0 -274
- package/lib/components/Button/buttonPrimaryStyle.ts +0 -62
- package/lib/components/Button/buttonSecondaryStyle.ts +0 -65
- 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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
<
|
|
33
|
-
|
|
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.
|
|
51
|
+
background-color: ${theme.colour.fill.inverse};
|
|
46
52
|
overflow: hidden;
|
|
47
53
|
|
|
48
54
|
&:hover {
|
|
49
|
-
background-color: ${theme.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
397
|
-
color: ${theme.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
58
|
+
color: ${theme.colour.text.default};
|
|
50
59
|
&::placeholder {
|
|
51
|
-
color: ${theme.
|
|
60
|
+
color: ${theme.colour.text.tertiary};
|
|
52
61
|
}
|
|
53
62
|
&:disabled {
|
|
54
|
-
color: ${theme.
|
|
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'
|