@sats-group/ui-lib 74.2.0

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 (227) hide show
  1. package/.nvmrc +1 -0
  2. package/README.md +35 -0
  3. package/catalog-info.yaml +14 -0
  4. package/eslint.config.mjs +94 -0
  5. package/fonts/Inter-BoldItalic.woff +0 -0
  6. package/fonts/Inter-BoldItalic.woff2 +0 -0
  7. package/fonts/Inter-ExtraBold.woff +0 -0
  8. package/fonts/Inter-ExtraBold.woff2 +0 -0
  9. package/fonts/Inter-Italic.woff +0 -0
  10. package/fonts/Inter-Italic.woff2 +0 -0
  11. package/fonts/Inter-Regular.woff +0 -0
  12. package/fonts/Inter-Regular.woff2 +0 -0
  13. package/fonts/Inter-SemiBold.woff +0 -0
  14. package/fonts/Inter-SemiBold.woff2 +0 -0
  15. package/fonts/LICENSE.txt +92 -0
  16. package/fonts/SATSHeadline-Bold.woff +0 -0
  17. package/fonts/SATSHeadline-BoldItalic.woff +0 -0
  18. package/fonts/SATSHeadline-RegularItalic.woff +0 -0
  19. package/fonts/SATSHeadline-SemiBoldItalic.woff +0 -0
  20. package/logos/e-avatar.svg +3 -0
  21. package/logos/elixia-letter.svg +3 -0
  22. package/logos/elixia-small.svg +8 -0
  23. package/logos/elixia.svg +8 -0
  24. package/logos/s-avatar.svg +3 -0
  25. package/logos/sats-letter.svg +3 -0
  26. package/logos/sats-small.svg +3 -0
  27. package/logos/sats.svg +4 -0
  28. package/package.json +58 -0
  29. package/react/add-bem-modifiers.ts +51 -0
  30. package/react/badge/badge.scss +53 -0
  31. package/react/badge/badge.tsx +28 -0
  32. package/react/badge/badge.types.ts +34 -0
  33. package/react/badge/index.ts +2 -0
  34. package/react/banner/banner.scss +118 -0
  35. package/react/banner/banner.tsx +92 -0
  36. package/react/banner/banner.types.ts +10 -0
  37. package/react/banner/index.ts +2 -0
  38. package/react/bomb/bomb.scss +33 -0
  39. package/react/bomb/bomb.tsx +19 -0
  40. package/react/bomb/bomb.types.ts +1 -0
  41. package/react/bomb/index.ts +2 -0
  42. package/react/button/button.tsx +19 -0
  43. package/react/button/button.types.ts +3 -0
  44. package/react/button/index.ts +2 -0
  45. package/react/checkbox/checkbox.scss +218 -0
  46. package/react/checkbox/checkbox.tsx +176 -0
  47. package/react/checkbox/checkbox.types.ts +19 -0
  48. package/react/checkbox/index.ts +2 -0
  49. package/react/chip/chip.scss +46 -0
  50. package/react/chip/chip.tsx +37 -0
  51. package/react/chip/chip.types.ts +18 -0
  52. package/react/chip/index.ts +2 -0
  53. package/react/chip/remove.tsx +14 -0
  54. package/react/chip-selected/chip-selected.scss +47 -0
  55. package/react/chip-selected/chip-selected.tsx +102 -0
  56. package/react/chip-selected/chip-selected.types.ts +11 -0
  57. package/react/chip-selected/index.ts +2 -0
  58. package/react/confirmation/confirmation.scss +60 -0
  59. package/react/confirmation/confirmation.tsx +85 -0
  60. package/react/confirmation/confirmation.types.ts +24 -0
  61. package/react/confirmation/index.ts +2 -0
  62. package/react/context-menu/context-menu.scss +183 -0
  63. package/react/context-menu/context-menu.tsx +200 -0
  64. package/react/context-menu/context-menu.types.ts +71 -0
  65. package/react/context-menu/index.ts +2 -0
  66. package/react/cropped-image/cropped-image.scss +48 -0
  67. package/react/cropped-image/cropped-image.tsx +36 -0
  68. package/react/cropped-image/cropped-image.types.ts +26 -0
  69. package/react/cropped-image/index.ts +2 -0
  70. package/react/dropdown-list/dropdown-list.scss +170 -0
  71. package/react/dropdown-list/dropdown-list.tsx +116 -0
  72. package/react/dropdown-list/dropdown-list.types.ts +17 -0
  73. package/react/dropdown-list/index.ts +2 -0
  74. package/react/expander/expander.scss +115 -0
  75. package/react/expander/expander.tsx +167 -0
  76. package/react/expander/expander.types.ts +26 -0
  77. package/react/expander/index.ts +2 -0
  78. package/react/filter/filter.scss +94 -0
  79. package/react/filter/filter.tsx +99 -0
  80. package/react/filter/filter.types.ts +8 -0
  81. package/react/filter/index.ts +2 -0
  82. package/react/filter-wrapper/filter-wrapper.scss +46 -0
  83. package/react/filter-wrapper/filter-wrapper.tsx +24 -0
  84. package/react/filter-wrapper/filter-wrapper.types.ts +10 -0
  85. package/react/filter-wrapper/index.ts +2 -0
  86. package/react/flag/flag.scss +26 -0
  87. package/react/flag/flag.tsx +27 -0
  88. package/react/flag/flag.types.ts +17 -0
  89. package/react/flag/index.ts +2 -0
  90. package/react/form-content/checkbox-category.tsx +183 -0
  91. package/react/form-content/form-content.checkbox-list.tsx +126 -0
  92. package/react/form-content/form-content.checkbox-list.types.ts +36 -0
  93. package/react/form-content/form-content.radio-list.tsx +58 -0
  94. package/react/form-content/form-content.range.tsx +20 -0
  95. package/react/form-content/form-content.range.types.ts +14 -0
  96. package/react/form-content/form-content.scss +234 -0
  97. package/react/form-content/form-content.search.tsx +47 -0
  98. package/react/form-content/form-content.tsx +95 -0
  99. package/react/form-content/form-content.types.ts +55 -0
  100. package/react/form-content/index.ts +2 -0
  101. package/react/form-content/types/index.d.ts +1 -0
  102. package/react/hidden-input/hidden-input.tsx +9 -0
  103. package/react/hidden-input/hidden-input.types.ts +6 -0
  104. package/react/hidden-input/index.ts +2 -0
  105. package/react/hooks/focus-previous-element.ts +30 -0
  106. package/react/hooks/is-running-on-client.ts +1 -0
  107. package/react/hooks/use-click-outside.ts +23 -0
  108. package/react/hooks/use-escape.ts +18 -0
  109. package/react/hooks/use-event.ts +29 -0
  110. package/react/hooks/use-is-mounted.ts +11 -0
  111. package/react/hooks/use-toggle.ts +19 -0
  112. package/react/icons/16/close.tsx +12 -0
  113. package/react/icons/18/close.tsx +18 -0
  114. package/react/icons/24/arrow-down.tsx +14 -0
  115. package/react/icons/24/arrow-right.tsx +14 -0
  116. package/react/icons/24/arrow-up.tsx +14 -0
  117. package/react/icons/24/close.tsx +12 -0
  118. package/react/icons/24/remove.tsx +12 -0
  119. package/react/icons/24/search.tsx +10 -0
  120. package/react/icons/icons.md +3 -0
  121. package/react/indexed-access-type.ts +1 -0
  122. package/react/link/index.ts +2 -0
  123. package/react/link/link.scss +44 -0
  124. package/react/link/link.tsx +62 -0
  125. package/react/link/link.types.ts +37 -0
  126. package/react/link-button/index.ts +2 -0
  127. package/react/link-button/link-button.tsx +17 -0
  128. package/react/link-button/link-button.types.ts +5 -0
  129. package/react/link-card/index.ts +2 -0
  130. package/react/link-card/link-card.scss +37 -0
  131. package/react/link-card/link-card.tsx +24 -0
  132. package/react/link-card/link-card.types.ts +5 -0
  133. package/react/logos/e-avatar.tsx +12 -0
  134. package/react/logos/elixia-letter.tsx +12 -0
  135. package/react/logos/elixia-small.tsx +12 -0
  136. package/react/logos/elixia.tsx +12 -0
  137. package/react/logos/index.ts +8 -0
  138. package/react/logos/s-avatar.tsx +12 -0
  139. package/react/logos/sats-letter.tsx +12 -0
  140. package/react/logos/sats-small.tsx +12 -0
  141. package/react/logos/sats.tsx +12 -0
  142. package/react/message/hook/use-message.ts +22 -0
  143. package/react/message/index.ts +2 -0
  144. package/react/message/message.scss +92 -0
  145. package/react/message/message.tsx +60 -0
  146. package/react/message/message.types.ts +39 -0
  147. package/react/message/publish.ts +19 -0
  148. package/react/message-field/index.ts +2 -0
  149. package/react/message-field/message-field.scss +21 -0
  150. package/react/message-field/message-field.tsx +70 -0
  151. package/react/message-field/message-field.types.ts +24 -0
  152. package/react/modal/index.ts +2 -0
  153. package/react/modal/modal.scss +162 -0
  154. package/react/modal/modal.tsx +130 -0
  155. package/react/modal/modal.types.ts +36 -0
  156. package/react/modal/tab-trapper.tsx +68 -0
  157. package/react/progress-bar/index.ts +2 -0
  158. package/react/progress-bar/progress-bar.scss +71 -0
  159. package/react/progress-bar/progress-bar.tsx +81 -0
  160. package/react/progress-bar/progress-bar.types.ts +35 -0
  161. package/react/radio/index.ts +2 -0
  162. package/react/radio/radio.scss +142 -0
  163. package/react/radio/radio.tsx +87 -0
  164. package/react/radio/radio.types.ts +15 -0
  165. package/react/scale-bar/index.ts +2 -0
  166. package/react/scale-bar/scale-bar.scss +22 -0
  167. package/react/scale-bar/scale-bar.tsx +29 -0
  168. package/react/scale-bar/scale-bar.types.ts +4 -0
  169. package/react/search/index.ts +2 -0
  170. package/react/search/search.scss +207 -0
  171. package/react/search/search.tsx +255 -0
  172. package/react/search/search.types.ts +43 -0
  173. package/react/select/chevron-down.tsx +24 -0
  174. package/react/select/index.ts +2 -0
  175. package/react/select/select.scss +135 -0
  176. package/react/select/select.tsx +105 -0
  177. package/react/select/select.types.ts +19 -0
  178. package/react/select-option/README.md +3 -0
  179. package/react/select-option/index.ts +2 -0
  180. package/react/select-option/select-option.tsx +16 -0
  181. package/react/select-option/select-option.types.ts +8 -0
  182. package/react/tag/index.ts +2 -0
  183. package/react/tag/tag.scss +107 -0
  184. package/react/tag/tag.tsx +26 -0
  185. package/react/tag/tag.types.ts +30 -0
  186. package/react/text/index.ts +2 -0
  187. package/react/text/text.scss +109 -0
  188. package/react/text/text.tsx +40 -0
  189. package/react/text/text.types.ts +29 -0
  190. package/react/text-area/index.ts +2 -0
  191. package/react/text-area/text-area.scss +180 -0
  192. package/react/text-area/text-area.tsx +153 -0
  193. package/react/text-area/text-area.types.ts +24 -0
  194. package/react/text-input/index.ts +2 -0
  195. package/react/text-input/text-input.scss +233 -0
  196. package/react/text-input/text-input.tsx +106 -0
  197. package/react/text-input/text-input.types.ts +19 -0
  198. package/react/toggle/index.ts +2 -0
  199. package/react/toggle/toggle.scss +69 -0
  200. package/react/toggle/toggle.tsx +83 -0
  201. package/react/toggle/toggle.types.ts +11 -0
  202. package/react/toolbox/index.ts +2 -0
  203. package/react/toolbox/toolbox.scss +68 -0
  204. package/react/toolbox/toolbox.tsx +43 -0
  205. package/react/toolbox/toolbox.types.ts +39 -0
  206. package/react/ts/debounce.ts +12 -0
  207. package/react/types.ts +38 -0
  208. package/react/use-input-validation.ts +47 -0
  209. package/react/use-input-validation.types.ts +12 -0
  210. package/react/visually-button/index.ts +2 -0
  211. package/react/visually-button/visually-button.scss +470 -0
  212. package/react/visually-button/visually-button.tsx +130 -0
  213. package/react/visually-button/visually-button.types.ts +71 -0
  214. package/react/visually-hidden/index.ts +2 -0
  215. package/react/visually-hidden/visually-hidden.scss +6 -0
  216. package/react/visually-hidden/visually-hidden.tsx +10 -0
  217. package/tokens/corner-radius.scss +5 -0
  218. package/tokens/dark.scss +392 -0
  219. package/tokens/darkmode.scss +131 -0
  220. package/tokens/elevation.scss +57 -0
  221. package/tokens/font-faces.scss +62 -0
  222. package/tokens/font-names.scss +2 -0
  223. package/tokens/font-sizes.scss +95 -0
  224. package/tokens/light.scss +392 -0
  225. package/tokens/lightmode.scss +131 -0
  226. package/tokens/primitives.scss +137 -0
  227. package/tokens/spacing.scss +12 -0
