react-native-molecules 0.5.0-beta.22 → 0.5.0-beta.23

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.
@@ -1,68 +1,34 @@
1
- import {
2
- type ComponentType,
3
- memo,
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 { memo, useCallback, useMemo } from 'react';
2
+ import { ScrollView, type StyleProp, type ViewStyle } from 'react-native';
11
3
 
12
4
  import { typedMemo } from '../../hocs';
13
5
  import { useActionState, useControlledValue, useLatest } from '../../hooks';
14
- import type { WithElements } from '../../types';
15
6
  import { resolveStateVariant } from '../../utils';
16
- import { Divider } from '../Divider';
17
- import { Icon } from '../Icon';
18
- import { IconButton } from '../IconButton';
19
7
  import { StateLayer } from '../StateLayer';
20
- import { Text } from '../Text';
21
- import { TextInput, type TextInputHandles, type TextInputProps } from '../TextInput';
22
- import { TouchableRipple, type TouchableRippleProps } from '../TouchableRipple';
8
+ import { TouchableRipple } from '../TouchableRipple';
23
9
  import { ListContextProvider, useListContextValue } from './context';
24
10
  import type {
25
11
  DefaultListItemT,
26
12
  ListContentProps,
27
13
  ListContextValue,
28
- ListGroupProps,
29
- ListItemOptionProps,
14
+ ListItemId,
15
+ ListItemProps,
30
16
  ListProps,
31
- ListSearchInputProps,
32
17
  } from './types';
33
- import { listItemStyles, listStyles } from './utils';
34
-
35
- const emptyArr: unknown[] = [];
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 = ({
18
+ import { listItemStyles } from './utils';
19
+
20
+ const _ListItemBase = ({
51
21
  ref,
52
- left,
53
- right,
54
22
  children,
55
23
  style: styleProp,
56
24
  disabled = false,
57
- divider = false,
58
- variant = 'default',
25
+ variant = 'menuItem',
59
26
  selected = false,
60
27
  onPress,
61
28
  hoverable: hoverableProp = false,
62
29
  hovered: hoveredProp = false,
63
- contentStyle: contentStyleProp,
64
30
  ...props
65
- }: InternalListItemProps) => {
31
+ }: ListItemProps) => {
66
32
  const {
67
33
  hovered: _hovered,
68
34
  focused,
@@ -77,63 +43,140 @@ const _InternalListItem = ({
77
43
  disabled,
78
44
  hovered: hoverable && hovered,
79
45
  focused,
80
- }) as any;
46
+ });
81
47
 
82
48
  listItemStyles.useVariants({
83
- state,
84
- variant: variant as any,
49
+ state: state as never,
50
+ variant: variant as never,
85
51
  });
86
52
 
87
- const {
88
- containerStyles,
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
- };
53
+ const containerStyles = useMemo(
54
+ () => [listItemStyles.root, styleProp],
102
55
  // eslint-disable-next-line react-hooks/exhaustive-deps
103
- }, [styleProp, state, variant]);
56
+ [styleProp, state, variant],
57
+ );
104
58
 
105
59
  return (
106
60
  <TouchableRipple
107
61
  {...props}
108
- style={containerStyles}
62
+ style={containerStyles as StyleProp<ViewStyle>}
109
63
  disabled={disabled}
110
64
  onPress={onPress}
111
65
  ref={actionsRef}>
112
66
  <>
113
- <View style={innerContainerStyle}>
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}
67
+ {children}
127
68
  <StateLayer style={listItemStyles.stateLayer} />
128
69
  </>
129
70
  </TouchableRipple>
130
71
  );
131
72
  };
132
73
 
