@transferwise/components 45.14.2 → 45.15.1

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 (160) hide show
  1. package/build/i18n/en.json +8 -0
  2. package/build/index.esm.js +1001 -367
  3. package/build/index.esm.js.map +1 -1
  4. package/build/index.js +1003 -366
  5. package/build/index.js.map +1 -1
  6. package/build/main.css +1 -1
  7. package/build/styles/common/closeButton/CloseButton.css +1 -1
  8. package/build/styles/dateLookup/DateLookup.css +1 -1
  9. package/build/styles/inputs/Input.css +1 -1
  10. package/build/styles/inputs/InputGroup.css +1 -1
  11. package/build/styles/inputs/SelectInput.css +1 -0
  12. package/build/styles/inputs/TextArea.css +1 -1
  13. package/build/styles/main.css +1 -1
  14. package/build/styles/promoCard/PromoCard.css +1 -1
  15. package/build/styles/stepper/Stepper.css +1 -1
  16. package/build/types/avatarWrapper/AvatarWrapper.d.ts +14 -5
  17. package/build/types/avatarWrapper/AvatarWrapper.d.ts.map +1 -1
  18. package/build/types/common/Option/Option.d.ts.map +1 -1
  19. package/build/types/common/focusBoundary/FocusBoundary.d.ts +2 -2
  20. package/build/types/common/focusBoundary/FocusBoundary.d.ts.map +1 -1
  21. package/build/types/common/hooks/useMedia.d.ts +2 -0
  22. package/build/types/common/hooks/useMedia.d.ts.map +1 -0
  23. package/build/types/common/hooks/useScreenSize.d.ts +3 -0
  24. package/build/types/common/hooks/useScreenSize.d.ts.map +1 -0
  25. package/build/types/common/preventScroll/PreventScroll.d.ts +2 -0
  26. package/build/types/common/preventScroll/PreventScroll.d.ts.map +1 -0
  27. package/build/types/dateInput/DateInput.d.ts +2 -0
  28. package/build/types/dateInput/DateInput.d.ts.map +1 -1
  29. package/build/types/dateLookup/DateLookup.messages.d.ts +65 -0
  30. package/build/types/dateLookup/DateLookup.messages.d.ts.map +1 -0
  31. package/build/types/dateLookup/dateHeader/DateHeader.d.ts +3 -1
  32. package/build/types/dateLookup/dateHeader/DateHeader.d.ts.map +1 -1
  33. package/build/types/dateLookup/dateTrigger/DateTrigger.messages.d.ts +7 -7
  34. package/build/types/dateLookup/dateTrigger/DateTrigger.messages.d.ts.map +1 -1
  35. package/build/types/dateLookup/tableLink/TableLink.d.ts +4 -26
  36. package/build/types/dateLookup/tableLink/TableLink.d.ts.map +1 -1
  37. package/build/types/dateLookup/yearCalendar/YearCalendar.d.ts +4 -29
  38. package/build/types/dateLookup/yearCalendar/YearCalendar.d.ts.map +1 -1
  39. package/build/types/index.d.ts +4 -0
  40. package/build/types/index.d.ts.map +1 -1
  41. package/build/types/inputs/Input.d.ts +1 -0
  42. package/build/types/inputs/Input.d.ts.map +1 -1
  43. package/build/types/inputs/SearchInput.d.ts +10 -0
  44. package/build/types/inputs/SearchInput.d.ts.map +1 -0
  45. package/build/types/inputs/SelectInput.d.ts +41 -0
  46. package/build/types/inputs/SelectInput.d.ts.map +1 -0
  47. package/build/types/inputs/_BottomSheet.d.ts +17 -0
  48. package/build/types/inputs/_BottomSheet.d.ts.map +1 -0
  49. package/build/types/inputs/_ButtonInput.d.ts +6 -0
  50. package/build/types/inputs/_ButtonInput.d.ts.map +1 -0
  51. package/build/types/inputs/_Popover.d.ts +18 -0
  52. package/build/types/inputs/_Popover.d.ts.map +1 -0
  53. package/build/types/inputs/_common.d.ts.map +1 -1
  54. package/build/types/logo/Logo.d.ts.map +1 -1
  55. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  56. package/build/types/snackbar/Snackbar.d.ts.map +1 -1
  57. package/build/types/stepper/Stepper.d.ts.map +1 -1
  58. package/build/types/tabs/Tabs.d.ts.map +1 -1
  59. package/build/types/tile/Tile.d.ts.map +1 -1
  60. package/build/types/upload/steps/completeStep/completeStep.d.ts.map +1 -1
  61. package/build/types/upload/steps/processingStep/processingStep.d.ts.map +1 -1
  62. package/build/types/utilities/wrapInFragment.d.ts +3 -0
  63. package/build/types/utilities/wrapInFragment.d.ts.map +1 -0
  64. package/package.json +27 -20
  65. package/src/accordion/AccordionItem/__snapshots__/AccordionItem.spec.js.snap +6 -6
  66. package/src/avatarWrapper/AvatarWrapper.tsx +20 -8
  67. package/src/avatarWrapper/__snapshots__/AvatarWrapper.spec.tsx.snap +1 -1
  68. package/src/card/Card.spec.js +2 -2
  69. package/src/common/Option/Option.tsx +1 -7
  70. package/src/common/bottomSheet/__snapshots__/BottomSheet.spec.tsx.snap +8 -1
  71. package/src/common/closeButton/CloseButton.css +1 -1
  72. package/src/common/focusBoundary/FocusBoundary.tsx +9 -32
  73. package/src/common/hooks/useMedia.spec.ts +39 -0
  74. package/src/common/hooks/useMedia.ts +15 -0
  75. package/src/common/hooks/useScreenSize.ts +7 -0
  76. package/src/common/preventScroll/PreventScroll.tsx +6 -0
  77. package/src/dateInput/DateInput.js +6 -0
  78. package/src/dateInput/DateInput.story.tsx +2 -0
  79. package/src/dateLookup/DateLookup.css +1 -1
  80. package/src/dateLookup/DateLookup.keyboardEvents.spec.js +3 -3
  81. package/src/dateLookup/DateLookup.less +4 -0
  82. package/src/dateLookup/DateLookup.messages.js +44 -0
  83. package/src/dateLookup/DateLookup.testingLibrary.spec.js +39 -0
  84. package/src/dateLookup/dateHeader/DateHeader.js +48 -26
  85. package/src/dateLookup/dateHeader/DateHeader.spec.js +37 -0
  86. package/src/dateLookup/dayCalendar/DayCalendar.js +3 -1
  87. package/src/dateLookup/dayCalendar/DayCalendar.spec.js +7 -1
  88. package/src/dateLookup/dayCalendar/table/DayCalendarTable.js +7 -3
  89. package/src/dateLookup/dayCalendar/table/DayCalendarTable.spec.js +1 -0
  90. package/src/dateLookup/monthCalendar/MonthCalendar.js +3 -1
  91. package/src/dateLookup/monthCalendar/MonthCalendar.spec.js +7 -1
  92. package/src/dateLookup/monthCalendar/table/MonthCalendarTable.spec.js +4 -5
  93. package/src/dateLookup/tableLink/TableLink.js +24 -3
  94. package/src/dateLookup/tableLink/TableLink.spec.js +60 -4
  95. package/src/dateLookup/yearCalendar/YearCalendar.js +16 -3
  96. package/src/dateLookup/yearCalendar/YearCalendar.spec.js +14 -1
  97. package/src/dateLookup/yearCalendar/table/YearCalendarTable.spec.js +4 -5
  98. package/src/decision/Decision.story.js +11 -11
  99. package/src/flowNavigation/__snapshots__/FlowNavigation.spec.js.snap +12 -12
  100. package/src/i18n/en.json +9 -0
  101. package/src/index.ts +8 -0
  102. package/src/inputs/Input.css +1 -1
  103. package/src/inputs/Input.less +14 -0
  104. package/src/inputs/Input.tsx +6 -2
  105. package/src/inputs/InputGroup.css +1 -1
  106. package/src/inputs/InputGroup.less +5 -0
  107. package/src/inputs/SearchInput.story.tsx +40 -0
  108. package/src/inputs/SearchInput.tsx +35 -0
  109. package/src/inputs/SelectInput.css +1 -0
  110. package/src/inputs/SelectInput.less +183 -0
  111. package/src/inputs/SelectInput.spec.tsx +120 -0
  112. package/src/inputs/SelectInput.story.tsx +264 -0
  113. package/src/inputs/SelectInput.tsx +565 -0
  114. package/src/inputs/TextArea.css +1 -1
  115. package/src/inputs/TextArea.less +5 -0
  116. package/src/inputs/_BottomSheet.less +107 -0
  117. package/src/inputs/_BottomSheet.tsx +128 -0
  118. package/src/inputs/_ButtonInput.less +7 -0
  119. package/src/inputs/_ButtonInput.tsx +27 -0
  120. package/src/inputs/_Popover.less +38 -0
  121. package/src/inputs/_Popover.tsx +118 -0
  122. package/src/inputs/_common.less +0 -4
  123. package/src/inputs/_common.ts +0 -1
  124. package/src/logo/Logo.js +3 -21
  125. package/src/logo/__snapshots__/Logo.spec.js.snap +78 -30
  126. package/src/main.css +1 -1
  127. package/src/main.less +4 -0
  128. package/src/phoneNumberInput/PhoneNumberInput.js +1 -0
  129. package/src/promoCard/PromoCard.css +1 -1
  130. package/src/select/searchBox/__snapshots__/SearchBox.spec.js.snap +1 -1
  131. package/src/snackbar/Snackbar.js +6 -1
  132. package/src/snackbar/Snackbar.spec.js +1 -3
  133. package/src/ssr.spec.js +7 -0
  134. package/src/stepper/Stepper.css +1 -1
  135. package/src/stepper/Stepper.less +1 -9
  136. package/src/stepper/Stepper.spec.js +4 -4
  137. package/src/stepper/Stepper.tsx +2 -5
  138. package/src/tabs/Tabs.js +2 -1
  139. package/src/tile/Tile.js +5 -11
  140. package/src/tile/__snapshots__/Tile.spec.js.snap +7 -9
  141. package/src/upload/Upload.js +1 -1
  142. package/src/upload/steps/completeStep/completeStep.js +4 -1
  143. package/src/upload/steps/processingStep/processingStep.js +1 -0
  144. package/src/uploadInput/uploadItem/UploadItem.tsx +1 -1
  145. package/src/utilities/wrapInFragment.tsx +3 -0
  146. package/build/types/common/focusBoundary/utils/getFocusableElements.d.ts +0 -2
  147. package/build/types/common/focusBoundary/utils/getFocusableElements.d.ts.map +0 -1
  148. package/build/types/common/focusBoundary/utils/index.d.ts +0 -3
  149. package/build/types/common/focusBoundary/utils/index.d.ts.map +0 -1
  150. package/build/types/common/focusBoundary/utils/resetFocus.d.ts +0 -2
  151. package/build/types/common/focusBoundary/utils/resetFocus.d.ts.map +0 -1
  152. package/src/common/focusBoundary/FocusBoundary.spec.tsx +0 -66
  153. package/src/common/focusBoundary/__snapshots__/FocusBoundary.spec.tsx.snap +0 -16
  154. package/src/common/focusBoundary/utils/getFocusableElements.js +0 -25
  155. package/src/common/focusBoundary/utils/getFocusableElements.spec.js +0 -51
  156. package/src/common/focusBoundary/utils/index.js +0 -2
  157. package/src/common/focusBoundary/utils/resetFocus.js +0 -23
  158. package/src/common/focusBoundary/utils/resetFocus.spec.js +0 -103
  159. package/src/snackbar/__snapshots__/Snackbar.spec.js.snap +0 -5
  160. /package/src/dateLookup/dateTrigger/{DateTrigger.messages.js → DateTrigger.messages.ts} +0 -0
