@transferwise/components 0.0.0-experimental-02eda23 → 0.0.0-experimental-32cf8db
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/build/index.esm.js +135 -81
- package/build/index.esm.js.map +1 -1
- package/build/index.js +136 -81
- package/build/index.js.map +1 -1
- package/build/main.css +1 -1
- package/build/styles/dateLookup/DateLookup.css +1 -1
- package/build/styles/inputs/SelectInput.css +1 -1
- package/build/styles/main.css +1 -1
- package/build/types/common/polymorphicWithOverrides/PolymorphicWithOverrides.d.ts +10 -0
- package/build/types/common/polymorphicWithOverrides/PolymorphicWithOverrides.d.ts.map +1 -0
- package/build/types/inputs/SelectInput.d.ts +13 -2
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/common/polymorphicWithOverrides/PolymorphicWithOverrides.tsx +16 -0
- package/src/dateLookup/DateLookup.css +1 -1
- package/src/dateLookup/DateLookup.less +4 -4
- package/src/dateLookup/dateHeader/DateHeader.js +1 -1
- package/src/inputs/SelectInput.css +1 -1
- package/src/inputs/SelectInput.less +2 -2
- package/src/inputs/SelectInput.story.tsx +45 -4
- package/src/inputs/SelectInput.tsx +153 -96
- package/src/inputs/_Popover.less +1 -1
- package/src/main.css +1 -1
|
@@ -2,12 +2,14 @@ import { Listbox as ListboxBase } from '@headlessui/react';
|
|
|
2
2
|
import { useId } from '@radix-ui/react-id';
|
|
3
3
|
import { Check, ChevronDown, Cross } from '@transferwise/icons';
|
|
4
4
|
import classNames from 'classnames';
|
|
5
|
-
import
|
|
5
|
+
import mergeProps from 'merge-props';
|
|
6
|
+
import { createContext, forwardRef, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
7
|
import { useIntl } from 'react-intl';
|
|
7
8
|
import mergeRefs from 'react-merge-refs';
|
|
8
9
|
|
|
9
10
|
import { useEffectEvent } from '../common/hooks/useEffectEvent';
|
|
10
11
|
import { useScreenSize } from '../common/hooks/useScreenSize';
|
|
12
|
+
import { PolymorphicWithOverrides } from '../common/polymorphicWithOverrides/PolymorphicWithOverrides';
|
|
11
13
|
import { Breakpoint } from '../common/propsValues/breakpoint';
|
|
12
14
|
import messages from '../dateLookup/dateTrigger/DateTrigger.messages';
|
|
13
15
|
import { wrapInFragment } from '../utilities/wrapInFragment';
|
|
@@ -15,7 +17,7 @@ import { wrapInFragment } from '../utilities/wrapInFragment';
|
|
|
15
17
|
import { InputGroup } from './InputGroup';
|
|
16
18
|
import { SearchInput } from './SearchInput';
|
|
17
19
|
import { BottomSheet } from './_BottomSheet';
|
|
18
|
-
import { ButtonInput
|
|
20
|
+
import { ButtonInput } from './_ButtonInput';
|
|
19
21
|
import { Popover } from './_Popover';
|
|
20
22
|
|
|
21
23
|
function searchableString(value: string) {
|
|
@@ -37,8 +39,12 @@ function inferSearchableStrings(value: unknown) {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const SelectInputHasValueContext = createContext(false);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
const SelectInputTriggerButtonPropsContext = createContext<{
|
|
43
|
+
ref?: React.ForwardedRef<HTMLButtonElement>;
|
|
44
|
+
onClick?: () => void;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
}>({});
|
|
47
|
+
const SelectInputOptionContentWithinTriggerContext = createContext(false);
|
|
42
48
|
|
|
43
49
|
interface SelectInputOptionItem<T = string> {
|
|
44
50
|
type: 'option';
|
|
@@ -106,10 +112,17 @@ export interface SelectInputProps<T = string> {
|
|
|
106
112
|
items: readonly SelectInputItem<NonNullable<T>>[];
|
|
107
113
|
defaultValue?: T;
|
|
108
114
|
value?: T;
|
|
109
|
-
renderValue?: (value: NonNullable<T>, compact: boolean) => React.ReactNode;
|
|
110
115
|
compareValues?:
|
|
111
116
|
| (keyof NonNullable<T> & string)
|
|
112
117
|
| ((a: T | undefined, b: T | undefined) => boolean);
|
|
118
|
+
renderValue?: (value: NonNullable<T>, withinTrigger: boolean) => React.ReactNode;
|
|
119
|
+
renderTrigger?: (args: {
|
|
120
|
+
content: React.ReactNode;
|
|
121
|
+
placeholderShown: boolean;
|
|
122
|
+
clear: (() => void) | undefined;
|
|
123
|
+
disabled: boolean;
|
|
124
|
+
className: string | undefined;
|
|
125
|
+
}) => React.ReactNode;
|
|
113
126
|
filterable?: boolean;
|
|
114
127
|
filterPlaceholder?: string;
|
|
115
128
|
disabled?: boolean;
|
|
@@ -118,14 +131,66 @@ export interface SelectInputProps<T = string> {
|
|
|
118
131
|
onClear?: () => void;
|
|
119
132
|
}
|
|
120
133
|
|
|
121
|
-
|
|
134
|
+
const defaultRenderTrigger = (({ content, placeholderShown, clear, disabled, className }) => (
|
|
135
|
+
<InputGroup
|
|
136
|
+
addonEnd={{
|
|
137
|
+
content: (
|
|
138
|
+
<span className={classNames('np-select-input-addon-container', disabled && 'disabled')}>
|
|
139
|
+
{clear != null && !placeholderShown ? (
|
|
140
|
+
<>
|
|
141
|
+
<SelectInputClearButton
|
|
142
|
+
onClick={(event) => {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
clear();
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
<span className="np-select-input-addon-separator" />
|
|
148
|
+
</>
|
|
149
|
+
) : null}
|
|
150
|
+
|
|
151
|
+
<span className="np-select-input-addon">
|
|
152
|
+
<ChevronDown size={16} />
|
|
153
|
+
</span>
|
|
154
|
+
</span>
|
|
155
|
+
),
|
|
156
|
+
padding: 'sm',
|
|
157
|
+
}}
|
|
158
|
+
disabled={disabled}
|
|
159
|
+
className={className}
|
|
160
|
+
>
|
|
161
|
+
<SelectInputTriggerButton as={ButtonInput}>
|
|
162
|
+
{placeholderShown ? <span className="np-select-input-placeholder"> {content}</span> : content}
|
|
163
|
+
</SelectInputTriggerButton>
|
|
164
|
+
</InputGroup>
|
|
165
|
+
)) satisfies SelectInputProps['renderTrigger'];
|
|
166
|
+
|
|
167
|
+
interface SelectInputClearButtonProps
|
|
168
|
+
extends Pick<React.ComponentPropsWithoutRef<'button'>, 'className' | 'onClick'> {}
|
|
169
|
+
|
|
170
|
+
function SelectInputClearButton({ className, onClick }: SelectInputClearButtonProps) {
|
|
171
|
+
const intl = useIntl();
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
aria-label={intl.formatMessage(messages.ariaLabel)}
|
|
177
|
+
className={classNames(className, 'np-select-input-addon np-select-input-addon--interactive')}
|
|
178
|
+
onClick={onClick}
|
|
179
|
+
>
|
|
180
|
+
<Cross size={16} />
|
|
181
|
+
</button>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function SelectInput<T = string>({
|
|
122
186
|
name,
|
|
123
187
|
placeholder,
|
|
124
188
|
items,
|
|
125
189
|
defaultValue,
|
|
126
190
|
value: controlledValue,
|
|
127
|
-
renderValue = wrapInFragment,
|
|
128
191
|
compareValues,
|
|
192
|
+
renderValue = wrapInFragment,
|
|
193
|
+
renderTrigger = defaultRenderTrigger,
|
|
129
194
|
filterable,
|
|
130
195
|
filterPlaceholder,
|
|
131
196
|
disabled,
|
|
@@ -133,8 +198,6 @@ export function SelectInput<T>({
|
|
|
133
198
|
onChange,
|
|
134
199
|
onClear,
|
|
135
200
|
}: SelectInputProps<T>) {
|
|
136
|
-
const intl = useIntl();
|
|
137
|
-
|
|
138
201
|
const [open, setOpen] = useState(false);
|
|
139
202
|
|
|
140
203
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
@@ -162,95 +225,85 @@ export function SelectInput<T>({
|
|
|
162
225
|
>
|
|
163
226
|
{({ disabled: uiDisabled, value }) => (
|
|
164
227
|
<SelectInputHasValueContext.Provider value={value != null}>
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
228
|
+
<OptionsOverlay
|
|
229
|
+
open={open}
|
|
230
|
+
renderTrigger={({ ref, getInteractionProps }) => (
|
|
231
|
+
<SelectInputTriggerButtonPropsContext.Provider
|
|
232
|
+
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
233
|
+
value={{
|
|
234
|
+
ref: mergeRefs([ref, triggerRef]),
|
|
235
|
+
...mergeProps(
|
|
236
|
+
{
|
|
237
|
+
onClick: () => {
|
|
238
|
+
setOpen((prev) => !prev);
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
getInteractionProps(),
|
|
242
|
+
),
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
{renderTrigger({
|
|
246
|
+
content:
|
|
247
|
+
value != null ? (
|
|
248
|
+
<SelectInputOptionContentWithinTriggerContext.Provider value>
|
|
249
|
+
{renderValue(value, true)}
|
|
250
|
+
</SelectInputOptionContentWithinTriggerContext.Provider>
|
|
251
|
+
) : (
|
|
252
|
+
placeholder
|
|
253
|
+
),
|
|
254
|
+
placeholderShown: value == null,
|
|
255
|
+
clear:
|
|
256
|
+
onClear != null
|
|
257
|
+
? () => {
|
|
183
258
|
onClear();
|
|
184
259
|
triggerRef.current?.focus({ preventScroll: true });
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
</span>
|
|
197
|
-
),
|
|
198
|
-
padding: 'sm',
|
|
260
|
+
}
|
|
261
|
+
: undefined,
|
|
262
|
+
disabled: uiDisabled,
|
|
263
|
+
className: classNames(className, 'np-text-body-large'),
|
|
264
|
+
})}
|
|
265
|
+
</SelectInputTriggerButtonPropsContext.Provider>
|
|
266
|
+
)}
|
|
267
|
+
initialFocusRef={controllerRef}
|
|
268
|
+
padding="none"
|
|
269
|
+
onClose={() => {
|
|
270
|
+
setOpen(false);
|
|
199
271
|
}}
|
|
200
|
-
className={className}
|
|
201
272
|
>
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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>
|
|
273
|
+
<SelectInputOptions
|
|
274
|
+
items={items}
|
|
275
|
+
renderValue={renderValue}
|
|
276
|
+
filterable={filterable}
|
|
277
|
+
filterPlaceholder={filterPlaceholder}
|
|
278
|
+
searchInputRef={searchInputRef}
|
|
279
|
+
listboxRef={listboxRef}
|
|
280
|
+
/>
|
|
281
|
+
</OptionsOverlay>
|
|
238
282
|
</SelectInputHasValueContext.Provider>
|
|
239
283
|
)}
|
|
240
284
|
</ListboxBase>
|
|
241
285
|
);
|
|
242
286
|
}
|
|
243
287
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
288
|
+
export type SelectInputTriggerButtonProps<T extends React.ComponentType | 'button' = 'button'> = {
|
|
289
|
+
as?: T;
|
|
290
|
+
} & React.ComponentPropsWithoutRef<T>;
|
|
247
291
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
) {
|
|
252
|
-
|
|
253
|
-
|
|
292
|
+
export function SelectInputTriggerButton<T extends React.ComponentType | 'button' = 'button'>({
|
|
293
|
+
as = 'button' as T,
|
|
294
|
+
...restProps
|
|
295
|
+
}: SelectInputTriggerButtonProps<T>) {
|
|
296
|
+
const { ref, onClick, ...interactionProps } = useContext(SelectInputTriggerButtonPropsContext);
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<ListboxBase.Button
|
|
300
|
+
ref={ref}
|
|
301
|
+
as={PolymorphicWithOverrides}
|
|
302
|
+
__overrides={{ as, ...interactionProps }}
|
|
303
|
+
{...mergeProps({ onClick }, restProps)}
|
|
304
|
+
/>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
254
307
|
|
|
255
308
|
interface SelectInputOptionsContainerProps extends React.ComponentPropsWithRef<'div'> {
|
|
256
309
|
onAriaActiveDescendantChange: (value: React.AriaAttributes['aria-activedescendant']) => void;
|
|
@@ -294,7 +347,7 @@ interface SelectInputOptionsProps<T = string>
|
|
|
294
347
|
listboxRef: React.RefObject<HTMLDivElement>;
|
|
295
348
|
}
|
|
296
349
|
|
|
297
|
-
function SelectInputOptions<T>({
|
|
350
|
+
function SelectInputOptions<T = string>({
|
|
298
351
|
items,
|
|
299
352
|
renderValue = wrapInFragment,
|
|
300
353
|
filterable,
|
|
@@ -395,7 +448,11 @@ interface SelectInputItemViewProps<
|
|
|
395
448
|
needle: string | null;
|
|
396
449
|
}
|
|
397
450
|
|
|
398
|
-
function SelectInputItemView<T
|
|
451
|
+
function SelectInputItemView<T = string>({
|
|
452
|
+
item,
|
|
453
|
+
renderValue,
|
|
454
|
+
needle,
|
|
455
|
+
}: SelectInputItemViewProps<T>) {
|
|
399
456
|
switch (item.type) {
|
|
400
457
|
case 'option': {
|
|
401
458
|
if (
|
|
@@ -418,7 +475,7 @@ function SelectInputItemView<T>({ item, renderValue, needle }: SelectInputItemVi
|
|
|
418
475
|
}
|
|
419
476
|
case 'separator': {
|
|
420
477
|
if (needle == null) {
|
|
421
|
-
return <hr className="np-select-input-separator-item"
|
|
478
|
+
return <hr className="np-select-input-separator-item" />;
|
|
422
479
|
}
|
|
423
480
|
break;
|
|
424
481
|
}
|
|
@@ -429,7 +486,7 @@ function SelectInputItemView<T>({ item, renderValue, needle }: SelectInputItemVi
|
|
|
429
486
|
interface SelectInputGroupItemViewProps<T = string>
|
|
430
487
|
extends SelectInputItemViewProps<T, SelectInputGroupItem<T | undefined>> {}
|
|
431
488
|
|
|
432
|
-
function SelectInputGroupItemView<T>({
|
|
489
|
+
function SelectInputGroupItemView<T = string>({
|
|
433
490
|
item,
|
|
434
491
|
renderValue,
|
|
435
492
|
needle,
|
|
@@ -472,7 +529,7 @@ interface SelectInputOptionProps<T = string> {
|
|
|
472
529
|
children?: React.ReactNode;
|
|
473
530
|
}
|
|
474
531
|
|
|
475
|
-
function SelectInputOption<T>({ value, disabled, children }: SelectInputOptionProps<T>) {
|
|
532
|
+
function SelectInputOption<T = string>({ value, disabled, children }: SelectInputOptionProps<T>) {
|
|
476
533
|
const parentHasValue = useContext(SelectInputHasValueContext);
|
|
477
534
|
|
|
478
535
|
// Avoid flash during exit transition
|
|
@@ -523,7 +580,7 @@ export function SelectInputOptionContent({
|
|
|
523
580
|
description,
|
|
524
581
|
icon,
|
|
525
582
|
}: SelectInputOptionContentProps) {
|
|
526
|
-
const
|
|
583
|
+
const withinTrigger = useContext(SelectInputOptionContentWithinTriggerContext);
|
|
527
584
|
|
|
528
585
|
return (
|
|
529
586
|
<div className="np-select-input-option-content-container np-text-body-large">
|
|
@@ -531,7 +588,7 @@ export function SelectInputOptionContent({
|
|
|
531
588
|
<div
|
|
532
589
|
className={classNames(
|
|
533
590
|
'np-select-input-option-content-icon',
|
|
534
|
-
!
|
|
591
|
+
!withinTrigger && 'np-select-input-option-content-icon--not-within-trigger',
|
|
535
592
|
)}
|
|
536
593
|
>
|
|
537
594
|
{icon}
|
|
@@ -542,7 +599,7 @@ export function SelectInputOptionContent({
|
|
|
542
599
|
<div
|
|
543
600
|
className={classNames(
|
|
544
601
|
'np-select-input-option-content-text-line-1',
|
|
545
|
-
|
|
602
|
+
withinTrigger && 'np-select-input-option-content-text-within-trigger',
|
|
546
603
|
)}
|
|
547
604
|
>
|
|
548
605
|
<h4 className="d-inline np-text-body-large">{title}</h4>
|
|
@@ -557,7 +614,7 @@ export function SelectInputOptionContent({
|
|
|
557
614
|
<div
|
|
558
615
|
className={classNames(
|
|
559
616
|
'np-select-input-option-content-text-secondary np-text-body-default',
|
|
560
|
-
|
|
617
|
+
withinTrigger && 'np-select-input-option-content-text-within-trigger',
|
|
561
618
|
)}
|
|
562
619
|
>
|
|
563
620
|
{description}
|
package/src/inputs/_Popover.less
CHANGED