133
- const ListItem = memo(_InternalListItem);
74
+ const ListItemBase = memo(_ListItemBase);
75
+
76
+ const _ListItemSelectable = <Option extends object = DefaultListItemT>({
77
+ value,
78
+ children,
79
+ onPress,
80
+ onBeforeToggle,
81
+ disabled: itemDisabledProp = false,
82
+ shouldToggleOnPress = true,
83
+ accessibilityRole,
84
+ accessibilityState,
85
+ variant,
86
+ ...rest
87
+ }: ListItemProps<Option> & { value: ListItemId }) => {
88
+ const {
89
+ onAdd,
90
+ onRemove,
91
+ disabled: listDisabled,
92
+ allowDeselect,
93
+ isSelectedId,
94
+ } = useListContextValue(state => ({
95
+ onAdd: state.onAdd as (item: Option) => void,
96
+ onRemove: state.onRemove as (item: Option) => void,
97
+ disabled: state.disabled,
98
+ allowDeselect: state.allowDeselect,
99
+ isSelectedId: state.isSelectedId,
100
+ }));
101
+
102
+ const option = useMemo(
103
+ () =>
104
+ ({
105
+ id: value,
106
+ ...(itemDisabledProp ? { selectable: false } : {}),
107
+ } as Option),
108
+ [itemDisabledProp, value],
109
+ );
110
+
111
+ const isSelected = isSelectedId(value);
112
+
113
+ const isSelectable = (option as Record<string, unknown>).selectable;
114
+ const isOptionDisabled = Boolean(listDisabled || itemDisabledProp || isSelectable === false);
115
+
116
+ const handlePress = useCallback(
117
+ (
118
+ event: NonNullable<ListItemProps<Option>['onPress']> extends (event: infer E) => void
119
+ ? E
120
+ : never,
121
+ ) => {
122
+ if (isOptionDisabled) return;
123
+ onPress?.(event);
124
+
125
+ if (!shouldToggleOnPress) return;
126
+ onBeforeToggle?.(event);
127
+
128
+ if (isSelected) {
129
+ if (allowDeselect) onRemove(option);
130
+ return;
131
+ }
132
+ onAdd(option);
133
+ },
134
+ [
135
+ allowDeselect,
136
+ isOptionDisabled,
137
+ isSelected,
138
+ onAdd,
139
+ onBeforeToggle,
140
+ onPress,
141
+ onRemove,
142
+ option,
143
+ shouldToggleOnPress,
144
+ ],
145
+ );
146
+
147
+ return (
148
+ <ListItemBase
149
+ {...(rest as ListItemProps)}
150
+ selected={isSelected}
151
+ disabled={isOptionDisabled}
152
+ onPress={handlePress}
153
+ variant={variant ?? 'menuItem'}
154
+ accessibilityRole={accessibilityRole}
155
+ accessibilityState={
156
+ accessibilityState ?? { selected: isSelected, disabled: isOptionDisabled }
157
+ }>
158
+ {children}
159
+ </ListItemBase>
160
+ );
161
+ };
162
+
163
+ const ListItemSelectable = typedMemo(_ListItemSelectable);
164
+
165
+ const _ListItem = <Option extends object = DefaultListItemT>(props: ListItemProps<Option>) => {
166
+ if (props.value !== undefined) {
167
+ return <ListItemSelectable {...(props as ListItemProps<Option> & { value: ListItemId })} />;
168
+ }
169
+ return <ListItemBase {...(props as ListItemProps)} />;
170
+ };
171
+
172
+ const ListItem = typedMemo(_ListItem);
173
+
174
+ type ControlledListValue = ListItemId | ListItemId[] | null;
175
+
176
+ const emptyArr: ListItemId[] = [];
134
177
 