@@ -0,0 +1,255 @@
1
+ import React, { useRef, useState } from 'react';
2
+
3
+ import Text from '../text';
4
+ import SearchIcon from '../icons/24/search';
5
+ import CloseIcon from '../icons/18/close';
6
+ import useInputValidation from '../use-input-validation';
7
+ import useClickOutside from '../hooks/use-click-outside';
8
+ import { useIsMounted } from '../hooks/use-is-mounted';
9
+
10
+ import {
11
+ Search as Props,
12
+ themes,
13
+ inputSizes,
14
+ variants,
15
+ expandDirections,
16
+ } from './search.types';
17
+ import classNames from 'classnames';
18
+ import Button from '../button';
19
+
20
+ const RefSearch = React.forwardRef<HTMLInputElement, Props>(
21
+ (
22
+ {
23
+ button,
24
+ customErrorMessages,
25
+ clear,
26
+ disabled,
27
+ label,
28
+ isLabelVisible = true,
29
+ name,
30
+ onChangeFunc = () => {},
31
+ expandable,
32
+ placeholder,
33
+ required,
34
+ inputSize = inputSizes.small,
35
+ theme = themes.inline,
36
+ variant = variants.default,
37
+ value = '',
38
+ helpText,
39
+ helpIcon,
40
+ ...restProps
41
+ },
42
+ ref,
43
+ ) => {
44
+ const [validationOnChange, onInvalid, error] =
45
+ useInputValidation(customErrorMessages);
46
+ const searchArea = useRef<HTMLDivElement>(null);
47
+ const [expand, setExpand] = useState(false);
48
+ const isMounted = useIsMounted();
49
+
50
+ useClickOutside(searchArea, () => setExpand(false));
51
+
52
+ const [inputValue, setInputValue] = useState(value);
53
+
54
+ const onMessageChange = (message: string) => setInputValue(message);
55
+
56
+ return (
57
+ <div
58
+ className={classNames('search', {
59
+ [`search--expand-direction-${expandable?.direction}`]:
60
+ expandable?.direction,
61
+ })}
62
+ >
63
+ {isMounted ? (
64
+ <div
65
+ className={classNames('search__inner', {
66
+ 'search__inner--expandable': expandable,
67
+ 'search__inner--expand': expand,
68
+ })}
69
+ ref={searchArea}
70
+ >
71
+ {theme === themes.inline && isLabelVisible ? (
72
+ <Text
73
+ theme={Text.themes.emphasis}
74
+ size={
75
+ inputSize === inputSizes.large
76
+ ? Text.sizes.large
77
+ : Text.sizes.small
78
+ }
79
+ className={classNames('search__header', {
80
+ [`search__header--variant-${variant}`]: variant,
81
+ })}
82
+ >
83
+ {label}
84
+ </Text>
85
+ ) : null}
86
+ <div
87
+ className={classNames('search__input-wrapper', {
88
+ [`search__input-wrapper--size-${inputSize}`]: inputSize,
89
+ [`search__input-wrapper--theme-${theme}`]: theme,
90
+ [`search__input-wrapper--variant-${variant}`]: variant,
91
+ })}
92
+ >
93
+ <label className="search__label">
94
+ <div
95
+ className={classNames('search__icon', {
96
+ [`search__icon--variant-${variant}`]: variant,
97
+ })}
98
+ >
99
+ <SearchIcon />
100
+ </div>
101
+ <input
102
+ {...restProps}
103
+ className={classNames('search__input', {
104
+ [`search__input--variant-${variant}`]: variant,
105
+ [`search__input--size-${inputSize}`]: inputSize,
106
+ })}
107
+ disabled={disabled}
108
+ name={name}
109
+ onChange={e => {
110
+ onChangeFunc(e.target.value);
111
+ validationOnChange(e);
112
+ onMessageChange(e.target.value);
113
+ }}
114
+ onClick={() => setExpand(true)}
115
+ value={inputValue}
116
+ onInvalid={e => onInvalid(e)}
117
+ placeholder={placeholder}
118
+ ref={ref}
119
+ required={required}
120
+ type="search"
121
+ aria-label={
122
+ theme === themes.floating || !isLabelVisible
123
+ ? label
124
+ : undefined
125
+ }
126
+ />
127
+ </label>
128
+ {inputValue ? (
129
+ <Button
130
+ {...clear}
131
+ className="search__clear-button"
132
+ onClick={() => {
133
+ onChangeFunc('');
134
+ setInputValue('');
135
+ }}
136
+ type="button"
137
+ variant={
138
+ variant === variants.fixed
139
+ ? Button.variants.fixedTertiary
140
+ : Button.variants.tertiary
141
+ }
142
+ size={
143
+ inputSize === inputSizes.small
144
+ ? Button.sizes.small
145
+ : Button.sizes.basic
146
+ }
147
+ />
148
+ ) : null}
149
+ {button ? (
150
+ <Button
151
+ {...button}
152
+ className="search__search-button"
153
+ variant={
154
+ variant === variants.fixed
155
+ ? Button.variants.secondaryWhite
156
+ : Button.variants.secondary
157
+ }
158
+ type="submit"
159
+ size={
160
+ inputSize === inputSizes.small
161
+ ? Button.sizes.small
162
+ : Button.sizes.basic
163
+ }
164
+ />
165
+ ) : null}
166
+ {expandable && expand ? (
167
+ <Button
168
+ {...expandable.close}
169
+ className="search__close-button"
170
+ variant={
171
+ variant === variants.fixed
172
+ ? Button.variants.fixedTertiary
173
+ : Button.variants.tertiary
174
+ }
175
+ leadingIcon={<CloseIcon />}
176
+ onClick={() => {
177
+ setExpand(false);
178
+ onChangeFunc('');
179
+ setInputValue('');
180
+ }}
181
+ size={
182
+ inputSize === inputSizes.small
183
+ ? Button.sizes.small
184
+ : Button.sizes.basic
185
+ }
186
+ />
187
+ ) : null}
188
+ </div>
189
+ {error ? (
190
+ <div aria-hidden="true" className="search__error">
191
+ <Text>{error}</Text>
192
+ </div>
193
+ ) : null}
194
+ {theme === themes.inline && helpText ? (
195
+ <Text
196
+ size={Text.sizes.interface}
197
+ className={classNames('search__help-text', {
198
+ [`search__help-text--variant-${variant}`]: variant,
199
+ })}
200
+ >
201
+ {helpIcon}
202
+ {helpText}
203
+ </Text>
204
+ ) : null}
205
+ </div>
206
+ ) : (
207
+ <div className="search__loading-inner">
208
+ {theme === themes.inline && isLabelVisible ? (
209
+ <div
210
+ className={classNames('search__loading-extra', {
211
+ [`search__loading-extra--${variant}`]: variant,
212
+ })}
213
+ >
214
+ {label}
215
+ </div>
216
+ ) : null}
217
+ <div
218
+ className={classNames('search__loading-input', {
219
+ [`search__loading-input--${variant}`]: variant,
220
+ [`search__loading-input--${inputSize}`]: inputSize,
221
+ })}
222
+ ></div>
223
+ {theme === themes.inline && helpText ? (
224
+ <div
225
+ className={classNames('search__loading-extra', {
226
+ [`search__loading-extra--${variant}`]: variant,
227
+ })}
228
+ >
229
+ {helpIcon}
230
+ {helpText}
231
+ </div>
232
+ ) : null}
233
+ </div>
234
+ )}
235
+ </div>
236
+ );
237
+ },
238
+ );
239
+
240
+ // NOTE: Stack traces will show "forwardRef" instead of "Search" if this is not set.
241
+ RefSearch.displayName = 'Search';
242
+
243
+ const Search: typeof RefSearch & {
244
+ themes: typeof themes;
245
+ variants: typeof variants;
246
+ inputSizes: typeof inputSizes;
247
+ expandDirections: typeof expandDirections;
248
+ } = Object.assign(RefSearch, {
249
+ themes,
250
+ variants,
251
+ inputSizes,
252
+ expandDirections,
253
+ });
254
+
255
+ export default Search;
@@ -0,0 +1,43 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { Button } from '../button/button.types';
3
+ import type { InputHtmlProps, ObjectValues } from '../types';
4
+ import type { Messages } from '../use-input-validation.types';
5
+
6
+ export const themes = {
7
+ floating: 'floating',
8
+ inline: 'inline',
9
+ } as const;
10
+
11
+ export const inputSizes = {
12
+ small: 'small',
13
+ large: 'large',
14
+ } as const;
15
+
16
+ export const variants = {
17
+ default: 'default',
18
+ fixed: 'fixed',
19
+ } as const;
20
+
21
+ export const expandDirections = {
22
+ left: 'left',
23
+ right: 'right',
24
+ } as const;
25
+
26
+ export type Search = {
27
+ button?: Button;
28
+ clear: Button;
29
+ customErrorMessages?: Messages;
30
+ inputSize?: ObjectValues<typeof inputSizes>; //NOTE: This is not named `size` due to InputHTMLProps having a `size` attribute that will clash
31
+ label: string;
32
+ isLabelVisible?: boolean;
33
+ name: string;
34
+ onChangeFunc?: (arg: string) => void;
35
+ expandable?: {
36
+ close: Button;
37
+ direction?: ObjectValues<typeof expandDirections>;
38
+ };
39
+ theme?: ObjectValues<typeof themes>;
40
+ variant?: ObjectValues<typeof variants>;
41
+ helpText?: string;
42
+ helpIcon?: ReactElement;
43
+ } & InputHtmlProps;
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+
3
+ const ChevronDown: React.FunctionComponent<{ className?: string }> = ({
4
+ className,
5
+ }) => (
6
+ <svg
7
+ width="14"
8
+ height="8"
9
+ viewBox="0 0 14 8"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ className={className}
13
+ >
14
+ <path
15
+ d="M12 1.5L7 6.5L2 1.5"
16
+ stroke="currentColor"
17
+ strokeWidth="1.8"
18
+ strokeMiterlimit="10"
19
+ strokeLinecap="square"
20
+ />
21
+ </svg>
22
+ );
23
+
24
+ export default ChevronDown;
@@ -0,0 +1,2 @@
1
+ import Select from './select';
2
+ export default Select;
@@ -0,0 +1,135 @@
1
+ @use '../../tokens/corner-radius';
2
+ @use '../../tokens/light';
3
+ @use '../../tokens/spacing';
4
+
5
+ .select {
6
+ display: flex;
7
+
8
+ &--label-position-inline {
9
+ flex-direction: row;
10
+ gap: spacing.$xs;
11
+ align-items: center;
12
+ flex-wrap: wrap;
13
+ }
14
+
15
+ &--label-position-stacked {
16
+ flex-direction: column;
17
+ }
18
+
19
+ // NOTE: Specificity hack
20
+ :root &--error {
21
+ select {
22
+ background-color: rgba(light.$signal-surface-error, 0.07);
23
+ border-color: light.$ge-signal-border-error;
24
+ color: light.$on-signal-surface-error-default;
25
+ }
26
+ }
27
+
28
+ &__native-wrapper {
29
+ color: light.$on-surface-primary-default;
30
+ background-color: light.$surface-primary-default;
31
+ border-radius: corner-radius.$s;
32
+ position: relative;
33
+ }
34
+
35
+ &__chevron {
36
+ color: light.$on-surface-primary-default;
37
+ position: absolute;
38
+ right: 13px;
39
+ top: 50%;
40
+ transform: translateY(-50%);
41
+ pointer-events: none;
42
+ }
43
+
44
+ &__icon {
45
+ color: light.$on-surface-primary-default;
46
+ position: absolute;
47
+ left: 13px;
48
+ top: 50%;
49
+ width: 20px;
50
+ transform: translateY(-50%);
51
+
52
+ svg {
53
+ display: block;
54
+ max-width: 100%;
55
+ height: auto;
56
+ }
57
+ }
58
+
59
+ :root &--icon {
60
+ select {
61
+ padding-left: 40px; // NOTE: Space for icon
62
+ }
63
+ }
64
+
65
+ select {
66
+ color: light.$on-surface-primary-default;
67
+ appearance: none;
68
+ background-color: transparent;
69
+ border: 1px solid light.$ge-border-default;
70
+ display: block;
71
+ font-size: 16px;
72
+ font-family: 'Inter', sans-serif;
73
+ line-height: 1.25;
74
+ padding: 13px 15px;
75
+ padding-right: 35px; // NOTE: Space for chevron icon
76
+ position: relative;
77
+ width: 100%;
78
+ border-radius: corner-radius.$s;
79
+
80
+ @media (hover: hover) {
81
+ &:hover {
82
+ &:not(:disabled) {
83
+ background-color: light.$surface-primary-hover;
84
+ border-color: light.$surface-primary-hover;
85
+
86
+ ~ .select__chevron,
87
+ ~ .select__icon {
88
+ color: light.$on-surface-primary-default;
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ &:active {
95
+ &:not(:disabled) {
96
+ background-color: light.$surface-primary-default;
97
+ border-color: light.$ge-border-focused;
98
+
99
+ ~ .select__chevron,
100
+ ~ .select__icon {
101
+ color: light.$on-surface-primary-default;
102
+ }
103
+ }
104
+ }
105
+
106
+ &:focus {
107
+ outline: 4px solid currentColor;
108
+ outline-offset: 2px;
109
+ }
110
+
111
+ &:disabled {
112
+ color: light.$on-surface-primary-disabled;
113
+
114
+ ~ .select__chevron,
115
+ ~ .select__icon {
116
+ color: light.$on-surface-primary-disabled;
117
+ }
118
+ }
119
+ }
120
+
121
+ &__error {
122
+ color: light.$ge-signal-error;
123
+ margin-top: 4px;
124
+ }
125
+
126
+ &__asterisk {
127
+ color: light.$on-surface-featured;
128
+ margin-left: spacing.$xs;
129
+ }
130
+
131
+ &__label {
132
+ display: flex;
133
+ flex-direction: row;
134
+ }
135
+ }
@@ -0,0 +1,105 @@
1
+ import cn from 'classnames';
2
+ import * as React from 'react';
3
+
4
+ import useInputValidation from '../use-input-validation';
5
+
6
+ import ChevronDown from './chevron-down';
7
+ import Option from '../select-option/select-option';
8
+ import Text from '../text';
9
+
10
+ import { labelPositions, Select as Props } from './select.types';
11
+
12
+ // NOTE: `onChangeOption` works like `onChange` but returns the props for that option instead of an event object.
13
+ const RefSelect = React.forwardRef<
14
+ HTMLSelectElement,
15
+ React.PropsWithChildren<Props>
16
+ >(
17
+ (
18
+ {
19
+ children,
20
+ className,
21
+ icon,
22
+ isLabelVisible = true,
23
+ label,
24
+ labelPosition = labelPositions.stacked,
25
+ name,
26
+ onChange = () => {},
27
+ onChangeOption = () => {},
28
+ options = [],
29
+ required,
30
+ ...restProps
31
+ },
32
+ ref,
33
+ ) => {
34
+ const [validationOnChange, onInvalid, error] = useInputValidation();
35
+
36
+ const onInputChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
37
+ onChange(e);
38
+ onChangeOption(options.find(({ value }) => value === e.target.value));
39
+ validationOnChange(e);
40
+ };
41
+
42
+ return (
43
+ <label
44
+ className={cn('select', className, {
45
+ 'select--error': error,
46
+ 'select--icon': icon,
47
+ [`select--label-position-${labelPosition}`]: labelPosition
48
+ ? labelPositions[labelPosition]
49
+ : undefined,
50
+ })}
51
+ >
52
+ {isLabelVisible && (label || children) ? (
53
+ <div className="select__label">
54
+ <Text size={Text.sizes.small} theme={Text.themes.emphasis}>
55
+ {label}
56
+ </Text>
57
+ {required ? <span className="select__asterisk">*</span> : null}
58
+ {children}
59
+ </div>
60
+ ) : required ? (
61
+ <span className="select__asterisk">*</span>
62
+ ) : null}
63
+
64
+ <div className="select__native-wrapper">
65
+ <select
66
+ name={name}
67
+ ref={ref}
68
+ onChange={onInputChange}
69
+ required={required}
70
+ onInvalid={e => {
71
+ // NOTE: To not break compatibility
72
+ if (restProps.onInvalid) restProps.onInvalid(e);
73
+ onInvalid(e);
74
+ }}
75
+ aria-label={isLabelVisible ? undefined : label}
76
+ {...restProps}
77
+ >
78
+ {options.map(option => (
79
+ <Option key={option.value} {...option} />
80
+ ))}
81
+ </select>
82
+ {icon && <div className="select__icon">{icon}</div>}
83
+ <ChevronDown className="select__chevron" />
84
+ </div>
85
+
86
+ {/* NOTE: This is aria-hidden because reporting of validation errors is handled by the browser */}
87
+ {error && (
88
+ <div aria-hidden="true" className="select__error">
89
+ {error}
90
+ </div>
91
+ )}
92
+ </label>
93
+ );
94
+ },
95
+ );
96
+
97
+ // NOTE: If this isn't set, stack traces say "forwardRef" instead of "Select"
98
+ RefSelect.displayName = 'Select';
99
+
100
+ // NOTE: Since `React.forwardRef` props aren't generic, the component is cast so that generics work as intended. See https://stackoverflow.com/a/58473012
101
+ const Select = RefSelect as <OptionExtra>(
102
+ props: Props<OptionExtra> & { ref?: React.Ref<HTMLSelectElement> },
103
+ ) => React.ReactElement;
104
+
105
+ export default Select;
@@ -0,0 +1,19 @@
1
+ import { ObjectValues, SelectHtmlProps } from '../types';
2
+
3
+ import { SelectOption } from '../select-option/select-option.types';
4
+
5
+ export const labelPositions = {
6
+ inline: 'inline',
7
+ stacked: 'stacked',
8
+ } as const;
9
+
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ export type Select<OptionExtra = any> = {
12
+ icon?: React.ReactNode;
13
+ isLabelVisible?: boolean;
14
+ label?: string;
15
+ labelPosition?: ObjectValues<typeof labelPositions>;
16
+ name: string;
17
+ onChangeOption?: (option?: SelectOption<OptionExtra>) => void;
18
+ options?: SelectOption<OptionExtra>[];
19
+ } & SelectHtmlProps;
@@ -0,0 +1,3 @@
1
+ # SelectOption
2
+
3
+ This component is not intended to be used directly. It is used by `Select` internally but needs to live in a separate folder in order for a TypeScript file to be created for it.
@@ -0,0 +1,2 @@
1
+ import SelectOption from './select-option';
2
+ export default SelectOption;
@@ -0,0 +1,16 @@
1
+ import * as React from 'react';
2
+
3
+ import { SelectOption as Props } from './select-option.types';
4
+
5
+ /** This component is not intended to be used directly. It is used by `Select` internally */
6
+ const SelectOption: React.FunctionComponent<Props> = ({
7
+ value,
8
+ text,
9
+ ...rest
10
+ }) => (
11
+ <option value={value} {...rest}>
12
+ {text}
13
+ </option>
14
+ );
15
+
16
+ export default SelectOption;
@@ -0,0 +1,8 @@
1
+ import { OptionHtmlProps } from '../types';
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export type SelectOption<Extra = any> = {
5
+ extra?: Extra;
6
+ value: string;
7
+ text: string;
8
+ } & OptionHtmlProps;
@@ -0,0 +1,2 @@
1
+ import Tag from './tag';
2
+ export default Tag;