react-native-molecules 0.5.0-beta.22 → 0.5.0-beta.24
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/components/Accordion/Accordion.tsx +1 -1
- package/components/Accordion/AccordionItem.tsx +1 -1
- package/components/Button/Button.tsx +3 -1
- package/components/Checkbox/Checkbox.tsx +2 -1
- package/components/DateField/useDateFieldState.ts +2 -2
- package/components/DatePicker/DatePickerProvider.tsx +1 -1
- package/components/DatePicker/utils.ts +2 -0
- package/components/DatePickerInline/DatePickerInline.tsx +1 -1
- package/components/DatePickerInline/DatePickerInlineBase.tsx +1 -1
- package/components/DatePickerInline/Day.tsx +1 -1
- package/components/DatePickerInline/MonthPicker.tsx +24 -40
- package/components/DatePickerInline/Swiper.tsx +1 -1
- package/components/DatePickerInline/SwiperUtils.ts +1 -1
- package/components/DatePickerInline/YearPicker.tsx +44 -79
- package/components/DatePickerInline/dateUtils.tsx +1 -1
- package/components/DatePickerInline/store.tsx +2 -1
- package/components/Divider/index.tsx +2 -3
- package/components/ElementGroup/ElementGroup.tsx +1 -1
- package/components/FilePicker/FilePicker.tsx +1 -1
- package/components/Icon/iconFactory.tsx +2 -1
- package/components/IconButton/IconButton.tsx +39 -13
- package/components/IconButton/index.tsx +1 -0
- package/components/IconButton/types.ts +2 -0
- package/components/List/List.tsx +156 -387
- package/components/List/context.tsx +4 -5
- package/components/List/index.ts +0 -1
- package/components/List/types.ts +77 -109
- package/components/List/utils.ts +4 -37
- package/components/Menu/Menu.tsx +13 -30
- package/components/Menu/index.tsx +0 -2
- package/components/Popover/Popover.tsx +7 -10
- package/components/Popover/PopoverRoot.tsx +6 -20
- package/components/Popover/common.ts +4 -0
- package/components/Popover/index.ts +2 -8
- package/components/Popover/usePlatformMeasure.ts +4 -2
- package/components/Portal/Portal.tsx +1 -2
- package/components/RadioButton/RadioButtonGroup.tsx +1 -2
- package/components/Rating/Rating.tsx +1 -1
- package/components/Select/Select.tsx +304 -71
- package/components/Select/context.tsx +30 -3
- package/components/Select/index.ts +20 -2
- package/components/Select/types.ts +43 -25
- package/components/Select/utils.ts +18 -4
- package/components/Switch/Switch.ios.tsx +1 -1
- package/components/Switch/Switch.tsx +2 -1
- package/components/Tabs/Tabs.tsx +2 -2
- package/components/TextInput/TextInput.tsx +4 -3
- package/components/TimePicker/AnalogClock.tsx +1 -1
- package/components/TimePicker/TimeInputs.tsx +1 -1
- package/components/TimePicker/TimePicker.tsx +1 -1
- package/components/TimePicker/TimePickerModal.tsx +1 -1
- package/components/Tooltip/Tooltip.tsx +1 -1
- package/components/TouchableRipple/TouchableRipple.tsx +76 -152
- package/hocs/index.tsx +1 -1
- package/hocs/withKeyboardAccessibility.tsx +2 -3
- package/hooks/index.tsx +2 -6
- package/hooks/useContrastColor.ts +1 -2
- package/hooks/useFilePicker.tsx +1 -1
- package/hooks/useHandleNumberFormat.tsx +2 -2
- package/hooks/useMediaQuery.tsx +1 -2
- package/package.json +5 -28
- package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +1 -1
- package/shortcuts-manager/ShortcutsManager/utils.tsx +1 -1
- package/shortcuts-manager/useSetScopes/useSetScopes.tsx +1 -1
- package/shortcuts-manager/useShortcut/useShortcut.tsx +1 -1
- package/utils/extractTextStyles.ts +1 -2
- package/utils/formatNumberWithMask/formatNumberWithMask.ts +2 -1
- package/utils/index.ts +0 -3
- package/utils/normalizeToNumberString/normalizeToNumberString.ts +1 -1
- package/context-bridge/index.tsx +0 -87
- package/fast-context/index.tsx +0 -190
- package/hocs/typedMemo.tsx +0 -5
- package/hooks/useControlledValue.tsx +0 -84
- package/hooks/useLatest.tsx +0 -9
- package/hooks/useMergedRefs.ts +0 -14
- package/hooks/usePrevious.ts +0 -13
- package/hooks/useToggle.tsx +0 -24
- package/hooks/useWhatHasUpdated.tsx +0 -48
- package/utils/color.ts +0 -22
- package/utils/compare/index.ts +0 -54
- package/utils/lodash.ts +0 -121
- package/utils/repository.ts +0 -53
package/components/List/List.tsx
CHANGED
|
@@ -1,68 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type RefObject,
|
|
5
|
-
useCallback,
|
|
6
|
-
useMemo,
|
|
7
|
-
useRef,
|
|
8
|
-
useState,
|
|
9
|
-
} from 'react';
|
|
10
|
-
import { ScrollView, type StyleProp, View, type ViewStyle } from 'react-native';
|
|
1
|
+
import { useControlledValue, useLatest } from '@react-native-molecules/utils/hooks';
|
|
2
|
+
import { memo, useCallback, useMemo } from 'react';
|
|
3
|
+
import { ScrollView, type StyleProp, type ViewStyle } from 'react-native';
|
|
11
4
|
|
|
12
5
|
import { typedMemo } from '../../hocs';
|
|
13
|
-
import { useActionState
|
|
14
|
-
import type { WithElements } from '../../types';
|
|
6
|
+
import { useActionState } from '../../hooks';
|
|
15
7
|
import { resolveStateVariant } from '../../utils';
|
|
16
|
-
import { Divider } from '../Divider';
|
|
17
|
-
import { Icon } from '../Icon';
|
|
18
|
-
import { IconButton } from '../IconButton';
|
|
19
8
|
import { StateLayer } from '../StateLayer';
|
|
20
|
-
import {
|
|
21
|
-
import { TextInput, type TextInputHandles, type TextInputProps } from '../TextInput';
|
|
22
|
-
import { TouchableRipple, type TouchableRippleProps } from '../TouchableRipple';
|
|
9
|
+
import { TouchableRipple } from '../TouchableRipple';
|
|
23
10
|
import { ListContextProvider, useListContextValue } from './context';
|
|
24
11
|
import type {
|
|
25
12
|
DefaultListItemT,
|
|
26
13
|
ListContentProps,
|
|
27
14
|
ListContextValue,
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
ListItemId,
|
|
16
|
+
ListItemProps,
|
|
30
17
|
ListProps,
|
|
31
|
-
ListSearchInputProps,
|
|
32
18
|
} from './types';
|
|
33
|
-
import { listItemStyles
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
type InternalListItemProps = Omit<TouchableRippleProps, 'children'> &
|
|
38
|
-
WithElements<React.ReactNode | ((renderArgs: { hovered: boolean }) => React.ReactNode)> & {
|
|
39
|
-
ref?: RefObject<any>;
|
|
40
|
-
hovered?: boolean;
|
|
41
|
-
children: React.ReactNode;
|
|
42
|
-
style?: StyleProp<ViewStyle>;
|
|
43
|
-
divider?: boolean;
|
|
44
|
-
variant?: 'default' | 'menuItem';
|
|
45
|
-
selected?: boolean;
|
|
46
|
-
hoverable?: boolean;
|
|
47
|
-
contentStyle?: StyleProp<ViewStyle>;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const _InternalListItem = ({
|
|
19
|
+
import { listItemStyles } from './utils';
|
|
20
|
+
|
|
21
|
+
const _ListItemBase = ({
|
|
51
22
|
ref,
|
|
52
|
-
left,
|
|
53
|
-
right,
|
|
54
23
|
children,
|
|
55
24
|
style: styleProp,
|
|
56
25
|
disabled = false,
|
|
57
|
-
|
|
58
|
-
variant = 'default',
|
|
26
|
+
variant = 'menuItem',
|
|
59
27
|
selected = false,
|
|
60
28
|
onPress,
|
|
61
29
|
hoverable: hoverableProp = false,
|
|
62
30
|
hovered: hoveredProp = false,
|
|
63
|
-
contentStyle: contentStyleProp,
|
|
64
31
|
...props
|
|
65
|
-
}:
|
|
32
|
+
}: ListItemProps) => {
|
|
66
33
|
const {
|
|
67
34
|
hovered: _hovered,
|
|
68
35
|
focused,
|
|
@@ -77,63 +44,140 @@ const _InternalListItem = ({
|
|
|
77
44
|
disabled,
|
|
78
45
|
hovered: hoverable && hovered,
|
|
79
46
|
focused,
|
|
80
|
-
})
|
|
47
|
+
});
|
|
81
48
|
|
|
82
49
|
listItemStyles.useVariants({
|
|
83
|
-
state,
|
|
84
|
-
variant: variant as
|
|
50
|
+
state: state as never,
|
|
51
|
+
variant: variant as never,
|
|
85
52
|
});
|
|
86
53
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
innerContainerStyle,
|
|
90
|
-
contentStyle,
|
|
91
|
-
leftElementStyle,
|
|
92
|
-
rightElementStyle,
|
|
93
|
-
} = useMemo(() => {
|
|
94
|
-
const { innerContainer, content, leftElement, rightElement } = listItemStyles;
|
|
95
|
-
return {
|
|
96
|
-
containerStyles: [listItemStyles.root, styleProp],
|
|
97
|
-
innerContainerStyle: innerContainer,
|
|
98
|
-
contentStyle: content,
|
|
99
|
-
leftElementStyle: leftElement,
|
|
100
|
-
rightElementStyle: rightElement,
|
|
101
|
-
};
|
|
54
|
+
const containerStyles = useMemo(
|
|
55
|
+
() => [listItemStyles.root, styleProp],
|
|
102
56
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
103
|
-
|
|
57
|
+
[styleProp, state, variant],
|
|
58
|
+
);
|
|
104
59
|
|
|
105
60
|
return (
|
|
106
61
|
<TouchableRipple
|
|
107
62
|
{...props}
|
|
108
|
-
style={containerStyles}
|
|
63
|
+
style={containerStyles as StyleProp<ViewStyle>}
|
|
109
64
|
disabled={disabled}
|
|
110
65
|
onPress={onPress}
|
|
111
66
|
ref={actionsRef}>
|
|
112
67
|
<>
|
|
113
|
-
|
|
114
|
-
{left ? (
|
|
115
|
-
<View style={leftElementStyle}>
|
|
116
|
-
{typeof left === 'function' ? left({ hovered }) : left}
|
|
117
|
-
</View>
|
|
118
|
-
) : null}
|
|
119
|
-
<View style={[contentStyle, contentStyleProp]}>{children}</View>
|
|
120
|
-
{right ? (
|
|
121
|
-
<View style={rightElementStyle}>
|
|
122
|
-
{typeof right === 'function' ? right({ hovered }) : right}
|
|
123
|
-
</View>
|
|
124
|
-
) : null}
|
|
125
|
-
</View>
|
|
126
|
-
{divider ? <Divider /> : null}
|
|
68
|
+
{children}
|
|
127
69
|
<StateLayer style={listItemStyles.stateLayer} />
|
|
128
70
|
</>
|
|
129
71
|
</TouchableRipple>
|
|
130
72
|
);
|
|
131
73
|
};
|
|
132
74
|
|
|
133
|
-
const
|
|
75
|
+
const ListItemBase = memo(_ListItemBase);
|
|
76
|
+
|
|
77
|
+
const _ListItemSelectable = <Option extends object = DefaultListItemT>({
|
|
78
|
+
value,
|
|
79
|
+
children,
|
|
80
|
+
onPress,
|
|
81
|
+
onBeforeToggle,
|
|
82
|
+
disabled: itemDisabledProp = false,
|
|
83
|
+
shouldToggleOnPress = true,
|
|
84
|
+
accessibilityRole,
|
|
85
|
+
accessibilityState,
|
|
86
|
+
variant,
|
|
87
|
+
...rest
|
|
88
|
+
}: ListItemProps<Option> & { value: ListItemId }) => {
|
|
89
|
+
const {
|
|
90
|
+
onAdd,
|
|
91
|
+
onRemove,
|
|
92
|
+
disabled: listDisabled,
|
|
93
|
+
allowDeselect,
|
|
94
|
+
isSelectedId,
|
|
95
|
+
} = useListContextValue(state => ({
|
|
96
|
+
onAdd: state.onAdd as (item: Option) => void,
|
|
97
|
+
onRemove: state.onRemove as (item: Option) => void,
|
|
98
|
+
disabled: state.disabled,
|
|
99
|
+
allowDeselect: state.allowDeselect,
|
|
100
|
+
isSelectedId: state.isSelectedId,
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
const option = useMemo(
|
|
104
|
+
() =>
|
|
105
|
+
({
|
|
106
|
+
id: value,
|
|
107
|
+
...(itemDisabledProp ? { selectable: false } : {}),
|
|
108
|
+
} as Option),
|
|
109
|
+
[itemDisabledProp, value],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const isSelected = isSelectedId(value);
|
|
113
|
+
|
|
114
|
+
const isSelectable = (option as Record<string, unknown>).selectable;
|
|
115
|
+
const isOptionDisabled = Boolean(listDisabled || itemDisabledProp || isSelectable === false);
|
|
116
|
+
|
|
117
|
+
const handlePress = useCallback(
|
|
118
|
+
(
|
|
119
|
+
event: NonNullable<ListItemProps<Option>['onPress']> extends (event: infer E) => void
|
|
120
|
+
? E
|
|
121
|
+
: never,
|
|
122
|
+
) => {
|
|
123
|
+
if (isOptionDisabled) return;
|
|
124
|
+
onPress?.(event);
|
|
125
|
+
|
|
126
|
+
if (!shouldToggleOnPress) return;
|
|
127
|
+
onBeforeToggle?.(event);
|
|
128
|
+
|
|
129
|
+
if (isSelected) {
|
|
130
|
+
if (allowDeselect) onRemove(option);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
onAdd(option);
|
|
134
|
+
},
|
|
135
|
+
[
|
|
136
|
+
allowDeselect,
|
|
137
|
+
isOptionDisabled,
|
|
138
|
+
isSelected,
|
|
139
|
+
onAdd,
|
|
140
|
+
onBeforeToggle,
|
|
141
|
+
onPress,
|
|
142
|
+
onRemove,
|
|
143
|
+
option,
|
|
144
|
+
shouldToggleOnPress,
|
|
145
|
+
],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<ListItemBase
|
|
150
|
+
{...(rest as ListItemProps)}
|
|
151
|
+
selected={isSelected}
|
|
152
|
+
disabled={isOptionDisabled}
|
|
153
|
+
onPress={handlePress}
|
|
154
|
+
variant={variant ?? 'menuItem'}
|
|
155
|
+
accessibilityRole={accessibilityRole}
|
|
156
|
+
accessibilityState={
|
|
157
|
+
accessibilityState ?? { selected: isSelected, disabled: isOptionDisabled }
|
|
158
|
+
}>
|
|
159
|
+
{children}
|
|
160
|
+
</ListItemBase>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const ListItemSelectable = typedMemo(_ListItemSelectable);
|
|
165
|
+
|
|
166
|
+
const _ListItem = <Option extends object = DefaultListItemT>(props: ListItemProps<Option>) => {
|
|
167
|
+
if (props.value !== undefined) {
|
|
168
|
+
return <ListItemSelectable {...(props as ListItemProps<Option> & { value: ListItemId })} />;
|
|
169
|
+
}
|
|
170
|
+
return <ListItemBase {...(props as ListItemProps)} />;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const ListItem = typedMemo(_ListItem);
|
|
174
|
+
|
|
175
|
+
type ControlledListValue = ListItemId | ListItemId[] | null;
|
|
176
|
+
|
|
177
|
+
const emptyArr: ListItemId[] = [];
|
|
134
178
|
|
|
135
179
|
const ListProvider = typedMemo(
|
|
136
|
-
<Option extends
|
|
180
|
+
<Option extends object = DefaultListItemT>({
|
|
137
181
|
children,
|
|
138
182
|
value: valueProp,
|
|
139
183
|
defaultValue,
|
|
@@ -141,81 +185,54 @@ const ListProvider = typedMemo(
|
|
|
141
185
|
multiple = false,
|
|
142
186
|
disabled = false,
|
|
143
187
|
error = false,
|
|
144
|
-
|
|
145
|
-
searchKey,
|
|
146
|
-
onSearchChange,
|
|
147
|
-
hideSelected: hideSelectedProp,
|
|
188
|
+
allowDeselect: allowDeselectProp,
|
|
148
189
|
}: ListProps<Option>) => {
|
|
149
|
-
|
|
150
|
-
const [value, onValueChange] = useControlledValue<Option['id'] | Option['id'][] | null>({
|
|
190
|
+
const [value, onValueChange] = useControlledValue<ControlledListValue>({
|
|
151
191
|
value: valueProp,
|
|
152
|
-
defaultValue: defaultValue ?? (multiple ? (emptyArr as
|
|
192
|
+
defaultValue: defaultValue ?? (multiple ? (emptyArr as ListItemId[]) : null),
|
|
153
193
|
onChange: onChange as
|
|
154
|
-
| ((value: ControlledListValue, item: Option, event?:
|
|
194
|
+
| ((value: ControlledListValue, item: Option, event?: unknown) => void)
|
|
155
195
|
| undefined,
|
|
156
196
|
});
|
|
157
197
|
const valueRef = useLatest(value);
|
|
158
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
159
198
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
199
|
+
const allowDeselect = allowDeselectProp !== undefined ? allowDeselectProp : multiple;
|
|
200
|
+
const isSelectedId = useCallback(
|
|
201
|
+
(id: ListItemId) => {
|
|
202
|
+
if (multiple) {
|
|
203
|
+
const values = (value as ListItemId[] | null | undefined) ?? [];
|
|
204
|
+
return values.some(v => v === id);
|
|
205
|
+
}
|
|
206
|
+
return (value as ListItemId | null) === id;
|
|
164
207
|
},
|
|
165
|
-
[
|
|
208
|
+
[multiple, value],
|
|
166
209
|
);
|
|
167
210
|
|
|
168
|
-
const hideSelected = hideSelectedProp !== undefined ? hideSelectedProp : multiple;
|
|
169
|
-
|
|
170
|
-
const filteredItems = useMemo(() => {
|
|
171
|
-
let result = items;
|
|
172
|
-
|
|
173
|
-
if (hideSelected) {
|
|
174
|
-
result = result.filter(item => {
|
|
175
|
-
if (multiple) {
|
|
176
|
-
const values = (value as Option['id'][]) || [];
|
|
177
|
-
return !values.some(v => v === item.id);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const singleValue = value as Option['id'] | null;
|
|
181
|
-
return singleValue !== item.id;
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (searchQuery) {
|
|
186
|
-
const key = searchKey || 'label';
|
|
187
|
-
const lowerQuery = searchQuery.toLowerCase();
|
|
188
|
-
result = result.filter(item => {
|
|
189
|
-
const itemValue = item[key];
|
|
190
|
-
return String(itemValue || '')
|
|
191
|
-
.toLowerCase()
|
|
192
|
-
.includes(lowerQuery);
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return result;
|
|
197
|
-
}, [hideSelected, items, multiple, searchKey, searchQuery, value]);
|
|
198
|
-
|
|
199
211
|
const onAdd = useCallback(
|
|
200
212
|
(item: Option) => {
|
|
213
|
+
const id = (item as { id?: ListItemId }).id as ListItemId;
|
|
201
214
|
if (multiple) {
|
|
202
|
-
const currentValue = (valueRef.current as
|
|
203
|
-
if (!currentValue.find(v => v ===
|
|
204
|
-
onValueChange([...currentValue,
|
|
215
|
+
const currentValue = (valueRef.current as ListItemId[]) || [];
|
|
216
|
+
if (!currentValue.find(v => v === id)) {
|
|
217
|
+
onValueChange([...currentValue, id], item);
|
|
205
218
|
}
|
|
206
219
|
return;
|
|
207
220
|
}
|
|
208
221
|
|
|
209
|
-
onValueChange(
|
|
222
|
+
onValueChange(id, item);
|
|
210
223
|
},
|
|
211
224
|
[multiple, onValueChange, valueRef],
|
|
212
225
|
);
|
|
213
226
|
|
|
214
227
|
const onRemove = useCallback(
|
|
215
228
|
(item: Option) => {
|
|
229
|
+
const id = (item as { id?: ListItemId }).id as ListItemId;
|
|
216
230
|
if (multiple) {
|
|
217
|
-
const currentValue = (valueRef.current as
|
|
218
|
-
onValueChange(
|
|
231
|
+
const currentValue = (valueRef.current as ListItemId[]) || [];
|
|
232
|
+
onValueChange(
|
|
233
|
+
currentValue.filter(v => v !== id),
|
|
234
|
+
item,
|
|
235
|
+
);
|
|
219
236
|
return;
|
|
220
237
|
}
|
|
221
238
|
|
|
@@ -229,12 +246,10 @@ const ListProvider = typedMemo(
|
|
|
229
246
|
multiple,
|
|
230
247
|
onAdd: onAdd as (item: DefaultListItemT) => void,
|
|
231
248
|
onRemove: onRemove as (item: DefaultListItemT) => void,
|
|
249
|
+
isSelectedId,
|
|
232
250
|
disabled,
|
|
233
251
|
error,
|
|
234
|
-
|
|
235
|
-
searchQuery,
|
|
236
|
-
setSearchQuery: handleSearchQueryChange,
|
|
237
|
-
filteredItems,
|
|
252
|
+
allowDeselect,
|
|
238
253
|
} as ListContextValue<DefaultListItemT>;
|
|
239
254
|
|
|
240
255
|
return <ListContextProvider value={contextValue}>{children}</ListContextProvider>;
|
|
@@ -242,266 +257,20 @@ const ListProvider = typedMemo(
|
|
|
242
257
|
);
|
|
243
258
|
|
|
244
259
|
const ListContent = typedMemo(
|
|
245
|
-
<
|
|
246
|
-
Option extends DefaultListItemT = DefaultListItemT,
|
|
247
|
-
C extends ComponentType<any> = typeof ScrollView,
|
|
248
|
-
>({
|
|
249
|
-
ref,
|
|
250
|
-
children,
|
|
251
|
-
ContainerComponent,
|
|
252
|
-
style,
|
|
253
|
-
emptyState,
|
|
254
|
-
processProps,
|
|
255
|
-
...rest
|
|
256
|
-
}: ListContentProps<Option, C> & { ref?: React.ForwardedRef<any> }) => {
|
|
257
|
-
const { filteredItems, value, multiple, searchQuery, items } = useListContextValue(
|
|
258
|
-
state => ({
|
|
259
|
-
filteredItems: state.filteredItems,
|
|
260
|
-
value: state.value,
|
|
261
|
-
multiple: state.multiple,
|
|
262
|
-
searchQuery: state.searchQuery,
|
|
263
|
-
items: state.items,
|
|
264
|
-
}),
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
const selectedIds = useMemo(() => {
|
|
268
|
-
const map: Record<string, true> = {};
|
|
269
|
-
const values = multiple ? (value as any[]) ?? [] : value == null ? [] : [value];
|
|
270
|
-
for (const v of values) {
|
|
271
|
-
const id = v?.id ?? v;
|
|
272
|
-
if (id != null) map[id] = true;
|
|
273
|
-
}
|
|
274
|
-
return map;
|
|
275
|
-
}, [multiple, value]);
|
|
276
|
-
|
|
277
|
-
const isSelected = useCallback(
|
|
278
|
-
(item: DefaultListItemT) => selectedIds[item.id as any] === true,
|
|
279
|
-
[selectedIds],
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
const content = useMemo(() => {
|
|
283
|
-
if (children === undefined) {
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
return filteredItems.map(item =>
|
|
287
|
-
children(item as Option, isSelected(item as DefaultListItemT)),
|
|
288
|
-
);
|
|
289
|
-
}, [children, filteredItems, isSelected]);
|
|
290
|
-
|
|
291
|
-
const defaultEmptyState = useMemo(() => {
|
|
292
|
-
const hasSearchQuery = searchQuery && searchQuery.trim().length > 0;
|
|
293
|
-
const hasNoItems = items.length === 0;
|
|
294
|
-
|
|
295
|
-
if (hasNoItems) {
|
|
296
|
-
return (
|
|
297
|
-
<View style={listStyles.emptyState}>
|
|
298
|
-
<Text style={listStyles.emptyStateText}>No items available</Text>
|
|
299
|
-
</View>
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (hasSearchQuery) {
|
|
304
|
-
return (
|
|
305
|
-
<View style={listStyles.emptyState}>
|
|
306
|
-
<Text style={listStyles.emptyStateText}>No results found</Text>
|
|
307
|
-
</View>
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return (
|
|
312
|
-
<View style={listStyles.emptyState}>
|
|
313
|
-
<Text style={listStyles.emptyStateText}>No items</Text>
|
|
314
|
-
</View>
|
|
315
|
-
);
|
|
316
|
-
}, [items.length, searchQuery]);
|
|
317
|
-
|
|
318
|
-
const resolvedEmptyState = emptyState ?? defaultEmptyState;
|
|
319
|
-
const isEmpty = filteredItems.length === 0;
|
|
320
|
-
|
|
321
|
-
const Container = (ContainerComponent ?? ScrollView) as ComponentType<any>;
|
|
322
|
-
|
|
323
|
-
if (processProps) {
|
|
324
|
-
const baseProps = { style, ...rest, accessibilityRole: 'listbox' } as Record<
|
|
325
|
-
string,
|
|
326
|
-
any
|
|
327
|
-
>;
|
|
328
|
-
const processedProps = processProps({
|
|
329
|
-
props: baseProps as any,
|
|
330
|
-
items: filteredItems as Option[],
|
|
331
|
-
isEmpty,
|
|
332
|
-
emptyState: resolvedEmptyState,
|
|
333
|
-
isSelected,
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
return <Container {...(processedProps as any)} ref={ref} />;
|
|
337
|
-
}
|
|
338
|
-
|
|
260
|
+
({ ref, children, style, ...rest }: ListContentProps & { ref?: React.ForwardedRef<any> }) => {
|
|
339
261
|
return (
|
|
340
|
-
<
|
|
341
|
-
{isEmpty ? resolvedEmptyState : content}
|
|
342
|
-
</Container>
|
|
343
|
-
);
|
|
344
|
-
},
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
const ListGroup = memo(({ children, label, style, ...rest }: ListGroupProps) => {
|
|
348
|
-
return (
|
|
349
|
-
<View style={style} {...rest}>
|
|
350
|
-
{label ? <Text style={listStyles.groupLabel}>{label}</Text> : null}
|
|
351
|
-
{children}
|
|
352
|
-
</View>
|
|
353
|
-
);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
const ListOption = memo(
|
|
357
|
-
<Option extends DefaultListItemT = DefaultListItemT>({
|
|
358
|
-
value,
|
|
359
|
-
children,
|
|
360
|
-
onPress,
|
|
361
|
-
disabled: itemDisabledProp = false,
|
|
362
|
-
shouldToggleOnPress = true,
|
|
363
|
-
accessibilityRole,
|
|
364
|
-
accessibilityState,
|
|
365
|
-
...rest
|
|
366
|
-
}: ListItemOptionProps<Option>) => {
|
|
367
|
-
const {
|
|
368
|
-
multiple,
|
|
369
|
-
onAdd,
|
|
370
|
-
onRemove,
|
|
371
|
-
disabled: listDisabled,
|
|
372
|
-
items,
|
|
373
|
-
} = useListContextValue(state => ({
|
|
374
|
-
multiple: state.multiple,
|
|
375
|
-
onAdd: state.onAdd,
|
|
376
|
-
onRemove: state.onRemove,
|
|
377
|
-
disabled: state.disabled,
|
|
378
|
-
items: state.items,
|
|
379
|
-
}));
|
|
380
|
-
|
|
381
|
-
const option = useMemo(() => {
|
|
382
|
-
const foundItem = items.find(i => i.id === value);
|
|
383
|
-
if (foundItem) return foundItem as Option;
|
|
384
|
-
|
|
385
|
-
return {
|
|
386
|
-
id: value,
|
|
387
|
-
...(itemDisabledProp ? { selectable: false } : {}),
|
|
388
|
-
} as Option;
|
|
389
|
-
}, [itemDisabledProp, items, value]);
|
|
390
|
-
|
|
391
|
-
const isSelected = useListContextValue(state => {
|
|
392
|
-
if (multiple) {
|
|
393
|
-
const values = state.value as any[];
|
|
394
|
-
return values?.some(v => (v?.id ?? v) === option.id) || false;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const singleValue = state.value as any;
|
|
398
|
-
return (singleValue?.id ?? singleValue) === option.id || false;
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
const isOptionDisabled = Boolean(
|
|
402
|
-
listDisabled || itemDisabledProp || option.selectable === false,
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
const handlePress = useCallback(
|
|
406
|
-
(event: any) => {
|
|
407
|
-
if (isOptionDisabled) return;
|
|
408
|
-
onPress?.(option, event);
|
|
409
|
-
|
|
410
|
-
if (shouldToggleOnPress) {
|
|
411
|
-
if (isSelected) {
|
|
412
|
-
onRemove(option);
|
|
413
|
-
} else {
|
|
414
|
-
onAdd(option);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
},
|
|
418
|
-
[isOptionDisabled, isSelected, onAdd, onPress, onRemove, option, shouldToggleOnPress],
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
return (
|
|
422
|
-
<ListItem
|
|
423
|
-
{...rest}
|
|
424
|
-
selected={isSelected}
|
|
425
|
-
disabled={isOptionDisabled}
|
|
426
|
-
onPress={handlePress}
|
|
427
|
-
variant={rest.variant || 'menuItem'}
|
|
428
|
-
accessibilityRole={accessibilityRole}
|
|
429
|
-
accessibilityState={
|
|
430
|
-
accessibilityState ?? { selected: isSelected, disabled: isOptionDisabled }
|
|
431
|
-
}>
|
|
262
|
+
<ScrollView style={style} {...rest} ref={ref}>
|
|
432
263
|
{children}
|
|
433
|
-
</
|
|
264
|
+
</ScrollView>
|
|
434
265
|
);
|
|
435
266
|
},
|
|
436
267
|
);
|
|
437
268
|
|
|
438
|
-
const ListSearchInput = memo(({ children, ...textInputProps }: ListSearchInputProps) => {
|
|
439
|
-
const { searchQuery, setSearchQuery } = useListContextValue(state => ({
|
|
440
|
-
searchQuery: state.searchQuery,
|
|
441
|
-
setSearchQuery: state.setSearchQuery,
|
|
442
|
-
}));
|
|
443
|
-
|
|
444
|
-
const textInputRef = useRef<TextInputHandles>(null);
|
|
445
|
-
|
|
446
|
-
const handleChangeText = useCallback(
|
|
447
|
-
(text: string) => {
|
|
448
|
-
setSearchQuery(text);
|
|
449
|
-
},
|
|
450
|
-
[setSearchQuery],
|
|
451
|
-
);
|
|
452
|
-
|
|
453
|
-
const inputProps = {
|
|
454
|
-
...textInputProps,
|
|
455
|
-
value: searchQuery,
|
|
456
|
-
onChangeText: handleChangeText,
|
|
457
|
-
placeholder: textInputProps.placeholder || 'Search...',
|
|
458
|
-
inputStyle: listStyles.searchInputInput,
|
|
459
|
-
} as TextInputProps;
|
|
460
|
-
|
|
461
|
-
const onPressLeftIcon = useCallback(() => {
|
|
462
|
-
textInputRef.current?.focus();
|
|
463
|
-
}, []);
|
|
464
|
-
|
|
465
|
-
const onClearSearchQuery = useCallback(() => {
|
|
466
|
-
handleChangeText('');
|
|
467
|
-
}, [handleChangeText]);
|
|
468
|
-
|
|
469
|
-
return (
|
|
470
|
-
<TextInput
|
|
471
|
-
ref={textInputRef}
|
|
472
|
-
style={listStyles.searchInput}
|
|
473
|
-
size="sm"
|
|
474
|
-
variant="outlined"
|
|
475
|
-
{...inputProps}>
|
|
476
|
-
<TextInput.Left>
|
|
477
|
-
<Icon onPress={onPressLeftIcon} name="magnify" size={20} />
|
|
478
|
-
</TextInput.Left>
|
|
479
|
-
{searchQuery ? (
|
|
480
|
-
<TextInput.Right>
|
|
481
|
-
<IconButton name="close" size={20} onPress={onClearSearchQuery} />
|
|
482
|
-
</TextInput.Right>
|
|
483
|
-
) : null}
|
|
484
|
-
{children}
|
|
485
|
-
</TextInput>
|
|
486
|
-
);
|
|
487
|
-
});
|
|
488
|
-
|
|
489
269
|
const List = Object.assign(ListProvider, {
|
|
490
270
|
Content: ListContent,
|
|
491
|
-
Item:
|
|
492
|
-
SearchInput: ListSearchInput,
|
|
493
|
-
Group: ListGroup,
|
|
494
|
-
Row: ListItem,
|
|
271
|
+
Item: ListItem,
|
|
495
272
|
});
|
|
496
273
|
|
|
497
274
|
export default List;
|
|
498
275
|
|
|
499
|
-
export {
|
|
500
|
-
type InternalListItemProps,
|
|
501
|
-
ListContent,
|
|
502
|
-
ListGroup,
|
|
503
|
-
ListItem,
|
|
504
|
-
ListOption,
|
|
505
|
-
ListProvider,
|
|
506
|
-
ListSearchInput,
|
|
507
|
-
};
|
|
276
|
+
export { ListContent, ListItem, ListProvider };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createFastContext } from '
|
|
1
|
+
import { createFastContext } from '@react-native-molecules/utils/fast-context';
|
|
2
|
+
|
|
2
3
|
import { registerPortalContext } from '../Portal';
|
|
3
4
|
import type { DefaultListItemT, ListContextValue } from './types';
|
|
4
5
|
|
|
@@ -7,12 +8,10 @@ const listContextDefaultValue: ListContextValue<DefaultListItemT> = {
|
|
|
7
8
|
multiple: false,
|
|
8
9
|
onAdd: () => {},
|
|
9
10
|
onRemove: () => {},
|
|
11
|
+
isSelectedId: () => false,
|
|
10
12
|
disabled: false,
|
|
11
13
|
error: false,
|
|
12
|
-
|
|
13
|
-
searchQuery: '',
|
|
14
|
-
setSearchQuery: () => {},
|
|
15
|
-
filteredItems: [],
|
|
14
|
+
allowDeselect: false,
|
|
16
15
|
};
|
|
17
16
|
|
|
18
17
|
const {
|
package/components/List/index.ts
CHANGED