@ledgerhq/lumen-ui-rnative 0.1.34 → 0.1.35
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 +17 -7
- 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/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/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/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/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/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 +34 -29
- 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/ListItem/ListItem.tsx +3 -3
- package/src/lib/Components/MediaImage/MediaImage.tsx +5 -1
- 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/types/index.ts +3 -3
- package/src/styles/lx/resolveStyle.ts +4 -3
|
@@ -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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect } from '@jest/globals';
|
|
2
|
-
import { renderHook } from '@testing-library/react-native';
|
|
1
|
+
import { describe, it, expect, jest } from '@jest/globals';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react-native';
|
|
3
3
|
import type { OptionListItemData } from '../types';
|
|
4
4
|
import { useOptionListItems } from './useOptionListItems';
|
|
5
5
|
|
|
@@ -70,4 +70,126 @@ describe('useOptionListItems', () => {
|
|
|
70
70
|
expect(result.current.groups[1].label).toBe('Fruits');
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
describe('filtering', () => {
|
|
75
|
+
it('applies the default case-insensitive label filter', () => {
|
|
76
|
+
const { result } = renderHook(() =>
|
|
77
|
+
useOptionListItems({ items: [btc, eth] }),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
act(() => {
|
|
81
|
+
result.current.handleSearchValueChange('BITCOIN');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(result.current.flatItems).toEqual([btc]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('filters within groups and removes empty groups', () => {
|
|
88
|
+
const { result } = renderHook(() =>
|
|
89
|
+
useOptionListItems({ items: [apple, banana, carrot, spinach] }),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
act(() => {
|
|
93
|
+
result.current.handleSearchValueChange('apple');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(result.current.groups).toEqual([
|
|
97
|
+
{ label: 'Fruits', items: [apple] },
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('uses a custom filter when provided', () => {
|
|
102
|
+
const customFilter = (item: OptionListItemData, query: string): boolean =>
|
|
103
|
+
item.value.startsWith(query);
|
|
104
|
+
|
|
105
|
+
const { result } = renderHook(() =>
|
|
106
|
+
useOptionListItems({ items: [btc, eth], filter: customFilter }),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
act(() => {
|
|
110
|
+
result.current.handleSearchValueChange('et');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(result.current.flatItems).toEqual([eth]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('disables filtering when filter is null', () => {
|
|
117
|
+
const { result } = renderHook(() =>
|
|
118
|
+
useOptionListItems({ items: [btc, eth], filter: null }),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
act(() => {
|
|
122
|
+
result.current.handleSearchValueChange('bitcoin');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(result.current.flatItems).toEqual([btc, eth]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('returns all items when the query is whitespace-only', () => {
|
|
129
|
+
const { result } = renderHook(() =>
|
|
130
|
+
useOptionListItems({ items: [btc, eth] }),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
act(() => {
|
|
134
|
+
result.current.handleSearchValueChange(' ');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(result.current.flatItems).toEqual([btc, eth]);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('external filteredItems', () => {
|
|
142
|
+
it('uses filteredItems instead of internal filtering', () => {
|
|
143
|
+
const { result } = renderHook(() =>
|
|
144
|
+
useOptionListItems({
|
|
145
|
+
items: [btc, eth],
|
|
146
|
+
filteredItems: [eth],
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(result.current.flatItems).toEqual([eth]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('groups filteredItems when items are grouped', () => {
|
|
154
|
+
const { result } = renderHook(() =>
|
|
155
|
+
useOptionListItems({
|
|
156
|
+
items: [apple, banana, carrot],
|
|
157
|
+
filteredItems: [apple, carrot],
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(result.current.isGrouped).toBe(true);
|
|
162
|
+
expect(result.current.groups).toEqual([
|
|
163
|
+
{ label: 'Fruits', items: [apple] },
|
|
164
|
+
{ label: 'Vegetables', items: [carrot] },
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('searchValue', () => {
|
|
170
|
+
it('calls onSearchValueChange with the new value', () => {
|
|
171
|
+
const onSearchValueChange = jest.fn();
|
|
172
|
+
const { result } = renderHook(() =>
|
|
173
|
+
useOptionListItems({ items: [btc, eth], onSearchValueChange }),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
act(() => {
|
|
177
|
+
result.current.handleSearchValueChange('test');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(onSearchValueChange).toHaveBeenCalledWith('test');
|
|
181
|
+
expect(result.current.resolvedSearchValue).toBe('test');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('uses defaultSearchValue as the initial uncontrolled value', () => {
|
|
185
|
+
const { result } = renderHook(() =>
|
|
186
|
+
useOptionListItems({
|
|
187
|
+
items: [btc, eth],
|
|
188
|
+
defaultSearchValue: 'default',
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(result.current.resolvedSearchValue).toBe('default');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
73
195
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
+
import { useControllableState } from '../../../utils/useControllableState';
|
|
2
3
|
import type {
|
|
3
4
|
MetaShape,
|
|
4
5
|
OptionListItemData,
|
|
@@ -24,26 +25,68 @@ const groupByField = <TMeta extends MetaShape = MetaShape>(
|
|
|
24
25
|
const hasGroups = (items: OptionListItemData[]): boolean =>
|
|
25
26
|
items.some((item) => item.group != null);
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return isGrouped
|
|
32
|
-
? { isGrouped: true, groups: groupByField(items), flatItems: [] }
|
|
33
|
-
: { isGrouped: false, groups: [], flatItems: items };
|
|
34
|
-
};
|
|
28
|
+
export const defaultLabelFilter = (
|
|
29
|
+
item: OptionListItemData,
|
|
30
|
+
query: string,
|
|
31
|
+
): boolean => item.label.toLowerCase().includes(query.toLowerCase());
|
|
35
32
|
|
|
36
33
|
type UseOptionListItemsParams<TMeta extends MetaShape = MetaShape> = {
|
|
37
34
|
items: OptionListItemData<TMeta>[];
|
|
35
|
+
filter?: null | ((item: OptionListItemData<TMeta>, query: string) => boolean);
|
|
36
|
+
filteredItems?: OptionListItemData<TMeta>[];
|
|
37
|
+
searchValue?: string;
|
|
38
|
+
defaultSearchValue?: string;
|
|
39
|
+
onSearchValueChange?: (value: string) => void;
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
type UseOptionListItemsResult<TMeta extends MetaShape = MetaShape> = {
|
|
41
43
|
isGrouped: boolean;
|
|
42
44
|
groups: OptionListItemGroup<TMeta>[];
|
|
43
45
|
flatItems: OptionListItemData<TMeta>[];
|
|
46
|
+
resolvedSearchValue: string;
|
|
47
|
+
handleSearchValueChange: (val: string) => void;
|
|
44
48
|
};
|
|
45
49
|
|
|
46
50
|
export const useOptionListItems = <TMeta extends MetaShape = MetaShape>({
|
|
47
51
|
items,
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
filter,
|
|
53
|
+
filteredItems,
|
|
54
|
+
searchValue: searchValueProp,
|
|
55
|
+
defaultSearchValue,
|
|
56
|
+
onSearchValueChange,
|
|
57
|
+
}: UseOptionListItemsParams<TMeta>): UseOptionListItemsResult<TMeta> => {
|
|
58
|
+
const [searchValue, handleSearchValueChange] = useControllableState<string>({
|
|
59
|
+
prop: searchValueProp,
|
|
60
|
+
defaultProp: defaultSearchValue ?? '',
|
|
61
|
+
onChange: onSearchValueChange,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const isGrouped = useMemo(() => hasGroups(items), [items]);
|
|
65
|
+
|
|
66
|
+
const visibleItems = useMemo(() => {
|
|
67
|
+
if (filteredItems) {
|
|
68
|
+
return filteredItems;
|
|
69
|
+
}
|
|
70
|
+
const query = searchValue.trim();
|
|
71
|
+
const activeFilter = filter === undefined ? defaultLabelFilter : filter;
|
|
72
|
+
if (!activeFilter || !query) {
|
|
73
|
+
return items;
|
|
74
|
+
}
|
|
75
|
+
return items.filter((item) => activeFilter(item, query));
|
|
76
|
+
}, [items, filteredItems, filter, searchValue]);
|
|
77
|
+
|
|
78
|
+
const groups = useMemo(() => {
|
|
79
|
+
if (!isGrouped) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
return groupByField(visibleItems);
|
|
83
|
+
}, [isGrouped, visibleItems]);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
isGrouped,
|
|
87
|
+
groups,
|
|
88
|
+
flatItems: isGrouped ? [] : visibleItems,
|
|
89
|
+
resolvedSearchValue: searchValue,
|
|
90
|
+
handleSearchValueChange,
|
|
91
|
+
};
|
|
92
|
+
};
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from '@jest/globals';
|
|
|
2
2
|
import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
|
|
3
3
|
import { render, screen } from '@testing-library/react-native';
|
|
4
4
|
|
|
5
|
+
import type { ComponentRef } from 'react';
|
|
5
6
|
import { createRef } from 'react';
|
|
6
7
|
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
7
8
|
import { PageIndicator } from './PageIndicator';
|
|
@@ -162,7 +163,7 @@ describe('PageIndicator Component', () => {
|
|
|
162
163
|
|
|
163
164
|
describe('Props', () => {
|
|
164
165
|
it('should accept ref prop', () => {
|
|
165
|
-
const ref = createRef<
|
|
166
|
+
const ref = createRef<ComponentRef<typeof PageIndicator>>();
|
|
166
167
|
renderWithProvider(
|
|
167
168
|
<PageIndicator
|
|
168
169
|
ref={ref}
|
|
@@ -110,22 +110,26 @@ const componentsMap = {
|
|
|
110
110
|
* // Tile variant
|
|
111
111
|
* <Skeleton component='tile' />
|
|
112
112
|
*/
|
|
113
|
-
const Skeleton = ({ lx, component, ...props }: SkeletonProps) => {
|
|
113
|
+
const Skeleton = ({ lx, component, style, ...props }: SkeletonProps) => {
|
|
114
114
|
/**
|
|
115
115
|
* Check if the component is a valid pre-built variant and return the corresponding component.
|
|
116
116
|
*/
|
|
117
117
|
if (component && componentsMap[component]) {
|
|
118
118
|
const Component = componentsMap[component];
|
|
119
119
|
return (
|
|
120
|
-
<Pulse animate>
|
|
121
|
-
<Component {...props}
|
|
120
|
+
<Pulse animate style={style} lx={lx}>
|
|
121
|
+
<Component {...props} />
|
|
122
122
|
</Pulse>
|
|
123
123
|
);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
return (
|
|
127
|
-
<Pulse animate>
|
|
128
|
-
<BaseSkeleton
|
|
127
|
+
<Pulse animate style={style} lx={lx}>
|
|
128
|
+
<BaseSkeleton
|
|
129
|
+
testID='skeleton'
|
|
130
|
+
lx={{ width: 'full', height: 'full' }}
|
|
131
|
+
{...props}
|
|
132
|
+
/>
|
|
129
133
|
</Pulse>
|
|
130
134
|
);
|
|
131
135
|
};
|
|
@@ -2,7 +2,7 @@ import { BlurView } from '@sbaiahmed1/react-native-blur';
|
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import { Children, isValidElement, useEffect, useMemo, useRef } from 'react';
|
|
4
4
|
import type { LayoutChangeEvent } from 'react-native';
|
|
5
|
-
import {
|
|
5
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
6
6
|
import Animated, {
|
|
7
7
|
cancelAnimation,
|
|
8
8
|
useAnimatedStyle,
|
|
@@ -15,6 +15,7 @@ import { useStyleSheet, useTheme } from '../../../styles';
|
|
|
15
15
|
import { useTimingConfig } from '../../Animations/useTimingConfig';
|
|
16
16
|
import { triggerHapticFeedback } from '../../Haptics';
|
|
17
17
|
import { Placeholder } from '../../Symbols';
|
|
18
|
+
import { RuntimeConstants } from '../../utils';
|
|
18
19
|
import { Box, Pressable } from '../Utility';
|
|
19
20
|
import { TabBarContextProvider, useTabBarContext } from './TabBarContext';
|
|
20
21
|
import type { TabBarItemProps, TabBarProps } from './types';
|
|
@@ -268,7 +269,7 @@ export function TabBar({
|
|
|
268
269
|
{...props}
|
|
269
270
|
>
|
|
270
271
|
{children}
|
|
271
|
-
{
|
|
272
|
+
{RuntimeConstants.isAndroid ? (
|
|
272
273
|
<View style={styles.fallbackBackground} />
|
|
273
274
|
) : (
|
|
274
275
|
<BlurView
|
package/src/lib/types/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { ElementType, ComponentPropsWithRef,
|
|
1
|
+
import type { ElementType, ComponentPropsWithRef, ComponentRef } from 'react';
|
|
2
2
|
import type { Pressable, View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
type ComponentPropsWithAsChild<T extends ElementType<any>> =
|
|
5
5
|
ComponentPropsWithRef<T> & { asChild?: boolean };
|
|
6
6
|
|
|
7
7
|
type SlottableViewProps = ComponentPropsWithAsChild<typeof View>;
|
|
8
|
-
type ViewRef =
|
|
9
|
-
type PressableRef =
|
|
8
|
+
type ViewRef = ComponentRef<typeof View>;
|
|
9
|
+
type PressableRef = ComponentRef<typeof Pressable>;
|
|
10
10
|
|
|
11
11
|
type SlottablePressableProps = ComponentPropsWithAsChild<typeof Pressable> & {
|
|
12
12
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getObjectPath } from '@ledgerhq/lumen-utils-shared';
|
|
2
|
-
import {
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
|
3
4
|
import { useTheme } from '../hooks/useTheme';
|
|
4
5
|
import type {
|
|
5
6
|
LumenStyleSheetTheme,
|
|
@@ -46,7 +47,7 @@ const resolveStyle = <T extends ViewStyle | TextStyle>(
|
|
|
46
47
|
*/
|
|
47
48
|
export const useResolveViewStyle = (
|
|
48
49
|
lx: LumenViewStyle,
|
|
49
|
-
bareStyle?: ViewStyle
|
|
50
|
+
bareStyle?: StyleProp<ViewStyle>,
|
|
50
51
|
): ViewStyle => {
|
|
51
52
|
const { theme } = useTheme();
|
|
52
53
|
const resolvedStyle = resolveStyle<ViewStyle>(theme, lx, VIEW_PROP_CONFIG);
|
|
@@ -58,7 +59,7 @@ export const useResolveViewStyle = (
|
|
|
58
59
|
*/
|
|
59
60
|
export const useResolveTextStyle = (
|
|
60
61
|
lx: LumenTextStyle,
|
|
61
|
-
bareStyle?: TextStyle
|
|
62
|
+
bareStyle?: StyleProp<TextStyle>,
|
|
62
63
|
): TextStyle => {
|
|
63
64
|
const { theme } = useTheme();
|
|
64
65
|
const resolvedStyle = resolveStyle<TextStyle>(theme, lx, TEXT_PROP_CONFIG);
|