@@ -0,0 +1,565 @@
1
+ import { Listbox as ListboxBase } from '@headlessui/react';
2
+ import { useId } from '@radix-ui/react-id';
3
+ import { Check, ChevronDown, Cross } from '@transferwise/icons';
4
+ import classNames from 'classnames';
5
+ import { createContext, useState, useRef, forwardRef, useEffect, useMemo, useContext } from 'react';
6
+ import { useIntl } from 'react-intl';
7
+ import mergeRefs from 'react-merge-refs';
8
+
9
+ import { useEffectEvent } from '../common/hooks/useEffectEvent';
10
+ import { useScreenSize } from '../common/hooks/useScreenSize';
11
+ import { Breakpoint } from '../common/propsValues/breakpoint';
12
+ import messages from '../dateLookup/dateTrigger/DateTrigger.messages';
13
+ import { wrapInFragment } from '../utilities/wrapInFragment';
14
+
15
+ import { InputGroup } from './InputGroup';
16
+ import { SearchInput } from './SearchInput';
17
+ import { BottomSheet } from './_BottomSheet';
18
+ import { ButtonInput, type ButtonInputProps } from './_ButtonInput';
19
+ import { Popover } from './_Popover';
20
+
21
+ function searchableString(value: string) {
22
+ return value.trim().replace(/\s+/gu, ' ').toLowerCase();
23
+ }
24
+
25
+ function inferSearchableStrings(value: unknown) {
26
+ if (typeof value === 'string') {
27
+ return [searchableString(value)];
28
+ }
29
+
30
+ if (typeof value === 'object' && value != null) {
31
+ return Object.values(value)
32
+ .filter((innerValue): innerValue is string => typeof innerValue === 'string')
33
+ .map((innerValue) => searchableString(innerValue));
34
+ }
35
+
36
+ return [];
37
+ }
38
+
39
+ const SelectInputHasValueContext = createContext(false);
40
+
41
+ const SelectInputOptionContentCompactContext = createContext(false);
42
+
43
+ interface SelectInputOptionItem<T = string> {
44
+ type: 'option';
45
+ value: T;
46
+ filterMatchers?: readonly string[];
47
+ disabled?: boolean;
48
+ }
49
+
50
+ interface SelectInputGroupItem<T = string> {
51
+ type: 'group';
52
+ label: string;
53
+ options: readonly SelectInputOptionItem<T>[];
54
+ }
55
+
56
+ interface SelectInputSeparatorItem {
57
+ type: 'separator';
58
+ }
59
+
60
+ export type SelectInputItem<T = string> =
61
+ | SelectInputOptionItem<T>
62
+ | SelectInputGroupItem<T>
63
+ | SelectInputSeparatorItem;
64
+
65
+ function dedupeSelectInputOptionItem<T>(
66
+ item: SelectInputOptionItem<T>,
67
+ existingValues: Set<T>,
68
+ ): SelectInputOptionItem<T | undefined> {
69
+ if (existingValues.has(item.value)) {
70
+ return {
71
+ ...item,
72
+ value: undefined,
73
+ };
74
+ }
75
+ existingValues.add(item.value);
76
+ return item;
77
+ }
78
+
79
+ function dedupeSelectInputItems<T>(
80
+ items: readonly SelectInputItem<T>[],
81
+ ): SelectInputItem<T | undefined>[] {
82
+ const existingValues = new Set<T>();
83
+ return items.map((item) => {
84
+ switch (item.type) {
85
+ case 'option': {
86
+ return dedupeSelectInputOptionItem(item, existingValues);
87
+ }
88
+ case 'group': {
89
+ return {
90
+ ...item,
91
+ options: item.options.map((option) =>
92
+ dedupeSelectInputOptionItem(option, existingValues),
93
+ ),
94
+ };
95
+ }
96
+ default:
97
+ }
98
+ return item;
99
+ });
100
+ }
101
+
102
+ export interface SelectInputProps<T = string> {
103
+ name?: string;
104
+ placeholder?: string;
105
+ // TODO: multiple?: boolean;
106
+ items: readonly SelectInputItem<NonNullable<T>>[];
107
+ defaultValue?: T;
108
+ value?: T;
109
+ renderValue?: (value: NonNullable<T>, compact: boolean) => React.ReactNode;
110
+ compareValues?:
111
+ | (keyof NonNullable<T> & string)
112
+ | ((a: T | undefined, b: T | undefined) => boolean);
113
+ filterable?: boolean;
114
+ filterPlaceholder?: string;
115
+ disabled?: boolean;
116
+ className?: string;
117
+ onChange?: (value: T) => void;
118
+ onClear?: () => void;
119
+ }
120
+
121
+ export function SelectInput<T>({
122
+ name,
123
+ placeholder,
124
+ items,
125
+ defaultValue,
126
+ value: controlledValue,
127
+ renderValue = wrapInFragment,
128
+ compareValues,
129
+ filterable,
130
+ filterPlaceholder,
131
+ disabled,
132
+ className,
133
+ onChange,
134
+ onClear,
135
+ }: SelectInputProps<T>) {
136
+ const intl = useIntl();
137
+
138
+ const [open, setOpen] = useState(false);
139
+
140
+ const triggerRef = useRef<HTMLButtonElement>(null);
141
+
142
+ const screenSm = useScreenSize(Breakpoint.SMALL);
143
+ const OptionsOverlay = screenSm ? Popover : BottomSheet;
144
+
145
+ const searchInputRef = useRef<HTMLInputElement>(null);
146
+ const listboxRef = useRef<HTMLDivElement>(null);
147
+ const controllerRef = filterable ? searchInputRef : listboxRef;
148
+
149
+ return (
150
+ <ListboxBase
151
+ name={name}
152
+ defaultValue={defaultValue}
153
+ value={controlledValue}
154
+ // TODO: Remove assertion when upgrading TypeScript to v5
155
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
156
+ by={compareValues as any}
157
+ disabled={disabled}
158
+ onChange={(value) => {
159
+ setOpen(false);
160
+ onChange?.(value);
161
+ }}
162
+ >
163
+ {({ disabled: uiDisabled, value }) => (
164
+ <SelectInputHasValueContext.Provider value={value != null}>
165
+ <InputGroup
166
+ addonEnd={{
167
+ content: (
168
+ <span
169
+ className={classNames(
170
+ 'np-select-input-addon-container',
171
+ uiDisabled && 'disabled',
172
+ )}
173
+ >
174
+ {onClear != null && value != null ? (
175
+ <>
176
+ <button
177
+ type="button"
178
+ aria-label={intl.formatMessage(messages.ariaLabel)}
179
+ disabled={uiDisabled}
180
+ className="np-select-input-addon np-select-input-addon--interactive"
181
+ onClick={(event) => {
182
+ event.preventDefault();
183
+ onClear();
184
+ triggerRef.current?.focus({ preventScroll: true });
185
+ }}
186
+ >
187
+ <Cross size={16} />
188
+ </button>
189
+ <span className="np-select-input-addon-separator" />
190
+ </>
191
+ ) : null}
192
+
193
+ <span className="np-select-input-addon">
194
+ <ChevronDown size={16} />
195
+ </span>
196
+ </span>
197
+ ),
198
+ padding: 'sm',
199
+ }}
200
+ className={className}
201
+ >
202
+ <OptionsOverlay
203
+ open={open}
204
+ renderTrigger={({ ref, getInteractionProps }) => (
205
+ <ListboxBase.Button
206
+ ref={mergeRefs([ref, triggerRef])}
207
+ as={SelectInputButton}
208
+ overrides={getInteractionProps()}
209
+ onClick={() => {
210
+ setOpen((prev) => !prev);
211
+ }}
212
+ >
213
+ {value != null ? (
214
+ <SelectInputOptionContentCompactContext.Provider value>
215
+ {renderValue(value, true)}
216
+ </SelectInputOptionContentCompactContext.Provider>
217
+ ) : (
218
+ <span className="np-select-input-placeholder">{placeholder}</span>
219
+ )}
220
+ </ListboxBase.Button>
221
+ )}
222
+ initialFocusRef={controllerRef}
223
+ padding="none"
224
+ onClose={() => {
225
+ setOpen(false);
226
+ }}
227
+ >
228
+ <SelectInputOptions
229
+ items={items}
230
+ renderValue={renderValue}
231
+ filterable={filterable}
232
+ filterPlaceholder={filterPlaceholder}
233
+ searchInputRef={searchInputRef}
234
+ listboxRef={listboxRef}
235
+ />
236
+ </OptionsOverlay>
237
+ </InputGroup>
238
+ </SelectInputHasValueContext.Provider>
239
+ )}
240
+ </ListboxBase>
241
+ );
242
+ }
243
+
244
+ interface SelectInputButtonProps extends ButtonInputProps {
245
+ overrides?: { [key: string]: unknown };
246
+ }
247
+
248
+ const SelectInputButton = forwardRef(function SelectInputButton(
249
+ { overrides, ...restProps }: SelectInputButtonProps,
250
+ ref: React.ForwardedRef<HTMLButtonElement>,
251
+ ) {
252
+ return <ButtonInput ref={ref} {...restProps} {...overrides} />;
253
+ });
254
+
255
+ interface SelectInputOptionsContainerProps extends React.ComponentPropsWithRef<'div'> {
256
+ onAriaActiveDescendantChange: (value: React.AriaAttributes['aria-activedescendant']) => void;
257
+ }
258
+
259
+ const SelectInputOptionsContainer = forwardRef(function SelectInputOptionsContainer(
260
+ {
261
+ 'aria-orientation': ariaOrientation,
262
+ 'aria-activedescendant': ariaActiveDescendant,
263
+ role,
264
+ tabIndex,
265
+ onAriaActiveDescendantChange,
266
+ onKeyDown,
267
+ ...restProps
268
+ }: SelectInputOptionsContainerProps,
269
+ ref: React.ForwardedRef<HTMLDivElement>,
270
+ ) {
271
+ const handleAriaActiveDescendantChange = useEffectEvent(onAriaActiveDescendantChange);
272
+ useEffect(() => {
273
+ handleAriaActiveDescendantChange(ariaActiveDescendant);
274
+ }, [ariaActiveDescendant, handleAriaActiveDescendantChange]);
275
+
276
+ return (
277
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
278
+ <div
279
+ ref={ref}
280
+ onKeyDown={(event) => {
281
+ // Prevent absorbing dismissal requests too early
282
+ if (event.key !== 'Escape') {
283
+ onKeyDown?.(event);
284
+ }
285
+ }}
286
+ {...restProps}
287
+ />
288
+ );
289
+ });
290
+
291
+ interface SelectInputOptionsProps<T = string>
292
+ extends Pick<SelectInputProps<T>, 'items' | 'renderValue' | 'filterable' | 'filterPlaceholder'> {
293
+ searchInputRef: React.RefObject<HTMLInputElement>;
294
+ listboxRef: React.RefObject<HTMLDivElement>;
295
+ }
296
+
297
+ function SelectInputOptions<T>({
298
+ items,
299
+ renderValue = wrapInFragment,
300
+ filterable,
301
+ filterPlaceholder,
302
+ searchInputRef,
303
+ listboxRef,
304
+ }: SelectInputOptionsProps<T>) {
305
+ const [query, setQuery] = useState('');
306
+ const needle = useMemo(() => (query ? searchableString(query) : null), [query]);
307
+
308
+ const listboxContainerRef = useRef<HTMLDivElement>(null);
309
+ useEffect(() => {
310
+ if (listboxContainerRef.current != null) {
311
+ listboxContainerRef.current.style.setProperty(
312
+ '--initial-height',
313
+ `${listboxContainerRef.current.offsetHeight}px`,
314
+ );
315
+ }
316
+ }, []);
317
+
318
+ const listboxId = useId();
319
+
320
+ const controllerRef = filterable ? searchInputRef : listboxRef;
321
+
322
+ return (
323
+ <ListboxBase.Options
324
+ as={SelectInputOptionsContainer}
325
+ static
326
+ className="np-select-input-options-container"
327
+ onAriaActiveDescendantChange={(value: React.AriaAttributes['aria-activedescendant']) => {
328
+ if (controllerRef.current != null) {
329
+ if (value != null) {
330
+ controllerRef.current.setAttribute('aria-activedescendant', value);
331
+ } else {
332
+ controllerRef.current.removeAttribute('aria-activedescendant');
333
+ }
334
+ }
335
+ }}
336
+ >
337
+ {filterable ? (
338
+ <div className="np-select-input-query-container">
339
+ <SearchInput
340
+ ref={searchInputRef}
341
+ shape="rectangle"
342
+ placeholder={filterPlaceholder}
343
+ value={query}
344
+ aria-controls={listboxId}
345
+ onKeyDown={(event) => {
346
+ // Prevent interfering with the matcher of Headless UI
347
+ // https://mathiasbynens.be/notes/javascript-unicode#regex
348
+ if (/^.$/u.test(event.key)) {
349
+ event.stopPropagation();
350
+ }
351
+ }}
352
+ onChange={(event) => {
353
+ setQuery(event.currentTarget.value);
354
+ }}
355
+ />
356
+ </div>
357
+ ) : null}
358
+
359
+ <div
360
+ ref={listboxContainerRef}
361
+ className={classNames(
362
+ 'np-select-input-listbox-container',
363
+ items.some((item) => item.type === 'group') &&
364
+ 'np-select-input-listbox-container--has-group',
365
+ )}
366
+ >
367
+ <div
368
+ ref={listboxRef}
369
+ id={listboxId}
370
+ role="listbox"
371
+ aria-orientation="vertical"
372
+ tabIndex={0}
373
+ className="np-select-input-listbox"
374
+ >
375
+ {(needle == null ? items : dedupeSelectInputItems(items)).map((item, index) => (
376
+ <SelectInputItemView
377
+ // eslint-disable-next-line react/no-array-index-key
378
+ key={index}
379
+ item={item}
380
+ renderValue={renderValue}
381
+ needle={needle}
382
+ />
383
+ ))}
384
+ </div>
385
+ </div>
386
+ </ListboxBase.Options>
387
+ );
388
+ }
389
+
390
+ interface SelectInputItemViewProps<
391
+ T = string,
392
+ I extends SelectInputItem<T | undefined> = SelectInputItem<T | undefined>,
393
+ > extends Required<Pick<SelectInputProps<T>, 'renderValue'>> {
394
+ item: I;
395
+ needle: string | null;
396
+ }
397
+
398
+ function SelectInputItemView<T>({ item, renderValue, needle }: SelectInputItemViewProps<T>) {
399
+ switch (item.type) {
400
+ case 'option': {
401
+ if (
402
+ item.value != null &&
403
+ (!needle ||
404
+ inferSearchableStrings(item.filterMatchers ?? item.value).some((haystack) =>
405
+ haystack.includes(needle),
406
+ ))
407
+ ) {
408
+ return (
409
+ <SelectInputOption value={item.value} disabled={item.disabled}>
410
+ {renderValue(item.value, false)}
411
+ </SelectInputOption>
412
+ );
413
+ }
414
+ break;
415
+ }
416
+ case 'group': {
417
+ return <SelectInputGroupItemView item={item} renderValue={renderValue} needle={needle} />;
418
+ }
419
+ case 'separator': {
420
+ if (needle == null) {
421
+ return <hr className="np-select-input-separator-item" aria-hidden />;
422
+ }
423
+ break;
424
+ }
425
+ }
426
+ return null;
427
+ }
428
+
429
+ interface SelectInputGroupItemViewProps<T = string>
430
+ extends SelectInputItemViewProps<T, SelectInputGroupItem<T | undefined>> {}
431
+
432
+ function SelectInputGroupItemView<T>({
433
+ item,
434
+ renderValue,
435
+ needle,
436
+ }: SelectInputGroupItemViewProps<T>) {
437
+ const headerId = useId();
438
+
439
+ return (
440
+ // An empty container may be rendered when no options match `needle`
441
+ // However, pre-filtering would result in worse performance overall
442
+ <section
443
+ role="group"
444
+ aria-labelledby={headerId}
445
+ className={classNames(needle == null && 'np-select-input-group-item--without-needle')}
446
+ >
447
+ {needle == null ? (
448
+ <header
449
+ id={headerId}
450
+ role="presentation"
451
+ className="np-select-input-group-item-header np-text-title-group"
452
+ >
453
+ {item.label}
454
+ </header>
455
+ ) : null}
456
+ {item.options.map((option, index) => (
457
+ <SelectInputItemView
458
+ // eslint-disable-next-line react/no-array-index-key
459
+ key={index}
460
+ item={option}
461
+ renderValue={renderValue}
462
+ needle={needle}
463
+ />
464
+ ))}
465
+ </section>
466
+ );
467
+ }
468
+
469
+ interface SelectInputOptionProps<T = string> {
470
+ value: T;
471
+ disabled?: boolean;
472
+ children?: React.ReactNode;
473
+ }
474
+
475
+ function SelectInputOption<T>({ value, disabled, children }: SelectInputOptionProps<T>) {
476
+ const parentHasValue = useContext(SelectInputHasValueContext);
477
+
478
+ // Avoid flash during exit transition
479
+ const { current: cachedParentHasValue } = useRef(parentHasValue);
480
+
481
+ return (
482
+ <ListboxBase.Option
483
+ as="div"
484
+ value={value}
485
+ disabled={disabled}
486
+ className={({ active, disabled: uiDisabled }) =>
487
+ classNames(
488
+ 'np-select-input-option-container np-text-body-large',
489
+ active && 'np-select-input-option-container--active',
490
+ uiDisabled && 'np-select-input-option-container--disabled',
491
+ )
492
+ }
493
+ >
494
+ {({ selected }) => (
495
+ <>
496
+ {cachedParentHasValue ? (
497
+ <Check
498
+ size={16}
499
+ className={classNames(!selected && 'np-select-input-option-check--not-selected')}
500
+ />
501
+ ) : null}
502
+ <div className="np-select-input-option">{children}</div>
503
+ </>
504
+ )}
505
+ </ListboxBase.Option>
506
+ );
507
+ }
508
+
509
+ export interface SelectInputOptionContentProps {
510
+ title: string;
511
+ note?: string;
512
+ description?: string;
513
+ icon?: React.ReactNode;
514
+ }
515
+
516
+ export function SelectInputOptionContent({
517
+ title,
518
+ note,
519
+ description,
520
+ icon,
521
+ }: SelectInputOptionContentProps) {
522
+ const compact = useContext(SelectInputOptionContentCompactContext);
523
+
524
+ return (
525
+ <div className="np-select-input-option-content-container np-text-body-large">
526
+ {icon ? (
527
+ <div
528
+ className={classNames(
529
+ 'np-select-input-option-content-icon',
530
+ !compact && 'np-select-input-option-content-icon--not-compact',
531
+ )}
532
+ >
533
+ {icon}
534
+ </div>
535
+ ) : null}
536
+
537
+ <div className="np-select-input-option-content-text">
538
+ <div
539
+ className={classNames(
540
+ 'np-select-input-option-content-text-line-1',
541
+ compact && 'np-select-input-option-content-text-compact',
542
+ )}
543
+ >
544
+ <h4 className="d-inline np-text-body-large">{title}</h4>
545
+ {note ? (
546
+ <span className="np-select-input-option-content-text-secondary np-text-body-default">
547
+ {note}
548
+ </span>
549
+ ) : null}
550
+ </div>
551
+
552
+ {description ? (
553
+ <div
554
+ className={classNames(
555
+ 'np-select-input-option-content-text-secondary np-text-body-default',
556
+ compact && 'np-select-input-option-content-text-compact',
557
+ )}
558
+ >
559
+ {description}
560
+ </div>
561
+ ) : null}
562
+ </div>
563
+ </div>
564
+ );
565
+ }
@@ -1 +1 @@
1
- .np-form-control{--ring-width:1px;--ring-color:var(--color-interactive-secondary);background-color:transparent;border:none;box-shadow:inset 0 0 0 var(--ring-width) var(--ring-color);color:#37517e;color:var(--color-content-primary);min-height:0;padding-left:16px;padding-left:var(--size-16);padding-right:16px;padding-right:var(--size-16);transition-duration:.3s;transition-property:color,opacity,box-shadow;transition-timing-function:ease-in-out}.np-form-control:focus-visible{outline:none}.np-form-control[aria-invalid=true]{--ring-width:2px;--ring-color:var(--color-sentiment-negative)!important}.np-form-control:hover:enabled{--ring-width:2px;--ring-color:var(--color-interactive-secondary-hover)}.np-form-control:focus:enabled{--ring-width:3px;--ring-color:var(--color-interactive-primary)}.np-form-control--size-auto{padding-bottom:12px;padding-bottom:var(--size-12);padding-top:12px;padding-top:var(--size-12)}.np-form-control--size-lg,.np-form-control--size-md,.np-form-control--size-sm{padding-bottom:0!important;padding-top:0!important}.np-form-control--size-sm{height:32px!important;height:var(--size-32)!important}.np-form-control--size-md{height:48px!important;height:var(--size-48)!important}.np-form-control--size-lg{height:72px!important;height:var(--size-72)!important}.np-form-control--shape-rectangle{border-radius:10px;border-radius:var(--radius-small)}.np-text-area{min-height:72px;min-height:var(--size-72);overscroll-behavior:none;scroll-padding-bottom:8px;scroll-padding-bottom:var(--size-8);scroll-padding-top:8px;scroll-padding-top:var(--size-8)}
1
+ .np-form-control{--ring-width:1px;--ring-color:var(--color-interactive-secondary);background-color:transparent;border:none;box-shadow:inset 0 0 0 var(--ring-width) var(--ring-color);color:#37517e;color:var(--color-content-primary);min-height:0;padding-left:16px;padding-left:var(--size-16);padding-right:16px;padding-right:var(--size-16);transition-duration:.3s;transition-property:color,opacity,box-shadow;transition-timing-function:ease-in-out}.np-form-control:focus-visible{outline:none}.np-form-control[aria-invalid=true]{--ring-width:2px;--ring-color:var(--color-sentiment-negative)!important}.np-form-control:hover:enabled{--ring-width:2px;--ring-color:var(--color-interactive-secondary-hover)}.np-form-control:focus:enabled{--ring-width:3px;--ring-color:var(--color-interactive-primary)}.np-form-control--size-auto{padding-bottom:12px;padding-bottom:var(--size-12);padding-top:12px;padding-top:var(--size-12)}.np-form-control--size-lg,.np-form-control--size-md,.np-form-control--size-sm{padding-bottom:0!important;padding-top:0!important}.np-form-control--size-sm{height:32px!important;height:var(--size-32)!important}.np-form-control--size-md{height:48px!important;height:var(--size-48)!important}.np-form-control--size-lg{height:72px!important;height:var(--size-72)!important}.np-text-area{border-radius:10px!important;border-radius:var(--radius-small)!important;min-height:72px;min-height:var(--size-72);overscroll-behavior:none;scroll-padding-bottom:8px;scroll-padding-bottom:var(--size-8);scroll-padding-top:8px;scroll-padding-top:var(--size-8)}.np-text-area::-moz-placeholder{color:#768e9c;color:var(--color-content-tertiary)}.np-text-area::placeholder{color:#768e9c;color:var(--color-content-tertiary)}
@@ -5,4 +5,9 @@
5
5
  scroll-padding-top: var(--size-8);
6
6
  scroll-padding-bottom: var(--size-8);
7
7
  overscroll-behavior: none;
8
+ border-radius: var(--radius-small) !important;
9
+
10
+ &::placeholder {
11
+ color: var(--color-content-tertiary);
12
+ }
8
13
  }
@@ -0,0 +1,107 @@
1
+ .np-bottom-sheet-v2-container {
2
+ position: relative;
3
+ z-index: 50;
4
+ }
5
+
6
+ .np-bottom-sheet-v2-backdrop-container {
7
+ &--enter, &--leave {
8
+ transition-property: opacity;
9
+ transition-timing-function: ease-out;
10
+ transition-duration: 150ms;
11
+ }
12
+
13
+ &--enter-from, &--leave-to {
14
+ opacity: 0;
15
+ }
16
+ }
17
+
18
+ .np-bottom-sheet-v2-backdrop {
19
+ position: fixed;
20
+ inset: 0px;
21
+ background-color: var(--color-content-primary);
22
+ opacity: 0.4;
23
+ }
24
+
25
+ .np-bottom-sheet-v2 {
26
+ position: fixed;
27
+ inset: 0px;
28
+ display: flex;
29
+ flex-direction: column;
30
+ justify-content: flex-end;
31
+ padding-left: var(--size-8);
32
+ padding-right: var(--size-8);
33
+ padding-top: var(--size-64);
34
+ }
35
+
36
+ .np-bottom-sheet-v2-content {
37
+ max-height: 100%;
38
+
39
+ &--enter, &--leave {
40
+ transition-property: transform;
41
+ transition-timing-function: ease-out;
42
+ transition-duration: 300ms;
43
+
44
+ @media (prefers-reduced-motion: reduce) {
45
+ & {
46
+ transition-property: opacity;
47
+ }
48
+ }
49
+ }
50
+
51
+ &--enter-from, &--leave-to {
52
+ @media (prefers-reduced-motion: no-preference) {
53
+ & {
54
+ transform: translateY(100%);
55
+ }
56
+ }
57
+
58
+ @media (prefers-reduced-motion: reduce) {
59
+ & {
60
+ opacity: 0;
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ .np-bottom-sheet-v2-content-inner-container {
67
+ display: flex;
68
+ height: 100%;
69
+ flex-direction: column;
70
+ border-top-left-radius: 32px; /* TODO: Tokenize */
71
+ border-top-right-radius: 32px; /* TODO: Tokenize */
72
+ background-color: var(--color-background-elevated);
73
+ box-shadow: 0 0 40px rgb(69 71 69 / 0.2);
74
+
75
+ &:focus {
76
+ outline: none;
77
+ }
78
+ }
79
+
80
+ .np-bottom-sheet-v2-header {
81
+ align-self: flex-end;
82
+ padding: var(--size-16);
83
+ }
84
+
85
+ .np-bottom-sheet-v2-content-inner {
86
+ padding-top: 0px;
87
+ display: grid;
88
+ row-gap: var(--size-8);
89
+ overflow-y: auto;
90
+ grid-template-rows: repeat(1, minmax(0, 1fr));
91
+
92
+ &--has-title {
93
+ grid-template-rows: auto 1fr;
94
+ }
95
+
96
+ &--padding-md {
97
+ padding: var(--size-16);
98
+ }
99
+ }
100
+
101
+ .np-bottom-sheet-v2-title {
102
+ color: var(--color-content-primary);
103
+ }
104
+
105
+ .np-bottom-sheet-v2-body {
106
+ color: var(--color-content-secondary);
107
+ }