135
178
  const ListProvider = typedMemo(
136
- <Option extends DefaultListItemT = DefaultListItemT>({
179
+ <Option extends object = DefaultListItemT>({
137
180
  children,
138
181
  value: valueProp,
139
182
  defaultValue,
@@ -141,81 +184,54 @@ const ListProvider = typedMemo(
141
184
  multiple = false,
142
185
  disabled = false,
143
186
  error = false,
144
- items = emptyArr as Option[],
145
- searchKey,
146
- onSearchChange,
147
- hideSelected: hideSelectedProp,
187
+ allowDeselect: allowDeselectProp,
148
188
  }: ListProps<Option>) => {
149
- type ControlledListValue = Option['id'] | Option['id'][] | null;
150
- const [value, onValueChange] = useControlledValue<Option['id'] | Option['id'][] | null>({
189
+ const [value, onValueChange] = useControlledValue<ControlledListValue>({
151
190
  value: valueProp,
152
- defaultValue: defaultValue ?? (multiple ? (emptyArr as Option['id'][]) : null),
191
+ defaultValue: defaultValue ?? (multiple ? (emptyArr as ListItemId[]) : null),
153
192
  onChange: onChange as
154
- | ((value: ControlledListValue, item: Option, event?: any) => void)
193
+ | ((value: ControlledListValue, item: Option, event?: unknown) => void)
155
194
  | undefined,
156
195
  });
157
196
  const valueRef = useLatest(value);
158
- const [searchQuery, setSearchQuery] = useState('');
159
197
 
160
- const handleSearchQueryChange = useCallback(
161
- (query: string) => {
162
- setSearchQuery(query);
163
- onSearchChange?.(query);
198
+ const allowDeselect = allowDeselectProp !== undefined ? allowDeselectProp : multiple;
199
+ const isSelectedId = useCallback(
200
+ (id: ListItemId) => {
201
+ if (multiple) {
202
+ const values = (value as ListItemId[] | null | undefined) ?? [];
203
+ return values.some(v => v === id);
204
+ }
205
+ return (value as ListItemId | null) === id;
164
206
  },
165
- [onSearchChange],
207
+ [multiple, value],
166
208
  );
167
209
 
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
210
  const onAdd = useCallback(
200
211
  (item: Option) => {
212
+ const id = (item as { id?: ListItemId }).id as ListItemId;
201
213
  if (multiple) {
202
- const currentValue = (valueRef.current as Option['id'][]) || [];
203
- if (!currentValue.find(v => v === item.id)) {
204
- onValueChange([...currentValue, item.id] as Option['id'][], item);
214
+ const currentValue = (valueRef.current as ListItemId[]) || [];
215
+ if (!currentValue.find(v => v === id)) {
216
+ onValueChange([...currentValue, id], item);
205
217
  }
206
218
  return;
207
219
  }
208
220
 
209
- onValueChange(item.id, item);
221
+ onValueChange(id, item);
210
222
  },
211
223
  [multiple, onValueChange, valueRef],
212
224
  );
213
225
 
214
226
  const onRemove = useCallback(
215
227
  (item: Option) => {
228
+ const id = (item as { id?: ListItemId }).id as ListItemId;
216
229
  if (multiple) {
217
- const currentValue = (valueRef.current as Option['id'][]) || [];
218
- onValueChange(currentValue.filter(v => v !== item.id) as Option['id'][], item);
230
+ const currentValue = (valueRef.current as ListItemId[]) || [];
231
+ onValueChange(
232
+ currentValue.filter(v => v !== id),
233
+ item,
234
+ );
219
235
  return;
220
236
  }
221
237
 
@@ -229,12 +245,10 @@ const ListProvider = typedMemo(
229
245
  multiple,
230
246
  onAdd: onAdd as (item: DefaultListItemT) => void,
231
247
  onRemove: onRemove as (item: DefaultListItemT) => void,
248
+ isSelectedId,
232
249
  disabled,
233
250
  error,
234
- items,
235
- searchQuery,
236
- setSearchQuery: handleSearchQueryChange,
237
- filteredItems,
251
+ allowDeselect,
238
252
  } as ListContextValue<DefaultListItemT>;
239
253
 
240
254
  return <ListContextProvider value={contextValue}>{children}</ListContextProvider>;
@@ -242,266 +256,20 @@ const ListProvider = typedMemo(
242
256
  );
243
257
 
244
258
  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
-
259
+ ({ ref, children, style, ...rest }: ListContentProps & { ref?: React.ForwardedRef<any> }) => {
339
260
  return (
340
- <Container style={style} {...rest} ref={ref} accessibilityRole="listbox">
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
- }>
261
+ <ScrollView style={style} {...rest} ref={ref}>
432
262
  {children}
433
- </ListItem>
263
+ </ScrollView>
434
264
  );
435
265
  },
436
266
  );
437
267
 
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
268
  const List = Object.assign(ListProvider, {
490
269
  Content: ListContent,
491
- Item: ListOption,
492
- SearchInput: ListSearchInput,
493
- Group: ListGroup,
494
- Row: ListItem,
270
+ Item: ListItem,
495
271
  });
496
272
 
497
273
  export default List;
498
274
 
499
- export {
500
- type InternalListItemProps,
501
- ListContent,
502
- ListGroup,
503
- ListItem,
504
- ListOption,
505
- ListProvider,
506
- ListSearchInput,
507
- };
275
+ export { ListContent, ListItem, ListProvider };
@@ -7,12 +7,10 @@ const listContextDefaultValue: ListContextValue<DefaultListItemT> = {
7
7
  multiple: false,
8
8
  onAdd: () => {},
9
9
  onRemove: () => {},
10
+ isSelectedId: () => false,
10
11
  disabled: false,
11
12
  error: false,
12
- items: [],
13
- searchQuery: '',
14
- setSearchQuery: () => {},
15
- filteredItems: [],
13
+ allowDeselect: false,
16
14
  };
17
15
 
18
16
  const {
@@ -1,6 +1,5 @@
1
1
  import { getRegisteredComponentWithFallback } from '../../core';
2
2
  import ListDefault from './List';
3
- export type { InternalListItemProps as ListItemProps } from './List';
4
3
 
5
4
  export const List = getRegisteredComponentWithFallback('List', ListDefault);
6
5