@transferwise/components 0.0.0-experimental-ea80215 → 0.0.0-experimental-e4e09f5
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/dateLookup/dateTrigger/DateTrigger.js +8 -4
- package/build/dateLookup/dateTrigger/DateTrigger.js.map +1 -1
- package/build/dateLookup/dateTrigger/DateTrigger.mjs +8 -4
- package/build/dateLookup/dateTrigger/DateTrigger.mjs.map +1 -1
- package/build/field/Field.js +9 -2
- package/build/field/Field.js.map +1 -1
- package/build/field/Field.mjs +9 -2
- package/build/field/Field.mjs.map +1 -1
- package/build/i18n/en.json +3 -1
- package/build/i18n/en.json.js +3 -1
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +3 -1
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/inputs/SelectInput.js +36 -100
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs +38 -102
- package/build/inputs/SelectInput.mjs.map +1 -1
- package/build/label/Label.js +29 -1
- package/build/label/Label.js.map +1 -1
- package/build/label/Label.messages.js +15 -0
- package/build/label/Label.messages.js.map +1 -0
- package/build/label/Label.messages.mjs +13 -0
- package/build/label/Label.messages.mjs.map +1 -0
- package/build/label/Label.mjs +30 -2
- package/build/label/Label.mjs.map +1 -1
- package/build/main.css +0 -18
- package/build/styles/dateLookup/dateTrigger/DateTrigger.css +0 -8
- package/build/styles/inputs/SelectInput.css +0 -10
- package/build/styles/main.css +0 -18
- package/build/types/dateLookup/dateTrigger/DateTrigger.d.ts.map +1 -1
- package/build/types/field/Field.d.ts +4 -2
- package/build/types/field/Field.d.ts.map +1 -1
- package/build/types/index.d.ts +1 -1
- package/build/types/index.d.ts.map +1 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/label/Label.d.ts +10 -1
- package/build/types/label/Label.d.ts.map +1 -1
- package/build/types/label/Label.messages.d.ts +12 -0
- package/build/types/label/Label.messages.d.ts.map +1 -0
- package/build/types/label/index.d.ts +3 -0
- package/build/types/label/index.d.ts.map +1 -0
- package/package.json +5 -6
- package/src/dateInput/DateInput.tests.story.tsx +8 -32
- package/src/dateLookup/DateLookup.rtl.spec.tsx +1 -1
- package/src/dateLookup/dateTrigger/DateTrigger.css +0 -8
- package/src/dateLookup/dateTrigger/DateTrigger.less +0 -8
- package/src/dateLookup/dateTrigger/DateTrigger.spec.js +1 -1
- package/src/dateLookup/dateTrigger/DateTrigger.tsx +9 -4
- package/src/field/Field.spec.tsx +3 -3
- package/src/field/Field.story.tsx +81 -3
- package/src/field/Field.tsx +10 -4
- package/src/i18n/en.json +3 -1
- package/src/index.ts +1 -1
- package/src/inlineAlert/InlineAlert.story.tsx +8 -21
- package/src/inputs/InputGroup.spec.tsx +1 -1
- package/src/inputs/SearchInput.spec.tsx +1 -1
- package/src/inputs/SelectInput.css +0 -10
- package/src/inputs/SelectInput.less +0 -12
- package/src/inputs/SelectInput.spec.tsx +1 -1
- package/src/inputs/SelectInput.story.tsx +0 -20
- package/src/inputs/SelectInput.tsx +46 -139
- package/src/label/Label.messages.tsx +12 -0
- package/src/label/Label.story.tsx +30 -21
- package/src/label/Label.tsx +43 -2
- package/src/label/index.ts +2 -0
- package/src/main.css +0 -18
- package/src/radioGroup/RadioGroup.rtl.spec.tsx +1 -1
- package/src/select/Select.rtl.spec.tsx +1 -1
- package/src/switch/Switch.spec.tsx +1 -1
- package/src/field/Field.tests.story.tsx +0 -33
|
@@ -322,26 +322,6 @@ export const Advanced: Story<Month> = {
|
|
|
322
322
|
},
|
|
323
323
|
};
|
|
324
324
|
|
|
325
|
-
export const ManyItems: Story<string, true> = {
|
|
326
|
-
args: {
|
|
327
|
-
multiple: true,
|
|
328
|
-
items: Array.from({ length: 1000 }, (_, index) => ({
|
|
329
|
-
type: 'option',
|
|
330
|
-
value: String(index + 1),
|
|
331
|
-
})),
|
|
332
|
-
renderValue: (value, withinTrigger) =>
|
|
333
|
-
withinTrigger ? (
|
|
334
|
-
value
|
|
335
|
-
) : (
|
|
336
|
-
<SelectInputOptionContent
|
|
337
|
-
title={value}
|
|
338
|
-
description={Number(value) % 10 === 0 ? 'Divisible by 10' : undefined}
|
|
339
|
-
/>
|
|
340
|
-
),
|
|
341
|
-
filterable: true,
|
|
342
|
-
},
|
|
343
|
-
};
|
|
344
|
-
|
|
345
325
|
export const WithinDrawer: Story<Currency> = {
|
|
346
326
|
args: CurrenciesArgs,
|
|
347
327
|
decorators: [
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
createContext,
|
|
7
7
|
forwardRef,
|
|
8
8
|
useContext,
|
|
9
|
-
useDeferredValue,
|
|
10
9
|
useEffect,
|
|
11
10
|
useId,
|
|
12
11
|
useMemo,
|
|
@@ -14,7 +13,6 @@ import {
|
|
|
14
13
|
useState,
|
|
15
14
|
} from 'react';
|
|
16
15
|
import { useIntl } from 'react-intl';
|
|
17
|
-
import { Virtualizer } from 'virtua';
|
|
18
16
|
|
|
19
17
|
import { useEffectEvent } from '../common/hooks/useEffectEvent';
|
|
20
18
|
import { useScreenSize } from '../common/hooks/useScreenSize';
|
|
@@ -31,8 +29,6 @@ import { InputGroup } from './InputGroup';
|
|
|
31
29
|
import { SearchInput } from './SearchInput';
|
|
32
30
|
import messages from './SelectInput.messages';
|
|
33
31
|
|
|
34
|
-
const MAX_ITEMS_WITHOUT_VIRTUALIZATION = 50;
|
|
35
|
-
|
|
36
32
|
function searchableString(value: string) {
|
|
37
33
|
return value.trim().replace(/\s+/gu, ' ').normalize('NFKC').toLowerCase();
|
|
38
34
|
}
|
|
@@ -44,13 +40,22 @@ function inferSearchableStrings(value: unknown) {
|
|
|
44
40
|
|
|
45
41
|
if (typeof value === 'object' && value != null) {
|
|
46
42
|
return Object.values(value)
|
|
47
|
-
.filter((innerValue) => typeof innerValue === 'string')
|
|
43
|
+
.filter((innerValue): innerValue is string => typeof innerValue === 'string')
|
|
48
44
|
.map((innerValue) => searchableString(innerValue));
|
|
49
45
|
}
|
|
50
46
|
|
|
51
47
|
return [];
|
|
52
48
|
}
|
|
53
49
|
|
|
50
|
+
const SelectInputTriggerButtonPropsContext = createContext<{
|
|
51
|
+
ref?: React.ForwardedRef<HTMLButtonElement | null>;
|
|
52
|
+
id?: string;
|
|
53
|
+
onClick?: (event: React.MouseEvent) => void;
|
|
54
|
+
onKeyDown?: (event: React.KeyboardEvent) => void;
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
}>({});
|
|
57
|
+
const SelectInputOptionContentWithinTriggerContext = createContext(false);
|
|
58
|
+
|
|
54
59
|
export interface SelectInputOptionItem<T = string> {
|
|
55
60
|
type: 'option';
|
|
56
61
|
value: T;
|
|
@@ -84,11 +89,6 @@ function dedupeSelectInputOptionItem<T>(
|
|
|
84
89
|
return { ...item, value: undefined };
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
/**
|
|
88
|
-
* Sets the `value` of duplicate option items to `undefined`, hiding them when
|
|
89
|
-
* rendered. Indexes are kept intact within groups to preserve the active item
|
|
90
|
-
* between filter changes when possible.
|
|
91
|
-
*/
|
|
92
92
|
function dedupeSelectInputItems<T>(
|
|
93
93
|
items: readonly SelectInputItem<T>[],
|
|
94
94
|
): SelectInputItem<T | undefined>[] {
|
|
@@ -112,23 +112,20 @@ function dedupeSelectInputItems<T>(
|
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function
|
|
115
|
+
function filterSelectInputOptionItem<T>(item: SelectInputOptionItem<T>, needle: string) {
|
|
116
116
|
return inferSearchableStrings(item.filterMatchers ?? item.value).some((haystack) =>
|
|
117
117
|
haystack.includes(needle),
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
function filterSelectInputItems<T>(
|
|
122
|
-
items: readonly SelectInputItem<T>[],
|
|
123
|
-
predicate: (item: SelectInputOptionItem<T>) => boolean,
|
|
124
|
-
) {
|
|
121
|
+
function filterSelectInputItems<T>(items: readonly SelectInputItem<T>[], needle: string) {
|
|
125
122
|
return items.filter((item) => {
|
|
126
123
|
switch (item.type) {
|
|
127
124
|
case 'option': {
|
|
128
|
-
return
|
|
125
|
+
return filterSelectInputOptionItem(item, needle);
|
|
129
126
|
}
|
|
130
127
|
case 'group': {
|
|
131
|
-
return item.options.some((option) =>
|
|
128
|
+
return item.options.some((option) => filterSelectInputOptionItem(option, needle));
|
|
132
129
|
}
|
|
133
130
|
default:
|
|
134
131
|
}
|
|
@@ -274,15 +271,12 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
274
271
|
}, [handleClose, open]);
|
|
275
272
|
|
|
276
273
|
const [filterQuery, _setFilterQuery] = useState('');
|
|
277
|
-
const deferredFilterQuery = useDeferredValue(filterQuery);
|
|
278
274
|
const setFilterQuery = useEffectEvent((query: string) => {
|
|
279
275
|
_setFilterQuery(query);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
});
|
|
285
|
-
}
|
|
276
|
+
onFilterChange({
|
|
277
|
+
query,
|
|
278
|
+
queryNormalized: query ? searchableString(query) : null,
|
|
279
|
+
});
|
|
286
280
|
});
|
|
287
281
|
|
|
288
282
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
@@ -300,7 +294,9 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
300
294
|
multiple={multiple}
|
|
301
295
|
defaultValue={defaultValue}
|
|
302
296
|
value={controlledValue}
|
|
303
|
-
|
|
297
|
+
// TODO: Remove assertion when upgrading TypeScript to v5
|
|
298
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
299
|
+
by={compareValues as any}
|
|
304
300
|
disabled={disabled}
|
|
305
301
|
onChange={
|
|
306
302
|
((value) => {
|
|
@@ -353,8 +349,8 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
353
349
|
content: !placeholderShown ? (
|
|
354
350
|
<SelectInputOptionContentWithinTriggerContext.Provider value>
|
|
355
351
|
{multiple && Array.isArray(value)
|
|
356
|
-
?
|
|
357
|
-
.map((option) => renderValue(option, true))
|
|
352
|
+
? value
|
|
353
|
+
.map((option: NonNullable<T>) => renderValue(option, true))
|
|
358
354
|
.filter((node) => node != null)
|
|
359
355
|
.join(', ')
|
|
360
356
|
: renderValue(value as NonNullable<T>, true)}
|
|
@@ -383,7 +379,9 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
383
379
|
setOpen(false);
|
|
384
380
|
}}
|
|
385
381
|
onCloseEnd={() => {
|
|
386
|
-
|
|
382
|
+
if (filterQuery !== '') {
|
|
383
|
+
setFilterQuery('');
|
|
384
|
+
}
|
|
387
385
|
}}
|
|
388
386
|
>
|
|
389
387
|
<SelectInputOptions
|
|
@@ -394,7 +392,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
394
392
|
filterPlaceholder={filterPlaceholder}
|
|
395
393
|
searchInputRef={searchInputRef}
|
|
396
394
|
listboxRef={listboxRef}
|
|
397
|
-
filterQuery={
|
|
395
|
+
filterQuery={filterQuery}
|
|
398
396
|
onFilterChange={setFilterQuery}
|
|
399
397
|
/>
|
|
400
398
|
</OptionsOverlay>
|
|
@@ -404,14 +402,6 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
404
402
|
);
|
|
405
403
|
}
|
|
406
404
|
|
|
407
|
-
const SelectInputTriggerButtonPropsContext = createContext<{
|
|
408
|
-
ref?: React.ForwardedRef<HTMLButtonElement | null>;
|
|
409
|
-
id?: string;
|
|
410
|
-
onClick?: (event: React.MouseEvent) => void;
|
|
411
|
-
onKeyDown?: (event: React.KeyboardEvent) => void;
|
|
412
|
-
[key: string]: unknown;
|
|
413
|
-
}>({});
|
|
414
|
-
|
|
415
405
|
type SelectInputTriggerButtonElementType = 'button' | React.ComponentType;
|
|
416
406
|
|
|
417
407
|
export type SelectInputTriggerButtonProps<
|
|
@@ -516,48 +506,7 @@ function SelectInputOptions<T = string>({
|
|
|
516
506
|
}
|
|
517
507
|
return undefined;
|
|
518
508
|
}, [filterQuery, filterable]);
|
|
519
|
-
|
|
520
|
-
if (needle) {
|
|
521
|
-
// Ensure having an active option while filtering
|
|
522
|
-
requestAnimationFrame(() => {
|
|
523
|
-
if (
|
|
524
|
-
controllerRef.current != null &&
|
|
525
|
-
!controllerRef.current.hasAttribute('aria-activedescendant')
|
|
526
|
-
) {
|
|
527
|
-
// Activate first option via synthetic key press
|
|
528
|
-
controllerRef.current.dispatchEvent(
|
|
529
|
-
new KeyboardEvent('keydown', { key: 'Home', bubbles: true }),
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
}, [controllerRef, needle]);
|
|
535
|
-
|
|
536
|
-
const filteredItems: readonly SelectInputItem<NonNullable<T> | undefined>[] =
|
|
537
|
-
needle != null
|
|
538
|
-
? filterSelectInputItems(dedupeSelectInputItems(items), (item) =>
|
|
539
|
-
selectInputOptionItemIncludesNeedle(item, needle),
|
|
540
|
-
)
|
|
541
|
-
: items;
|
|
542
|
-
const resultsEmpty = needle != null && filteredItems.length === 0;
|
|
543
|
-
|
|
544
|
-
const virtualized = filteredItems.length > MAX_ITEMS_WITHOUT_VIRTUALIZATION;
|
|
545
|
-
|
|
546
|
-
// Items shown once shall be kept mounted until the needle changes, otherwise
|
|
547
|
-
// the scroll position may jump around inadvertently. Pattern adopted from:
|
|
548
|
-
// https://inokawa.github.io/virtua/?path=/story/advanced-keep-offscreen-items--append-only
|
|
549
|
-
const [mountedIndexes, setMountedIndexes] = useState<number[]>([]);
|
|
550
|
-
useEffect(() => {
|
|
551
|
-
// Ensure the 'End' key works as intended by keeping the last item mounted
|
|
552
|
-
setMountedIndexes((prevMountedIndexes) => {
|
|
553
|
-
const indexes = new Set(prevMountedIndexes);
|
|
554
|
-
indexes.add(filteredItems.length - 1);
|
|
555
|
-
return [...indexes]; // Sorting is redundant by nature here
|
|
556
|
-
});
|
|
557
|
-
}, [
|
|
558
|
-
needle, // Needed as `filteredItems.length` may be equal between two updates
|
|
559
|
-
filteredItems.length,
|
|
560
|
-
]);
|
|
509
|
+
const resultsEmpty = needle != null && filterSelectInputItems(items, needle).length === 0;
|
|
561
510
|
|
|
562
511
|
const listboxContainerRef = useRef<HTMLDivElement>(null);
|
|
563
512
|
useEffect(() => {
|
|
@@ -573,19 +522,6 @@ function SelectInputOptions<T = string>({
|
|
|
573
522
|
const statusId = useId();
|
|
574
523
|
const listboxId = useId();
|
|
575
524
|
|
|
576
|
-
const getItemNode = (index: number) => {
|
|
577
|
-
const item = filteredItems[index];
|
|
578
|
-
return (
|
|
579
|
-
<SelectInputItemView
|
|
580
|
-
// eslint-disable-next-line react/no-array-index-key
|
|
581
|
-
key={index}
|
|
582
|
-
item={item}
|
|
583
|
-
renderValue={renderValue}
|
|
584
|
-
needle={needle}
|
|
585
|
-
/>
|
|
586
|
-
);
|
|
587
|
-
};
|
|
588
|
-
|
|
589
525
|
return (
|
|
590
526
|
<ListboxBase.Options
|
|
591
527
|
as={SelectInputOptionsContainer}
|
|
@@ -597,6 +533,12 @@ function SelectInputOptions<T = string>({
|
|
|
597
533
|
controllerRef.current.setAttribute('aria-activedescendant', value);
|
|
598
534
|
} else {
|
|
599
535
|
controllerRef.current.removeAttribute('aria-activedescendant');
|
|
536
|
+
if (filterQuery) {
|
|
537
|
+
// Ensure having an active option while filtering
|
|
538
|
+
controllerRef.current.dispatchEvent(
|
|
539
|
+
new KeyboardEvent('keydown', { key: 'Home', bubbles: true }),
|
|
540
|
+
);
|
|
541
|
+
}
|
|
600
542
|
}
|
|
601
543
|
}
|
|
602
544
|
}}
|
|
@@ -607,7 +549,7 @@ function SelectInputOptions<T = string>({
|
|
|
607
549
|
ref={searchInputRef}
|
|
608
550
|
shape="rectangle"
|
|
609
551
|
placeholder={filterPlaceholder}
|
|
610
|
-
|
|
552
|
+
value={filterQuery}
|
|
611
553
|
aria-controls={listboxId}
|
|
612
554
|
aria-describedby={showStatus ? statusId : undefined}
|
|
613
555
|
onKeyDown={(event) => {
|
|
@@ -618,9 +560,6 @@ function SelectInputOptions<T = string>({
|
|
|
618
560
|
}
|
|
619
561
|
}}
|
|
620
562
|
onChange={(event) => {
|
|
621
|
-
// Free up resources and ensure not to go out of bounds when the
|
|
622
|
-
// resulting item count is less than before
|
|
623
|
-
setMountedIndexes([]);
|
|
624
563
|
onFilterChange(event.currentTarget.value);
|
|
625
564
|
}}
|
|
626
565
|
/>
|
|
@@ -632,9 +571,7 @@ function SelectInputOptions<T = string>({
|
|
|
632
571
|
tabIndex={-1}
|
|
633
572
|
className={clsx(
|
|
634
573
|
'np-select-input-listbox-container',
|
|
635
|
-
|
|
636
|
-
needle == null && // Groups aren't shown when filtering
|
|
637
|
-
items.some((item) => item.type === 'group') &&
|
|
574
|
+
items.some((item) => item.type === 'group') &&
|
|
638
575
|
'np-select-input-listbox-container--has-group',
|
|
639
576
|
)}
|
|
640
577
|
>
|
|
@@ -653,33 +590,15 @@ function SelectInputOptions<T = string>({
|
|
|
653
590
|
tabIndex={0}
|
|
654
591
|
className="np-select-input-listbox"
|
|
655
592
|
>
|
|
656
|
-
{
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
setMountedIndexes((prevMountedIndexes) => {
|
|
666
|
-
const indexes = new Set(prevMountedIndexes);
|
|
667
|
-
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
668
|
-
indexes.add(index);
|
|
669
|
-
}
|
|
670
|
-
return [...indexes].sort((a, b) => a - b);
|
|
671
|
-
});
|
|
672
|
-
}}
|
|
673
|
-
>
|
|
674
|
-
{(index) => (
|
|
675
|
-
<SelectInputItemsCountContext.Provider value={filteredItems.length}>
|
|
676
|
-
<SelectInputItemPositionContext.Provider value={index + 1}>
|
|
677
|
-
{getItemNode(index)}
|
|
678
|
-
</SelectInputItemPositionContext.Provider>
|
|
679
|
-
</SelectInputItemsCountContext.Provider>
|
|
680
|
-
)}
|
|
681
|
-
</Virtualizer>
|
|
682
|
-
)}
|
|
593
|
+
{(needle != null ? dedupeSelectInputItems(items) : items).map((item, index) => (
|
|
594
|
+
<SelectInputItemView
|
|
595
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
596
|
+
key={index}
|
|
597
|
+
item={item}
|
|
598
|
+
renderValue={renderValue}
|
|
599
|
+
needle={needle}
|
|
600
|
+
/>
|
|
601
|
+
))}
|
|
683
602
|
</div>
|
|
684
603
|
|
|
685
604
|
{renderFooter != null ? (
|
|
@@ -720,10 +639,7 @@ function SelectInputItemView<T = string>({
|
|
|
720
639
|
}: SelectInputItemViewProps<T>) {
|
|
721
640
|
switch (item.type) {
|
|
722
641
|
case 'option': {
|
|
723
|
-
if (
|
|
724
|
-
item.value != null &&
|
|
725
|
-
(needle == null || selectInputOptionItemIncludesNeedle(item, needle))
|
|
726
|
-
) {
|
|
642
|
+
if (item.value != null && (needle == null || filterSelectInputOptionItem(item, needle))) {
|
|
727
643
|
return (
|
|
728
644
|
<SelectInputOption value={item.value} disabled={item.disabled}>
|
|
729
645
|
{renderValue(item.value, false)}
|
|
@@ -785,9 +701,6 @@ function SelectInputGroupItemView<T = string>({
|
|
|
785
701
|
);
|
|
786
702
|
}
|
|
787
703
|
|
|
788
|
-
const SelectInputItemsCountContext = createContext<number | undefined>(undefined);
|
|
789
|
-
const SelectInputItemPositionContext = createContext<number | undefined>(undefined);
|
|
790
|
-
|
|
791
704
|
interface SelectInputOptionProps<T = string> {
|
|
792
705
|
value: T;
|
|
793
706
|
disabled?: boolean;
|
|
@@ -795,14 +708,10 @@ interface SelectInputOptionProps<T = string> {
|
|
|
795
708
|
}
|
|
796
709
|
|
|
797
710
|
function SelectInputOption<T = string>({ value, disabled, children }: SelectInputOptionProps<T>) {
|
|
798
|
-
const itemsCount = useContext(SelectInputItemsCountContext);
|
|
799
|
-
const itemPosition = useContext(SelectInputItemPositionContext);
|
|
800
711
|
return (
|
|
801
712
|
<ListboxBase.Option
|
|
802
713
|
as="div"
|
|
803
714
|
value={value}
|
|
804
|
-
aria-setsize={itemsCount}
|
|
805
|
-
aria-posinset={itemPosition}
|
|
806
715
|
disabled={disabled}
|
|
807
716
|
className={({ active, disabled: uiDisabled }) =>
|
|
808
717
|
clsx(
|
|
@@ -828,8 +737,6 @@ function SelectInputOption<T = string>({ value, disabled, children }: SelectInpu
|
|
|
828
737
|
);
|
|
829
738
|
}
|
|
830
739
|
|
|
831
|
-
const SelectInputOptionContentWithinTriggerContext = createContext(false);
|
|
832
|
-
|
|
833
740
|
export interface SelectInputOptionContentProps {
|
|
834
741
|
title: string;
|
|
835
742
|
note?: string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineMessages } from 'react-intl';
|
|
2
|
+
|
|
3
|
+
export default defineMessages({
|
|
4
|
+
optionalLabel: {
|
|
5
|
+
id: 'neptune.Label.optional',
|
|
6
|
+
defaultMessage: '(Optional)',
|
|
7
|
+
},
|
|
8
|
+
optionalAriaLabel: {
|
|
9
|
+
id: 'neptune.aria.Label.optional',
|
|
10
|
+
defaultMessage: 'This field is optional',
|
|
11
|
+
},
|
|
12
|
+
});
|
|
@@ -1,37 +1,46 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import Info from '../info/Info';
|
|
4
3
|
import { Input } from '../inputs/Input';
|
|
5
4
|
import { Label } from './Label';
|
|
5
|
+
import InlineAlert from '../inlineAlert/InlineAlert';
|
|
6
|
+
import { lorem10 } from '../test-utils';
|
|
6
7
|
|
|
7
8
|
export default {
|
|
8
9
|
component: Label,
|
|
9
10
|
title: 'Label',
|
|
11
|
+
tags: ['autodocs'],
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export const Basic = () => {
|
|
13
15
|
const [value, setValue] = useState<string | undefined>('This is some text');
|
|
14
16
|
return (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
17
|
+
<>
|
|
18
|
+
<Label className="m-b-2">
|
|
19
|
+
Phone number
|
|
20
|
+
<Input value={value} id="input" onChange={({ target }) => setValue(target.value)} />
|
|
21
|
+
</Label>
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
<Label className="m-b-2">
|
|
24
|
+
<Label.Optional>Phone number</Label.Optional>
|
|
25
|
+
<Input value={value} id="input" onChange={({ target }) => setValue(target.value)} />
|
|
26
|
+
</Label>
|
|
27
|
+
|
|
28
|
+
<Label className="m-b-2">
|
|
29
|
+
<Label.Optional>Phone number</Label.Optional>
|
|
30
|
+
<Label.Description>This an field Description</Label.Description>
|
|
31
|
+
<Input value={value} id="input" onChange={({ target }) => setValue(target.value)} />
|
|
32
|
+
</Label>
|
|
33
|
+
|
|
34
|
+
<Label htmlFor="phone-number-1">
|
|
35
|
+
<Label.Optional>Phone number</Label.Optional>
|
|
36
|
+
<Label.Description>This an field Description</Label.Description>
|
|
37
|
+
</Label>
|
|
38
|
+
<Input
|
|
39
|
+
id="phone-number-1"
|
|
40
|
+
className="m-b-2"
|
|
41
|
+
value={value}
|
|
42
|
+
onChange={({ target }) => setValue(target.value)}
|
|
43
|
+
/>
|
|
44
|
+
</>
|
|
36
45
|
);
|
|
37
46
|
};
|
package/src/label/Label.tsx
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { clsx } from 'clsx';
|
|
2
|
+
import messages from './Label.messages';
|
|
3
|
+
import { useIntl } from 'react-intl';
|
|
4
|
+
import Body from '../body';
|
|
5
|
+
import { CommonProps } from '../common';
|
|
6
|
+
import { PropsWithChildren } from 'react';
|
|
2
7
|
|
|
3
8
|
export type LabelProps = {
|
|
4
9
|
id?: string;
|
|
@@ -7,14 +12,50 @@ export type LabelProps = {
|
|
|
7
12
|
children?: React.ReactNode;
|
|
8
13
|
};
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
const Label = ({ id, htmlFor, className, children }: LabelProps) => {
|
|
11
16
|
return (
|
|
12
17
|
<label
|
|
13
18
|
id={id}
|
|
14
19
|
htmlFor={htmlFor}
|
|
15
|
-
className={clsx(
|
|
20
|
+
className={clsx(
|
|
21
|
+
'd-flex',
|
|
22
|
+
'flex-column',
|
|
23
|
+
'gap-y-4',
|
|
24
|
+
'm-b-0',
|
|
25
|
+
'np-text-body-default-bold',
|
|
26
|
+
'text-primary',
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
16
29
|
>
|
|
17
30
|
{children}
|
|
18
31
|
</label>
|
|
19
32
|
);
|
|
20
33
|
};
|
|
34
|
+
|
|
35
|
+
export type LabelOptionalProps = PropsWithChildren<CommonProps>;
|
|
36
|
+
|
|
37
|
+
const Optional = ({ children, className }: LabelOptionalProps) => {
|
|
38
|
+
const { formatMessage } = useIntl();
|
|
39
|
+
return (
|
|
40
|
+
<div>
|
|
41
|
+
{children}
|
|
42
|
+
<Body
|
|
43
|
+
as="span"
|
|
44
|
+
aria-label={formatMessage(messages.optionalAriaLabel)}
|
|
45
|
+
className={clsx('text-secondary', 'm-l-1', className)}
|
|
46
|
+
>
|
|
47
|
+
{formatMessage(messages.optionalLabel)}
|
|
48
|
+
</Body>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type LabelDescriptionProps = PropsWithChildren<CommonProps>;
|
|
54
|
+
|
|
55
|
+
const Description = ({ children, className }: LabelDescriptionProps) =>
|
|
56
|
+
children ? <Body className={clsx('text-secondary', className)}>{children}</Body> : null;
|
|
57
|
+
|
|
58
|
+
Label.Optional = Optional;
|
|
59
|
+
Label.Description = Description;
|
|
60
|
+
|
|
61
|
+
export { Label };
|
package/src/main.css
CHANGED
|
@@ -1719,18 +1719,10 @@ button.np-option {
|
|
|
1719
1719
|
white-space: nowrap;
|
|
1720
1720
|
width: 100%;
|
|
1721
1721
|
}
|
|
1722
|
-
.np-date-trigger .control-label {
|
|
1723
|
-
font-weight: 400;
|
|
1724
|
-
font-weight: var(--font-weight-regular);
|
|
1725
|
-
}
|
|
1726
1722
|
.np-theme-personal .np-date-trigger {
|
|
1727
1723
|
padding-left: 16px;
|
|
1728
1724
|
padding-left: var(--size-16);
|
|
1729
1725
|
}
|
|
1730
|
-
.np-theme-personal .np-date-trigger .control-label + span {
|
|
1731
|
-
font-weight: 400;
|
|
1732
|
-
font-weight: var(--font-weight-regular);
|
|
1733
|
-
}
|
|
1734
1726
|
.clear-btn {
|
|
1735
1727
|
transition: color 0.15s ease-in-out;
|
|
1736
1728
|
color: #c9cbce;
|
|
@@ -2655,10 +2647,6 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
|
|
|
2655
2647
|
height: auto;
|
|
2656
2648
|
}
|
|
2657
2649
|
}
|
|
2658
|
-
.np-select-input-listbox-container--virtualized {
|
|
2659
|
-
/* The wrapping element shrinks this as needed */
|
|
2660
|
-
height: 100vh;
|
|
2661
|
-
}
|
|
2662
2650
|
.np-select-input-listbox-container--has-group {
|
|
2663
2651
|
scroll-padding-top: 32px;
|
|
2664
2652
|
scroll-padding-top: var(--size-32);
|
|
@@ -2677,12 +2665,6 @@ html:not([dir="rtl"]) .np-flow-navigation--sm .np-flow-navigation__stepper {
|
|
|
2677
2665
|
outline: var(--ring-outline-color) solid var(--ring-outline-width);
|
|
2678
2666
|
outline-offset: var(--ring-outline-offset);
|
|
2679
2667
|
}
|
|
2680
|
-
.np-select-input-listbox-container--virtualized .np-select-input-listbox {
|
|
2681
|
-
/* Adopted from `VList` in virtua: https://github.com/inokawa/virtua/blob/7f6ed5b37df6b480d4ff350f3960067c5b3519d2/src/react/VList.tsx#L113-L116 */
|
|
2682
|
-
overflow-y: auto;
|
|
2683
|
-
contain: strict;
|
|
2684
|
-
height: 100%;
|
|
2685
|
-
}
|
|
2686
2668
|
.np-select-input-separator-item {
|
|
2687
2669
|
margin: 8px;
|
|
2688
2670
|
margin: var(--size-8);
|
|
@@ -12,6 +12,6 @@ describe('Select', () => {
|
|
|
12
12
|
<Select options={options} selected={options[0]} onChange={() => {}} />
|
|
13
13
|
</Field>,
|
|
14
14
|
);
|
|
15
|
-
expect(screen.getByLabelText(
|
|
15
|
+
expect(screen.getByLabelText(/Currency/)).toHaveTextContent('USD');
|
|
16
16
|
});
|
|
17
17
|
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
import { Input } from '../inputs/Input';
|
|
4
|
-
import { Field } from './Field';
|
|
5
|
-
import { Sentiment } from '../common';
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
component: Field,
|
|
9
|
-
title: 'Field/Tests',
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const WithHelpAndErrorOnBlur = () => {
|
|
13
|
-
const [value, setValue] = useState('This is some text');
|
|
14
|
-
const [error, setError] = useState<string | undefined>(undefined);
|
|
15
|
-
return (
|
|
16
|
-
<Field
|
|
17
|
-
label="Phone number"
|
|
18
|
-
sentiment={error ? Sentiment.NEGATIVE : Sentiment.NEUTRAL}
|
|
19
|
-
message={error || 'Please include country code'}
|
|
20
|
-
>
|
|
21
|
-
<Input
|
|
22
|
-
value={value}
|
|
23
|
-
onChange={({ target }) => {
|
|
24
|
-
setValue(target.value);
|
|
25
|
-
setError(undefined);
|
|
26
|
-
}}
|
|
27
|
-
onBlur={() => {
|
|
28
|
-
setError('Something went wrong');
|
|
29
|
-
}}
|
|
30
|
-
/>
|
|
31
|
-
</Field>
|
|
32
|
-
);
|
|
33
|
-
};
|