@ledgerhq/lumen-ui-rnative 0.1.34 → 0.1.36
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/dist/module/index.js +1 -0
- package/dist/module/index.js.map +1 -1
- package/dist/module/lib/Animations/Pulse/Pulse.js +16 -6
- package/dist/module/lib/Animations/Pulse/Pulse.js.map +1 -1
- package/dist/module/lib/Components/BottomSheet/BottomSheet.js +12 -7
- package/dist/module/lib/Components/BottomSheet/BottomSheet.js.map +1 -1
- package/dist/module/lib/Components/BottomSheet/BottomSheet.stories.js +220 -1
- package/dist/module/lib/Components/BottomSheet/BottomSheet.stories.js.map +1 -1
- package/dist/module/lib/Components/BottomSheet/BottomSheet.test.js +73 -0
- package/dist/module/lib/Components/BottomSheet/BottomSheet.test.js.map +1 -1
- package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js +1 -1
- package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js.map +1 -1
- package/dist/module/lib/Components/BottomSheet/CustomHandle.js +15 -2
- package/dist/module/lib/Components/BottomSheet/CustomHandle.js.map +1 -1
- package/dist/module/lib/Components/Card/Card.js.map +1 -1
- package/dist/module/lib/Components/DescriptionItem/DescriptionItem.js +184 -0
- package/dist/module/lib/Components/DescriptionItem/DescriptionItem.js.map +1 -0
- package/dist/module/lib/Components/DescriptionItem/DescriptionItem.mdx +139 -0
- package/dist/module/lib/Components/DescriptionItem/DescriptionItem.stories.js +258 -0
- package/dist/module/lib/Components/DescriptionItem/DescriptionItem.stories.js.map +1 -0
- package/dist/module/lib/Components/DescriptionItem/DescriptionItem.test.js +94 -0
- package/dist/module/lib/Components/DescriptionItem/DescriptionItem.test.js.map +1 -0
- package/dist/module/lib/Components/DescriptionItem/index.js +5 -0
- package/dist/module/lib/Components/DescriptionItem/index.js.map +1 -0
- package/dist/module/lib/Components/DescriptionItem/types.js +4 -0
- package/dist/module/lib/Components/DescriptionItem/types.js.map +1 -0
- package/dist/module/lib/Components/ListItem/ListItem.js.map +1 -1
- package/dist/module/lib/Components/MediaImage/MediaImage.js +5 -1
- package/dist/module/lib/Components/MediaImage/MediaImage.js.map +1 -1
- package/dist/module/lib/Components/NavBar/CoinCapsule.js +1 -0
- package/dist/module/lib/Components/NavBar/CoinCapsule.js.map +1 -1
- package/dist/module/lib/Components/OptionList/OptionList.js +45 -4
- package/dist/module/lib/Components/OptionList/OptionList.js.map +1 -1
- package/dist/module/lib/Components/OptionList/OptionList.mdx +19 -0
- package/dist/module/lib/Components/OptionList/OptionList.stories.js +254 -1
- package/dist/module/lib/Components/OptionList/OptionList.stories.js.map +1 -1
- package/dist/module/lib/Components/OptionList/OptionList.test.js +136 -1
- package/dist/module/lib/Components/OptionList/OptionList.test.js.map +1 -1
- package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.js +39 -13
- package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.js.map +1 -1
- package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.test.js +117 -2
- package/dist/module/lib/Components/OptionList/useOptionList/useOptionListItems.test.js.map +1 -1
- package/dist/module/lib/Components/PageIndicator/PageIndicator.test.js.map +1 -1
- package/dist/module/lib/Components/Skeleton/Skeleton.js +10 -3
- package/dist/module/lib/Components/Skeleton/Skeleton.js.map +1 -1
- package/dist/module/lib/Components/TabBar/TabBar.js +7 -6
- package/dist/module/lib/Components/TabBar/TabBar.js.map +1 -1
- package/dist/module/lib/Components/index.js +1 -0
- package/dist/module/lib/Components/index.js.map +1 -1
- package/dist/module/styles/lx/resolveStyle.js.map +1 -1
- package/dist/typescript/src/index.d.ts +1 -0
- package/dist/typescript/src/index.d.ts.map +1 -1
- package/dist/typescript/src/lib/Animations/Pulse/Pulse.d.ts +1 -1
- package/dist/typescript/src/lib/Animations/Pulse/Pulse.d.ts.map +1 -1
- package/dist/typescript/src/lib/Animations/Pulse/types.d.ts +2 -1
- package/dist/typescript/src/lib/Animations/Pulse/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/BottomSheet/BottomSheet.d.ts +1 -1
- package/dist/typescript/src/lib/Components/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/BottomSheet/CustomHandle.d.ts +5 -2
- package/dist/typescript/src/lib/Components/BottomSheet/CustomHandle.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/BottomSheet/types.d.ts +16 -3
- package/dist/typescript/src/lib/Components/BottomSheet/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Card/Card.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/DescriptionItem/DescriptionItem.d.ts +42 -0
- package/dist/typescript/src/lib/Components/DescriptionItem/DescriptionItem.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/DescriptionItem/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/DescriptionItem/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/DescriptionItem/types.d.ts +39 -0
- package/dist/typescript/src/lib/Components/DescriptionItem/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/ListItem/ListItem.d.ts +3 -3
- package/dist/typescript/src/lib/Components/ListItem/ListItem.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/MediaImage/MediaImage.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/OptionList/OptionList.d.ts +3 -2
- package/dist/typescript/src/lib/Components/OptionList/OptionList.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/OptionList/types.d.ts +42 -5
- package/dist/typescript/src/lib/Components/OptionList/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/OptionList/useOptionList/useOptionListItems.d.ts +9 -1
- package/dist/typescript/src/lib/Components/OptionList/useOptionList/useOptionListItems.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Skeleton/Skeleton.d.ts +1 -1
- package/dist/typescript/src/lib/Components/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/TabBar/TabBar.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/index.d.ts +1 -0
- package/dist/typescript/src/lib/Components/index.d.ts.map +1 -1
- package/dist/typescript/src/lib/types/index.d.ts +3 -3
- package/dist/typescript/src/lib/types/index.d.ts.map +1 -1
- package/dist/typescript/src/styles/lx/resolveStyle.d.ts +3 -3
- package/dist/typescript/src/styles/lx/resolveStyle.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/lib/Animations/Pulse/Pulse.tsx +38 -30
- package/src/lib/Animations/Pulse/types.ts +2 -1
- package/src/lib/Components/BottomSheet/BottomSheet.stories.tsx +174 -1
- package/src/lib/Components/BottomSheet/BottomSheet.test.tsx +59 -0
- package/src/lib/Components/BottomSheet/BottomSheet.tsx +19 -7
- package/src/lib/Components/BottomSheet/BottomSheetHeader.tsx +1 -1
- package/src/lib/Components/BottomSheet/CustomHandle.tsx +26 -5
- package/src/lib/Components/BottomSheet/types.ts +24 -3
- package/src/lib/Components/Card/Card.tsx +3 -3
- package/src/lib/Components/DescriptionItem/DescriptionItem.mdx +139 -0
- package/src/lib/Components/DescriptionItem/DescriptionItem.stories.tsx +234 -0
- package/src/lib/Components/DescriptionItem/DescriptionItem.test.tsx +112 -0
- package/src/lib/Components/DescriptionItem/DescriptionItem.tsx +224 -0
- package/src/lib/Components/DescriptionItem/index.ts +2 -0
- package/src/lib/Components/DescriptionItem/types.ts +44 -0
- package/src/lib/Components/ListItem/ListItem.tsx +3 -3
- package/src/lib/Components/MediaImage/MediaImage.tsx +5 -1
- package/src/lib/Components/NavBar/CoinCapsule.tsx +1 -0
- package/src/lib/Components/OptionList/OptionList.mdx +19 -0
- package/src/lib/Components/OptionList/OptionList.stories.tsx +254 -0
- package/src/lib/Components/OptionList/OptionList.test.tsx +143 -0
- package/src/lib/Components/OptionList/OptionList.tsx +49 -3
- package/src/lib/Components/OptionList/types.ts +46 -5
- package/src/lib/Components/OptionList/useOptionList/useOptionListItems.test.ts +124 -2
- package/src/lib/Components/OptionList/useOptionList/useOptionListItems.ts +53 -10
- package/src/lib/Components/PageIndicator/PageIndicator.test.tsx +2 -1
- package/src/lib/Components/Skeleton/Skeleton.tsx +9 -5
- package/src/lib/Components/TabBar/TabBar.tsx +3 -2
- package/src/lib/Components/index.ts +1 -0
- package/src/lib/types/index.ts +3 -3
- package/src/styles/lx/resolveStyle.ts +4 -3
|
@@ -36,6 +36,7 @@ It handles **selection state**, **automatic grouping** (via a `group` field on i
|
|
|
36
36
|
- **OptionListItemContentRow**: Horizontal row for placing elements side-by-side (e.g. title + tag)
|
|
37
37
|
- **OptionListItemText**: Styled title text
|
|
38
38
|
- **OptionListItemDescription**: Styled description text
|
|
39
|
+
- **OptionListSearch**: Search input that filters items by label (default) or via a custom `filter` function
|
|
39
40
|
|
|
40
41
|
## Properties
|
|
41
42
|
|
|
@@ -63,6 +64,24 @@ Combining grouping with rich item content:
|
|
|
63
64
|
|
|
64
65
|
<Canvas of={OptionListStories.GroupedWithContentRow} />
|
|
65
66
|
|
|
67
|
+
### Search
|
|
68
|
+
|
|
69
|
+
Drop `OptionListSearch` inside the list to enable case-insensitive filtering on item labels. Pair with `OptionListEmptyState` to handle no-match scenarios. Pass a custom `filter` to match against meta (or anything else), or `filter={null}` to disable filtering. Use `filteredItems` + `onSearchValueChange` for async/server-side search.
|
|
70
|
+
|
|
71
|
+
<Canvas of={OptionListStories.WithSearch} />
|
|
72
|
+
|
|
73
|
+
Search works within grouped items too. Empty groups are hidden automatically:
|
|
74
|
+
|
|
75
|
+
<Canvas of={OptionListStories.WithSearchAndGroups} />
|
|
76
|
+
|
|
77
|
+
Pass a custom `filter` to match against fields beyond the label. Here, the query also matches each item's ticker via `meta`:
|
|
78
|
+
|
|
79
|
+
<Canvas of={OptionListStories.WithCustomSearchFilter} />
|
|
80
|
+
|
|
81
|
+
Drive the search input from the outside with `searchValue` + `onSearchValueChange`. This is useful for syncing search state with other UI or kicking off async fetches:
|
|
82
|
+
|
|
83
|
+
<Canvas of={OptionListStories.WithControlledSearch} />
|
|
84
|
+
|
|
66
85
|
### Trigger showcase
|
|
67
86
|
|
|
68
87
|
OptionList can be opened from any trigger. `MediaButton` supports multiple appearances (`gray`, `transparent`, `no-background`), optional icons (`flat` / `rounded`), and disabled state:
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
OptionListItemContent,
|
|
23
23
|
OptionListItemDescription,
|
|
24
24
|
OptionListItemContentRow,
|
|
25
|
+
OptionListSearch,
|
|
25
26
|
OptionListTrigger,
|
|
26
27
|
OptionListItemText,
|
|
27
28
|
} from './OptionList';
|
|
@@ -38,6 +39,7 @@ const meta = {
|
|
|
38
39
|
OptionListItemText,
|
|
39
40
|
OptionListItemDescription,
|
|
40
41
|
OptionListItemContentRow,
|
|
42
|
+
OptionListSearch,
|
|
41
43
|
OptionListTrigger,
|
|
42
44
|
},
|
|
43
45
|
decorators: [
|
|
@@ -477,6 +479,258 @@ export const GroupedWithContentRow: Story = {
|
|
|
477
479
|
},
|
|
478
480
|
};
|
|
479
481
|
|
|
482
|
+
export const WithSearch: Story = {
|
|
483
|
+
render: () => {
|
|
484
|
+
const [value, setValue] = useState<string | null>(null);
|
|
485
|
+
const bottomSheetRef = useBottomSheetRef();
|
|
486
|
+
const selected = CURRENCIES.find((c) => c.value === value);
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
<>
|
|
490
|
+
<OptionListTrigger
|
|
491
|
+
label='Currency'
|
|
492
|
+
onPress={() => bottomSheetRef.current?.present()}
|
|
493
|
+
>
|
|
494
|
+
{selected && <Text lx={{ color: 'base' }}>{selected.label}</Text>}
|
|
495
|
+
</OptionListTrigger>
|
|
496
|
+
<BottomSheet
|
|
497
|
+
ref={bottomSheetRef}
|
|
498
|
+
enableDynamicSizing
|
|
499
|
+
snapPoints={null}
|
|
500
|
+
onClose={() => bottomSheetRef.current?.dismiss()}
|
|
501
|
+
>
|
|
502
|
+
<BottomSheetView>
|
|
503
|
+
<BottomSheetHeader title='Select currency' />
|
|
504
|
+
<OptionList
|
|
505
|
+
items={CURRENCIES}
|
|
506
|
+
value={value}
|
|
507
|
+
onValueChange={(v) => {
|
|
508
|
+
setValue(v);
|
|
509
|
+
bottomSheetRef.current?.dismiss();
|
|
510
|
+
}}
|
|
511
|
+
>
|
|
512
|
+
<OptionListSearch placeholder='Search currencies' />
|
|
513
|
+
<OptionListContent
|
|
514
|
+
renderItem={(item) => {
|
|
515
|
+
const ticker = (item.meta as { ticker: string }).ticker;
|
|
516
|
+
return (
|
|
517
|
+
<OptionListItem value={item.value}>
|
|
518
|
+
<OptionListItemLeading>
|
|
519
|
+
<CryptoIcon
|
|
520
|
+
ledgerId={(item.meta?.ledgerId as string) ?? ''}
|
|
521
|
+
ticker={ticker}
|
|
522
|
+
size={32}
|
|
523
|
+
/>
|
|
524
|
+
</OptionListItemLeading>
|
|
525
|
+
<OptionListItemContent>
|
|
526
|
+
<OptionListItemText>{item.label}</OptionListItemText>
|
|
527
|
+
<OptionListItemDescription>
|
|
528
|
+
{ticker}
|
|
529
|
+
</OptionListItemDescription>
|
|
530
|
+
</OptionListItemContent>
|
|
531
|
+
</OptionListItem>
|
|
532
|
+
);
|
|
533
|
+
}}
|
|
534
|
+
/>
|
|
535
|
+
<OptionListEmptyState
|
|
536
|
+
title='No currencies found'
|
|
537
|
+
description='Try a different search term'
|
|
538
|
+
/>
|
|
539
|
+
</OptionList>
|
|
540
|
+
</BottomSheetView>
|
|
541
|
+
</BottomSheet>
|
|
542
|
+
</>
|
|
543
|
+
);
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
export const WithSearchAndGroups: Story = {
|
|
548
|
+
render: () => {
|
|
549
|
+
const [value, setValue] = useState<string | null>(null);
|
|
550
|
+
const bottomSheetRef = useBottomSheetRef();
|
|
551
|
+
const selected = GROUPED_NETWORKS.find((n) => n.value === value);
|
|
552
|
+
|
|
553
|
+
return (
|
|
554
|
+
<>
|
|
555
|
+
<OptionListTrigger
|
|
556
|
+
label='Network'
|
|
557
|
+
onPress={() => bottomSheetRef.current?.present()}
|
|
558
|
+
>
|
|
559
|
+
{selected && <Text lx={{ color: 'base' }}>{selected.label}</Text>}
|
|
560
|
+
</OptionListTrigger>
|
|
561
|
+
<BottomSheet
|
|
562
|
+
ref={bottomSheetRef}
|
|
563
|
+
enableDynamicSizing
|
|
564
|
+
snapPoints={null}
|
|
565
|
+
onClose={() => bottomSheetRef.current?.dismiss()}
|
|
566
|
+
>
|
|
567
|
+
<BottomSheetScrollView>
|
|
568
|
+
<BottomSheetHeader title='Select network' />
|
|
569
|
+
<OptionList
|
|
570
|
+
items={GROUPED_NETWORKS}
|
|
571
|
+
value={value}
|
|
572
|
+
onValueChange={(v) => {
|
|
573
|
+
setValue(v);
|
|
574
|
+
bottomSheetRef.current?.dismiss();
|
|
575
|
+
}}
|
|
576
|
+
>
|
|
577
|
+
<OptionListSearch placeholder='Search networks' />
|
|
578
|
+
<OptionListContent
|
|
579
|
+
renderItem={(item) => (
|
|
580
|
+
<OptionListItem value={item.value}>
|
|
581
|
+
<OptionListItemContent>
|
|
582
|
+
<OptionListItemText>{item.label}</OptionListItemText>
|
|
583
|
+
</OptionListItemContent>
|
|
584
|
+
</OptionListItem>
|
|
585
|
+
)}
|
|
586
|
+
/>
|
|
587
|
+
<OptionListEmptyState title='No networks found' />
|
|
588
|
+
</OptionList>
|
|
589
|
+
</BottomSheetScrollView>
|
|
590
|
+
</BottomSheet>
|
|
591
|
+
</>
|
|
592
|
+
);
|
|
593
|
+
},
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
export const WithCustomSearchFilter: Story = {
|
|
597
|
+
render: () => {
|
|
598
|
+
const [value, setValue] = useState<string | null>(null);
|
|
599
|
+
const bottomSheetRef = useBottomSheetRef();
|
|
600
|
+
const selected = CURRENCIES.find((c) => c.value === value);
|
|
601
|
+
|
|
602
|
+
return (
|
|
603
|
+
<>
|
|
604
|
+
<OptionListTrigger
|
|
605
|
+
label='Currency'
|
|
606
|
+
onPress={() => bottomSheetRef.current?.present()}
|
|
607
|
+
>
|
|
608
|
+
{selected && <Text lx={{ color: 'base' }}>{selected.label}</Text>}
|
|
609
|
+
</OptionListTrigger>
|
|
610
|
+
<BottomSheet
|
|
611
|
+
ref={bottomSheetRef}
|
|
612
|
+
enableDynamicSizing
|
|
613
|
+
snapPoints={null}
|
|
614
|
+
onClose={() => bottomSheetRef.current?.dismiss()}
|
|
615
|
+
>
|
|
616
|
+
<BottomSheetView>
|
|
617
|
+
<BottomSheetHeader title='Select currency' />
|
|
618
|
+
<OptionList
|
|
619
|
+
items={CURRENCIES}
|
|
620
|
+
value={value}
|
|
621
|
+
onValueChange={(v) => {
|
|
622
|
+
setValue(v);
|
|
623
|
+
bottomSheetRef.current?.dismiss();
|
|
624
|
+
}}
|
|
625
|
+
filter={(item, query) => {
|
|
626
|
+
const q = query.toLowerCase();
|
|
627
|
+
const ticker = (item.meta as { ticker: string }).ticker;
|
|
628
|
+
return (
|
|
629
|
+
item.label.toLowerCase().includes(q) ||
|
|
630
|
+
ticker.toLowerCase().includes(q)
|
|
631
|
+
);
|
|
632
|
+
}}
|
|
633
|
+
>
|
|
634
|
+
<OptionListSearch placeholder='Search by name or ticker' />
|
|
635
|
+
<OptionListContent
|
|
636
|
+
renderItem={(item) => {
|
|
637
|
+
const ticker = (item.meta as { ticker: string }).ticker;
|
|
638
|
+
return (
|
|
639
|
+
<OptionListItem value={item.value}>
|
|
640
|
+
<OptionListItemLeading>
|
|
641
|
+
<CryptoIcon
|
|
642
|
+
ledgerId={(item.meta?.ledgerId as string) ?? ''}
|
|
643
|
+
ticker={ticker}
|
|
644
|
+
size={32}
|
|
645
|
+
/>
|
|
646
|
+
</OptionListItemLeading>
|
|
647
|
+
<OptionListItemContent>
|
|
648
|
+
<OptionListItemText>{item.label}</OptionListItemText>
|
|
649
|
+
<OptionListItemDescription>
|
|
650
|
+
{ticker}
|
|
651
|
+
</OptionListItemDescription>
|
|
652
|
+
</OptionListItemContent>
|
|
653
|
+
</OptionListItem>
|
|
654
|
+
);
|
|
655
|
+
}}
|
|
656
|
+
/>
|
|
657
|
+
<OptionListEmptyState title='No currencies found' />
|
|
658
|
+
</OptionList>
|
|
659
|
+
</BottomSheetView>
|
|
660
|
+
</BottomSheet>
|
|
661
|
+
</>
|
|
662
|
+
);
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
export const WithControlledSearch: Story = {
|
|
667
|
+
render: () => {
|
|
668
|
+
const [value, setValue] = useState<string | null>(null);
|
|
669
|
+
const [searchValue, setSearchValue] = useState('');
|
|
670
|
+
const bottomSheetRef = useBottomSheetRef();
|
|
671
|
+
const selected = CURRENCIES.find((c) => c.value === value);
|
|
672
|
+
|
|
673
|
+
return (
|
|
674
|
+
<>
|
|
675
|
+
<OptionListTrigger
|
|
676
|
+
label='Currency'
|
|
677
|
+
onPress={() => bottomSheetRef.current?.present()}
|
|
678
|
+
>
|
|
679
|
+
{selected && <Text lx={{ color: 'base' }}>{selected.label}</Text>}
|
|
680
|
+
</OptionListTrigger>
|
|
681
|
+
<BottomSheet
|
|
682
|
+
ref={bottomSheetRef}
|
|
683
|
+
enableDynamicSizing
|
|
684
|
+
snapPoints={null}
|
|
685
|
+
onClose={() => bottomSheetRef.current?.dismiss()}
|
|
686
|
+
>
|
|
687
|
+
<BottomSheetView>
|
|
688
|
+
<BottomSheetHeader title='Select currency' />
|
|
689
|
+
<Box lx={{ padding: 's8' }}>
|
|
690
|
+
<Text lx={{ color: 'muted' }}>Search: "{searchValue}"</Text>
|
|
691
|
+
</Box>
|
|
692
|
+
<OptionList
|
|
693
|
+
items={CURRENCIES}
|
|
694
|
+
value={value}
|
|
695
|
+
onValueChange={(v) => {
|
|
696
|
+
setValue(v);
|
|
697
|
+
bottomSheetRef.current?.dismiss();
|
|
698
|
+
}}
|
|
699
|
+
searchValue={searchValue}
|
|
700
|
+
onSearchValueChange={setSearchValue}
|
|
701
|
+
>
|
|
702
|
+
<OptionListSearch placeholder='Search currencies' />
|
|
703
|
+
<OptionListContent
|
|
704
|
+
renderItem={(item) => {
|
|
705
|
+
const ticker = (item.meta as { ticker: string }).ticker;
|
|
706
|
+
return (
|
|
707
|
+
<OptionListItem value={item.value}>
|
|
708
|
+
<OptionListItemLeading>
|
|
709
|
+
<CryptoIcon
|
|
710
|
+
ledgerId={(item.meta?.ledgerId as string) ?? ''}
|
|
711
|
+
ticker={ticker}
|
|
712
|
+
size={32}
|
|
713
|
+
/>
|
|
714
|
+
</OptionListItemLeading>
|
|
715
|
+
<OptionListItemContent>
|
|
716
|
+
<OptionListItemText>{item.label}</OptionListItemText>
|
|
717
|
+
<OptionListItemDescription>
|
|
718
|
+
{ticker}
|
|
719
|
+
</OptionListItemDescription>
|
|
720
|
+
</OptionListItemContent>
|
|
721
|
+
</OptionListItem>
|
|
722
|
+
);
|
|
723
|
+
}}
|
|
724
|
+
/>
|
|
725
|
+
<OptionListEmptyState title='No currencies found' />
|
|
726
|
+
</OptionList>
|
|
727
|
+
</BottomSheetView>
|
|
728
|
+
</BottomSheet>
|
|
729
|
+
</>
|
|
730
|
+
);
|
|
731
|
+
},
|
|
732
|
+
};
|
|
733
|
+
|
|
480
734
|
export const EmptyState: Story = {
|
|
481
735
|
render: () => {
|
|
482
736
|
const bottomSheetRef = useBottomSheetRef();
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
OptionListItemText,
|
|
14
14
|
OptionListItemDescription,
|
|
15
15
|
OptionListEmptyState,
|
|
16
|
+
OptionListSearch,
|
|
16
17
|
OptionListTrigger,
|
|
17
18
|
} from './OptionList';
|
|
18
19
|
import type { OptionListItemData } from './types';
|
|
@@ -321,6 +322,148 @@ describe('OptionList', () => {
|
|
|
321
322
|
});
|
|
322
323
|
});
|
|
323
324
|
|
|
325
|
+
describe('OptionListSearch', () => {
|
|
326
|
+
const renderSearchable = ({
|
|
327
|
+
items = ITEMS,
|
|
328
|
+
filter,
|
|
329
|
+
filteredItems,
|
|
330
|
+
searchValue,
|
|
331
|
+
onSearchValueChange,
|
|
332
|
+
}: {
|
|
333
|
+
items?: OptionListItemData[];
|
|
334
|
+
filter?: null | ((item: OptionListItemData, query: string) => boolean);
|
|
335
|
+
filteredItems?: OptionListItemData[];
|
|
336
|
+
searchValue?: string;
|
|
337
|
+
onSearchValueChange?: (v: string) => void;
|
|
338
|
+
} = {}) =>
|
|
339
|
+
render(
|
|
340
|
+
<TestWrapper>
|
|
341
|
+
<OptionList
|
|
342
|
+
items={items}
|
|
343
|
+
filter={filter}
|
|
344
|
+
filteredItems={filteredItems}
|
|
345
|
+
searchValue={searchValue}
|
|
346
|
+
onSearchValueChange={onSearchValueChange}
|
|
347
|
+
>
|
|
348
|
+
<OptionListSearch placeholder='Search' />
|
|
349
|
+
<OptionListContent
|
|
350
|
+
renderItem={(item) => (
|
|
351
|
+
<OptionListItem value={item.value}>
|
|
352
|
+
<OptionListItemContent>
|
|
353
|
+
<OptionListItemText>{item.label}</OptionListItemText>
|
|
354
|
+
</OptionListItemContent>
|
|
355
|
+
</OptionListItem>
|
|
356
|
+
)}
|
|
357
|
+
/>
|
|
358
|
+
<OptionListEmptyState title='No results' />
|
|
359
|
+
</OptionList>
|
|
360
|
+
</TestWrapper>,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
it('renders the search input', () => {
|
|
364
|
+
const { getByPlaceholderText } = renderSearchable();
|
|
365
|
+
|
|
366
|
+
expect(getByPlaceholderText('Search')).toBeTruthy();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('filters items with the default label filter', () => {
|
|
370
|
+
const { getByPlaceholderText, getByText, queryByText } =
|
|
371
|
+
renderSearchable();
|
|
372
|
+
|
|
373
|
+
fireEvent.changeText(getByPlaceholderText('Search'), 'alp');
|
|
374
|
+
|
|
375
|
+
expect(getByText('Alpha')).toBeTruthy();
|
|
376
|
+
expect(queryByText('Beta')).toBeNull();
|
|
377
|
+
expect(queryByText('Gamma')).toBeNull();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('default filter is case-insensitive', () => {
|
|
381
|
+
const { getByPlaceholderText, getByText, queryByText } =
|
|
382
|
+
renderSearchable();
|
|
383
|
+
|
|
384
|
+
fireEvent.changeText(getByPlaceholderText('Search'), 'BETA');
|
|
385
|
+
|
|
386
|
+
expect(getByText('Beta')).toBeTruthy();
|
|
387
|
+
expect(queryByText('Alpha')).toBeNull();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('uses a custom filter when provided', () => {
|
|
391
|
+
const filter = (item: OptionListItemData, query: string): boolean =>
|
|
392
|
+
item.value.startsWith(query);
|
|
393
|
+
|
|
394
|
+
const { getByPlaceholderText, getByText, queryByText } = renderSearchable(
|
|
395
|
+
{ filter },
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
fireEvent.changeText(getByPlaceholderText('Search'), 'b');
|
|
399
|
+
|
|
400
|
+
expect(getByText('Beta')).toBeTruthy();
|
|
401
|
+
expect(queryByText('Alpha')).toBeNull();
|
|
402
|
+
expect(queryByText('Gamma')).toBeNull();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('disables filtering when filter is null', () => {
|
|
406
|
+
const { getByPlaceholderText, getByText } = renderSearchable({
|
|
407
|
+
filter: null,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
fireEvent.changeText(getByPlaceholderText('Search'), 'alpha');
|
|
411
|
+
|
|
412
|
+
expect(getByText('Alpha')).toBeTruthy();
|
|
413
|
+
expect(getByText('Beta')).toBeTruthy();
|
|
414
|
+
expect(getByText('Gamma')).toBeTruthy();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('filters within groups and hides empty groups', () => {
|
|
418
|
+
const { getByPlaceholderText, getByText, queryByText } = renderSearchable(
|
|
419
|
+
{ items: GROUPED_ITEMS },
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
fireEvent.changeText(getByPlaceholderText('Search'), 'apple');
|
|
423
|
+
|
|
424
|
+
expect(getByText('Fruits')).toBeTruthy();
|
|
425
|
+
expect(getByText('Apple')).toBeTruthy();
|
|
426
|
+
expect(queryByText('Banana')).toBeNull();
|
|
427
|
+
expect(queryByText('Vegetables')).toBeNull();
|
|
428
|
+
expect(queryByText('Carrot')).toBeNull();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('renders empty state when no item matches', () => {
|
|
432
|
+
const { getByPlaceholderText, getByText, queryByText } =
|
|
433
|
+
renderSearchable();
|
|
434
|
+
|
|
435
|
+
fireEvent.changeText(getByPlaceholderText('Search'), 'zzz');
|
|
436
|
+
|
|
437
|
+
expect(getByText('No results')).toBeTruthy();
|
|
438
|
+
expect(queryByText('Alpha')).toBeNull();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('fires onSearchValueChange with the typed query', () => {
|
|
442
|
+
const onSearchValueChange = jest.fn();
|
|
443
|
+
const { getByPlaceholderText } = renderSearchable({
|
|
444
|
+
onSearchValueChange,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
fireEvent.changeText(getByPlaceholderText('Search'), 'be');
|
|
448
|
+
|
|
449
|
+
expect(onSearchValueChange).toHaveBeenCalledWith('be');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('uses filteredItems instead of the internal filter when provided', () => {
|
|
453
|
+
const { getByPlaceholderText, getByText, queryByText } = renderSearchable(
|
|
454
|
+
{
|
|
455
|
+
filteredItems: [{ value: 'b', label: 'Beta' }],
|
|
456
|
+
},
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
fireEvent.changeText(getByPlaceholderText('Search'), 'alpha');
|
|
460
|
+
|
|
461
|
+
expect(getByText('Beta')).toBeTruthy();
|
|
462
|
+
expect(queryByText('Alpha')).toBeNull();
|
|
463
|
+
expect(queryByText('Gamma')).toBeNull();
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
324
467
|
describe('OptionListTrigger', () => {
|
|
325
468
|
it('calls onPress when pressed', () => {
|
|
326
469
|
const onPress = jest.fn();
|
|
@@ -9,6 +9,7 @@ import { useStyleSheet } from '../../../styles';
|
|
|
9
9
|
import { Check, ChevronDown } from '../../Symbols';
|
|
10
10
|
import { useControllableState } from '../../utils/useControllableState';
|
|
11
11
|
import { Divider } from '../Divider';
|
|
12
|
+
import { SearchInput } from '../SearchInput';
|
|
12
13
|
import { Box, Pressable, Text } from '../Utility';
|
|
13
14
|
import type {
|
|
14
15
|
MetaShape,
|
|
@@ -23,6 +24,7 @@ import type {
|
|
|
23
24
|
OptionListItemContentProps,
|
|
24
25
|
OptionListItemContentRowProps,
|
|
25
26
|
OptionListEmptyStateProps,
|
|
27
|
+
OptionListSearchProps,
|
|
26
28
|
OptionListTriggerProps,
|
|
27
29
|
OptionListLabelProps,
|
|
28
30
|
} from './types';
|
|
@@ -37,6 +39,11 @@ export const OptionList = <TMeta extends MetaShape = MetaShape>({
|
|
|
37
39
|
defaultValue,
|
|
38
40
|
onValueChange,
|
|
39
41
|
disabled: disabledProp,
|
|
42
|
+
filter,
|
|
43
|
+
filteredItems,
|
|
44
|
+
searchValue,
|
|
45
|
+
defaultSearchValue,
|
|
46
|
+
onSearchValueChange,
|
|
40
47
|
children,
|
|
41
48
|
}: OptionListProps<TMeta>) => {
|
|
42
49
|
const disabled = useDisabledContext({
|
|
@@ -52,7 +59,20 @@ export const OptionList = <TMeta extends MetaShape = MetaShape>({
|
|
|
52
59
|
},
|
|
53
60
|
);
|
|
54
61
|
|
|
55
|
-
const {
|
|
62
|
+
const {
|
|
63
|
+
isGrouped,
|
|
64
|
+
groups,
|
|
65
|
+
flatItems,
|
|
66
|
+
resolvedSearchValue,
|
|
67
|
+
handleSearchValueChange,
|
|
68
|
+
} = useOptionListItems<TMeta>({
|
|
69
|
+
items,
|
|
70
|
+
filter,
|
|
71
|
+
filteredItems,
|
|
72
|
+
searchValue,
|
|
73
|
+
defaultSearchValue,
|
|
74
|
+
onSearchValueChange,
|
|
75
|
+
});
|
|
56
76
|
|
|
57
77
|
return (
|
|
58
78
|
<DisabledProvider value={{ disabled }}>
|
|
@@ -63,6 +83,8 @@ export const OptionList = <TMeta extends MetaShape = MetaShape>({
|
|
|
63
83
|
isGrouped,
|
|
64
84
|
groups,
|
|
65
85
|
flatItems,
|
|
86
|
+
resolvedSearchValue,
|
|
87
|
+
handleSearchValueChange,
|
|
66
88
|
}}
|
|
67
89
|
>
|
|
68
90
|
{children}
|
|
@@ -381,6 +403,25 @@ const OptionListLabel = ({ children }: OptionListLabelProps) => (
|
|
|
381
403
|
</Text>
|
|
382
404
|
);
|
|
383
405
|
|
|
406
|
+
export const OptionListSearch = ({ ref, ...props }: OptionListSearchProps) => {
|
|
407
|
+
const { resolvedSearchValue, handleSearchValueChange } = useOptionListContext(
|
|
408
|
+
{
|
|
409
|
+
consumerName: 'OptionListSearch',
|
|
410
|
+
contextRequired: true,
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<SearchInput
|
|
416
|
+
ref={ref}
|
|
417
|
+
value={resolvedSearchValue}
|
|
418
|
+
onChangeText={handleSearchValueChange}
|
|
419
|
+
lx={{ paddingBottom: 's8' }}
|
|
420
|
+
{...props}
|
|
421
|
+
/>
|
|
422
|
+
);
|
|
423
|
+
};
|
|
424
|
+
|
|
384
425
|
export const OptionListEmptyState = ({
|
|
385
426
|
title,
|
|
386
427
|
description,
|
|
@@ -389,10 +430,13 @@ export const OptionListEmptyState = ({
|
|
|
389
430
|
ref,
|
|
390
431
|
...props
|
|
391
432
|
}: OptionListEmptyStateProps) => {
|
|
392
|
-
const { flatItems } = useOptionListContext({
|
|
433
|
+
const { isGrouped, groups, flatItems } = useOptionListContext({
|
|
393
434
|
consumerName: 'OptionListEmptyState',
|
|
394
435
|
contextRequired: true,
|
|
395
436
|
});
|
|
437
|
+
const visibleCount = isGrouped
|
|
438
|
+
? groups.reduce((acc, g) => acc + g.items.length, 0)
|
|
439
|
+
: flatItems.length;
|
|
396
440
|
|
|
397
441
|
const styles = useStyleSheet(
|
|
398
442
|
(t) => ({
|
|
@@ -414,7 +458,9 @@ export const OptionListEmptyState = ({
|
|
|
414
458
|
[],
|
|
415
459
|
);
|
|
416
460
|
|
|
417
|
-
if (
|
|
461
|
+
if (visibleCount > 0) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
418
464
|
|
|
419
465
|
return (
|
|
420
466
|
<Box
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
StyledTextProps,
|
|
5
5
|
StyledViewProps,
|
|
6
6
|
} from '../../../styles';
|
|
7
|
+
import type { SearchInputProps } from '../SearchInput';
|
|
7
8
|
|
|
8
9
|
export type MetaShape = Record<string, unknown>;
|
|
9
10
|
|
|
@@ -37,6 +38,8 @@ export type OptionListContextValue = {
|
|
|
37
38
|
isGrouped: boolean;
|
|
38
39
|
groups: OptionListItemGroup[];
|
|
39
40
|
flatItems: OptionListItemData[];
|
|
41
|
+
resolvedSearchValue: string;
|
|
42
|
+
handleSearchValueChange: (value: string) => void;
|
|
40
43
|
};
|
|
41
44
|
|
|
42
45
|
/** Internal type -- consumers never construct this directly. */
|
|
@@ -46,16 +49,49 @@ export type OptionListItemGroup<TMeta extends MetaShape = MetaShape> = {
|
|
|
46
49
|
};
|
|
47
50
|
|
|
48
51
|
export type OptionListProps<TMeta extends MetaShape = MetaShape> = {
|
|
49
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Flat array of items.
|
|
54
|
+
* Use the `group` field on each item for automatic grouping.
|
|
55
|
+
*/
|
|
50
56
|
items: OptionListItemData<TMeta>[];
|
|
51
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The controlled selected value.
|
|
59
|
+
*/
|
|
52
60
|
value?: string | null;
|
|
53
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* The default selected value (uncontrolled)
|
|
63
|
+
*/
|
|
54
64
|
defaultValue?: string | null;
|
|
55
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Called when the selected value changes.
|
|
67
|
+
*/
|
|
56
68
|
onValueChange?: (value: string | null) => void;
|
|
57
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* When true, prevents interaction with the entire list.
|
|
71
|
+
*/
|
|
58
72
|
disabled?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Custom item/query matcher.
|
|
75
|
+
* Defaults to case-insensitive label match; `null` disables filtering.
|
|
76
|
+
*/
|
|
77
|
+
filter?: null | ((item: OptionListItemData<TMeta>, query: string) => boolean);
|
|
78
|
+
/**
|
|
79
|
+
* Pre-filtered items for async/remote search.
|
|
80
|
+
* Bypasses internal filtering.
|
|
81
|
+
*/
|
|
82
|
+
filteredItems?: OptionListItemData<TMeta>[];
|
|
83
|
+
/**
|
|
84
|
+
* Controlled search input value.
|
|
85
|
+
*/
|
|
86
|
+
searchValue?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Initial uncontrolled search value.
|
|
89
|
+
*/
|
|
90
|
+
defaultSearchValue?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Fired when search input changes.
|
|
93
|
+
*/
|
|
94
|
+
onSearchValueChange?: (value: string) => void;
|
|
59
95
|
children: ReactNode;
|
|
60
96
|
};
|
|
61
97
|
|
|
@@ -103,6 +139,11 @@ export type OptionListEmptyStateProps = {
|
|
|
103
139
|
description?: string;
|
|
104
140
|
} & Omit<StyledViewProps, 'children'>;
|
|
105
141
|
|
|
142
|
+
export type OptionListSearchProps = Omit<
|
|
143
|
+
SearchInputProps,
|
|
144
|
+
'value' | 'onChangeText' | 'defaultValue'
|
|
145
|
+
>;
|
|
146
|
+
|
|
106
147
|
export type OptionListTriggerProps = {
|
|
107
148
|
/** Floating label shown above the selected value. */
|
|
108
149
|
label?: string;
|