react-native-molecules 0.5.0-beta.21 → 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.
Files changed (59) hide show
  1. package/components/Button/Button.tsx +3 -1
  2. package/components/Card/Card.tsx +1 -1
  3. package/components/Checkbox/CheckboxBase.ios.tsx +1 -4
  4. package/components/Checkbox/CheckboxBase.tsx +2 -7
  5. package/components/DatePicker/DateCalendar.tsx +4 -4
  6. package/components/DatePicker/DatePickerModal.tsx +2 -1
  7. package/components/DatePicker/utils.ts +2 -0
  8. package/components/DatePickerInline/DatePickerDockedHeader.tsx +3 -3
  9. package/components/DatePickerInline/DatePickerInline.tsx +1 -1
  10. package/components/DatePickerInline/DatePickerInlineBase.tsx +2 -2
  11. package/components/DatePickerInline/DatePickerInlineHeader.tsx +43 -17
  12. package/components/DatePickerInline/HeaderItem.tsx +2 -2
  13. package/components/DatePickerInline/MonthPicker.tsx +58 -64
  14. package/components/DatePickerInline/Swiper.native.tsx +2 -2
  15. package/components/DatePickerInline/Swiper.tsx +3 -3
  16. package/components/DatePickerInline/YearPicker.tsx +108 -119
  17. package/components/DatePickerInline/{DatePickerContext.tsx → store.tsx} +7 -3
  18. package/components/DatePickerInline/types.ts +1 -1
  19. package/components/Divider/Divider.tsx +192 -0
  20. package/components/Divider/index.tsx +11 -0
  21. package/components/Drawer/DrawerItemGroup.tsx +3 -7
  22. package/components/IconButton/IconButton.tsx +2 -12
  23. package/components/List/List.tsx +275 -0
  24. package/components/List/context.tsx +26 -0
  25. package/components/List/index.ts +8 -0
  26. package/components/List/types.ts +117 -0
  27. package/components/List/utils.ts +79 -0
  28. package/components/Menu/Menu.tsx +146 -19
  29. package/components/Menu/index.tsx +9 -7
  30. package/components/Menu/utils.ts +21 -70
  31. package/components/Popover/Popover.tsx +7 -10
  32. package/components/Popover/PopoverRoot.tsx +6 -20
  33. package/components/Popover/common.ts +4 -0
  34. package/components/Popover/index.ts +2 -8
  35. package/components/Popover/usePlatformMeasure.ts +4 -2
  36. package/components/RadioButton/RadioButtonAndroid.tsx +38 -54
  37. package/components/RadioButton/RadioButtonIOS.tsx +2 -16
  38. package/components/Select/Select.tsx +307 -501
  39. package/components/Select/context.tsx +39 -32
  40. package/components/Select/types.ts +63 -56
  41. package/components/Select/utils.ts +19 -44
  42. package/components/Text/textFactory.tsx +17 -5
  43. package/components/TimePicker/TimeInput.tsx +2 -7
  44. package/components/TimePicker/utils.ts +0 -4
  45. package/components/TouchableRipple/TouchableRipple.native.tsx +36 -5
  46. package/components/TouchableRipple/TouchableRipple.tsx +121 -163
  47. package/components/TouchableRipple/rippleFromForegroundColor.ts +21 -0
  48. package/package.json +6 -3
  49. package/components/HorizontalDivider/HorizontalDivider.tsx +0 -103
  50. package/components/HorizontalDivider/index.tsx +0 -9
  51. package/components/ListItem/ListItem.tsx +0 -138
  52. package/components/ListItem/ListItemDescription.tsx +0 -25
  53. package/components/ListItem/ListItemTitle.tsx +0 -25
  54. package/components/ListItem/index.tsx +0 -14
  55. package/components/ListItem/utils.ts +0 -115
  56. package/components/Menu/MenuDivider.tsx +0 -13
  57. package/components/Menu/MenuItem.tsx +0 -128
  58. package/components/VerticalDivider/VerticalDivider.tsx +0 -100
  59. package/components/VerticalDivider/index.tsx +0 -9
@@ -0,0 +1,275 @@
1
+ import { memo, useCallback, useMemo } from 'react';
2
+ import { ScrollView, type StyleProp, type ViewStyle } from 'react-native';
3
+
4
+ import { typedMemo } from '../../hocs';
5
+ import { useActionState, useControlledValue, useLatest } from '../../hooks';
6
+ import { resolveStateVariant } from '../../utils';
7
+ import { StateLayer } from '../StateLayer';
8
+ import { TouchableRipple } from '../TouchableRipple';
9
+ import { ListContextProvider, useListContextValue } from './context';
10
+ import type {
11
+ DefaultListItemT,
12
+ ListContentProps,
13
+ ListContextValue,
14
+ ListItemId,
15
+ ListItemProps,
16
+ ListProps,
17
+ } from './types';
18
+ import { listItemStyles } from './utils';
19
+
20
+ const _ListItemBase = ({
21
+ ref,
22
+ children,
23
+ style: styleProp,
24
+ disabled = false,
25
+ variant = 'menuItem',
26
+ selected = false,
27
+ onPress,
28
+ hoverable: hoverableProp = false,
29
+ hovered: hoveredProp = false,
30
+ ...props
31
+ }: ListItemProps) => {
32
+ const {
33
+ hovered: _hovered,
34
+ focused,
35
+ actionsRef,
36
+ } = useActionState({ ref, actionsToListen: ['hover', 'focus'] });
37
+ const hoverable = hoverableProp || !!onPress;
38
+ const hovered = hoveredProp || _hovered;
39
+
40
+ const state = resolveStateVariant({
41
+ selectedAndFocused: selected && focused,
42
+ selected,
43
+ disabled,
44
+ hovered: hoverable && hovered,
45
+ focused,
46
+ });
47
+
48
+ listItemStyles.useVariants({
49
+ state: state as never,
50
+ variant: variant as never,
51
+ });
52
+
53
+ const containerStyles = useMemo(
54
+ () => [listItemStyles.root, styleProp],
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ [styleProp, state, variant],
57
+ );
58
+
59
+ return (
60
+ <TouchableRipple
61
+ {...props}
62
+ style={containerStyles as StyleProp<ViewStyle>}
63
+ disabled={disabled}
64
+ onPress={onPress}
65
+ ref={actionsRef}>
66
+ <>
67
+ {children}
68
+ <StateLayer style={listItemStyles.stateLayer} />
69
+ </>
70
+ </TouchableRipple>
71
+ );
72
+ };
73
+
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[] = [];
177
+
178
+ const ListProvider = typedMemo(
179
+ <Option extends object = DefaultListItemT>({
180
+ children,
181
+ value: valueProp,
182
+ defaultValue,
183
+ onChange,
184
+ multiple = false,
185
+ disabled = false,
186
+ error = false,
187
+ allowDeselect: allowDeselectProp,
188
+ }: ListProps<Option>) => {
189
+ const [value, onValueChange] = useControlledValue<ControlledListValue>({
190
+ value: valueProp,
191
+ defaultValue: defaultValue ?? (multiple ? (emptyArr as ListItemId[]) : null),
192
+ onChange: onChange as
193
+ | ((value: ControlledListValue, item: Option, event?: unknown) => void)
194
+ | undefined,
195
+ });
196
+ const valueRef = useLatest(value);
197
+
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;
206
+ },
207
+ [multiple, value],
208
+ );
209
+
210
+ const onAdd = useCallback(
211
+ (item: Option) => {
212
+ const id = (item as { id?: ListItemId }).id as ListItemId;
213
+ if (multiple) {
214
+ const currentValue = (valueRef.current as ListItemId[]) || [];
215
+ if (!currentValue.find(v => v === id)) {
216
+ onValueChange([...currentValue, id], item);
217
+ }
218
+ return;
219
+ }
220
+
221
+ onValueChange(id, item);
222
+ },
223
+ [multiple, onValueChange, valueRef],
224
+ );
225
+
226
+ const onRemove = useCallback(
227
+ (item: Option) => {
228
+ const id = (item as { id?: ListItemId }).id as ListItemId;
229
+ if (multiple) {
230
+ const currentValue = (valueRef.current as ListItemId[]) || [];
231
+ onValueChange(
232
+ currentValue.filter(v => v !== id),
233
+ item,
234
+ );
235
+ return;
236
+ }
237
+
238
+ onValueChange(null, item);
239
+ },
240
+ [multiple, onValueChange, valueRef],
241
+ );
242
+
243
+ const contextValue = {
244
+ value,
245
+ multiple,
246
+ onAdd: onAdd as (item: DefaultListItemT) => void,
247
+ onRemove: onRemove as (item: DefaultListItemT) => void,
248
+ isSelectedId,
249
+ disabled,
250
+ error,
251
+ allowDeselect,
252
+ } as ListContextValue<DefaultListItemT>;
253
+
254
+ return <ListContextProvider value={contextValue}>{children}</ListContextProvider>;
255
+ },
256
+ );
257
+
258
+ const ListContent = typedMemo(
259
+ ({ ref, children, style, ...rest }: ListContentProps & { ref?: React.ForwardedRef<any> }) => {
260
+ return (
261
+ <ScrollView style={style} {...rest} ref={ref}>
262
+ {children}
263
+ </ScrollView>
264
+ );
265
+ },
266
+ );
267
+
268
+ const List = Object.assign(ListProvider, {
269
+ Content: ListContent,
270
+ Item: ListItem,
271
+ });
272
+
273
+ export default List;
274
+
275
+ export { ListContent, ListItem, ListProvider };
@@ -0,0 +1,26 @@
1
+ import { createFastContext } from '../../fast-context';
2
+ import { registerPortalContext } from '../Portal';
3
+ import type { DefaultListItemT, ListContextValue } from './types';
4
+
5
+ const listContextDefaultValue: ListContextValue<DefaultListItemT> = {
6
+ value: null,
7
+ multiple: false,
8
+ onAdd: () => {},
9
+ onRemove: () => {},
10
+ isSelectedId: () => false,
11
+ disabled: false,
12
+ error: false,
13
+ allowDeselect: false,
14
+ };
15
+
16
+ const {
17
+ useStoreRef: useListStoreRef,
18
+ Provider: ListContextProvider,
19
+ useContext: useListContext,
20
+ useContextValue: useListContextValue,
21
+ Context: ListContext,
22
+ } = createFastContext<ListContextValue<DefaultListItemT>>(listContextDefaultValue, true);
23
+
24
+ export { ListContext, ListContextProvider, useListContext, useListContextValue, useListStoreRef };
25
+
26
+ registerPortalContext([ListContext]);
@@ -0,0 +1,8 @@
1
+ import { getRegisteredComponentWithFallback } from '../../core';
2
+ import ListDefault from './List';
3
+
4
+ export const List = getRegisteredComponentWithFallback('List', ListDefault);
5
+
6
+ export * from './context';
7
+ export type * from './types';
8
+ export * from './utils';
@@ -0,0 +1,117 @@
1
+ import type { ReactNode, RefObject } from 'react';
2
+ import {
3
+ type AccessibilityRole,
4
+ type GestureResponderEvent,
5
+ type ScrollViewProps,
6
+ type StyleProp,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+
10
+ import type { TouchableRippleProps } from '../TouchableRipple';
11
+
12
+ export type ListItemId = string | number;
13
+
14
+ export type DefaultListItemT = {
15
+ id?: ListItemId;
16
+ label?: string;
17
+ selectable?: boolean;
18
+ [key: string]: unknown;
19
+ };
20
+
21
+ export type ListValue<Multiple extends boolean = false> = Multiple extends true
22
+ ? ListItemId[]
23
+ : ListItemId | null;
24
+
25
+ export type ListEmptyStateRender = (ctx: {
26
+ /** True when `items` (the raw input) has at least one entry. */
27
+ hasItems: boolean;
28
+ }) => ReactNode;
29
+
30
+ export type ListContextValue<Option extends object = DefaultListItemT> = {
31
+ value: ListItemId | ListItemId[] | null;
32
+ multiple: boolean;
33
+ onAdd: (item: Option) => void;
34
+ onRemove: (item: Option) => void;
35
+ isSelectedId: (id: ListItemId) => boolean;
36
+ disabled?: boolean;
37
+ error: boolean;
38
+ allowDeselect: boolean;
39
+ };
40
+
41
+ type ListPropsBase = {
42
+ children: ReactNode;
43
+ disabled?: boolean;
44
+ error?: boolean;
45
+ /**
46
+ * Whether re-clicking the currently-selected row should remove it. Defaults
47
+ * to `true` for multiple, `false` for single (re-clicking the picked row
48
+ * in a "pick one and close" flow shouldn't clear the value).
49
+ */
50
+ allowDeselect?: boolean;
51
+ };
52
+
53
+ type SingleListProps<Option extends object = DefaultListItemT> = {
54
+ multiple?: false | undefined;
55
+ value?: ListValue<false>;
56
+ defaultValue?: ListValue<false>;
57
+ onChange?: (value: ListValue<false>, item: Option, event?: GestureResponderEvent) => void;
58
+ };
59
+
60
+ type MultipleListProps<Option extends object = DefaultListItemT> = {
61
+ multiple: true;
62
+ value?: ListValue<true>;
63
+ defaultValue?: ListValue<true>;
64
+ onChange?: (value: ListValue<true>, item: Option, event?: GestureResponderEvent) => void;
65
+ };
66
+
67
+ export type ListProps<Option extends object = DefaultListItemT> = ListPropsBase &
68
+ (SingleListProps<Option> | MultipleListProps<Option>);
69
+
70
+ export type ListContentProps = Omit<ScrollViewProps, 'children'> & {
71
+ children?: ReactNode;
72
+ };
73
+
74
+ /**
75
+ * Props for `<List.Item>`. When `value` is provided, the item participates in the
76
+ * surrounding `<List>` context — it derives its `selected` state from the context's
77
+ * value and toggles selection on press (unless `shouldToggleOnPress` is false).
78
+ *
79
+ * Without `value`, the item is a plain styled row (use it for menu-style entries
80
+ * that don't represent a selectable option).
81
+ *
82
+ * Note: when `value` is set, both `onPress` and the selection toggle fire on press,
83
+ * in that order. For most cases that's fine — pass `onPress` for side effects
84
+ * (e.g. closing a menu) and let the toggle drive `onChange`. Pass
85
+ * `onBeforeToggle` for side effects that should only run when the built-in
86
+ * toggle will happen. Set `shouldToggleOnPress={false}` to suppress the toggle entirely.
87
+ *
88
+ * Deselection: by default, single-select rows do **not** deselect on re-click
89
+ * (use `<List allowDeselect>` to opt in or out at the list level).
90
+ */
91
+ export type ListItemProps<Option extends object = DefaultListItemT> = Omit<
92
+ TouchableRippleProps,
93
+ 'children' | 'onPress'
94
+ > & {
95
+ ref?: RefObject<unknown>;
96
+ children?: ReactNode;
97
+ value?: ListItemId;
98
+ style?: StyleProp<ViewStyle>;
99
+ variant?: 'default' | 'menuItem';
100
+ selected?: boolean;
101
+ disabled?: boolean;
102
+ hovered?: boolean;
103
+ hoverable?: boolean;
104
+ shouldToggleOnPress?: boolean;
105
+ /** Runs after `onPress`, before the built-in selection toggle. */
106
+ onBeforeToggle?: (event: GestureResponderEvent) => void;
107
+ onPress?: (event: GestureResponderEvent) => void;
108
+ accessibilityRole?: AccessibilityRole;
109
+ accessibilityState?: Record<string, unknown>;
110
+ /** Reserved for generic item shape; not consumed directly. */
111
+ __optionType?: Option;
112
+ };
113
+
114
+ export type ListItemElementProps = {
115
+ children?: ReactNode;
116
+ style?: StyleProp<ViewStyle>;
117
+ };
@@ -0,0 +1,79 @@
1
+ import { StyleSheet } from 'react-native-unistyles';
2
+
3
+ import { getRegisteredComponentStylesWithFallback } from '../../core';
4
+
5
+ const defaultStyles = StyleSheet.create(theme => ({
6
+ emptyState: {
7
+ paddingHorizontal: theme.spacings['4'],
8
+ paddingVertical: theme.spacings['6'],
9
+ alignItems: 'center',
10
+ justifyContent: 'center',
11
+ },
12
+ emptyStateText: {
13
+ color: theme.colors.onSurfaceVariant,
14
+ fontSize: 14,
15
+ },
16
+ }));
17
+
18
+ export const listStyles = getRegisteredComponentStylesWithFallback('List', defaultStyles);
19
+
20
+ const listItemStylesDefault = StyleSheet.create(theme => ({
21
+ root: {
22
+ backgroundColor: theme.colors.surface,
23
+ flexDirection: 'row',
24
+ alignItems: 'center',
25
+ gap: theme.spacings['4'],
26
+
27
+ _web: {
28
+ outlineStyle: 'none',
29
+ },
30
+
31
+ variants: {
32
+ state: {
33
+ disabled: {
34
+ opacity: 0.38,
35
+ },
36
+ hovered: {},
37
+ focused: {},
38
+
39
+ selected: {
40
+ backgroundColor: theme.colors.surfaceVariant,
41
+ },
42
+ selectedAndFocused: {
43
+ backgroundColor: theme.colors.surfaceVariant,
44
+ },
45
+ },
46
+ variant: {
47
+ default: {
48
+ paddingLeft: theme.spacings['4'],
49
+ paddingRight: theme.spacings['6'],
50
+ minHeight: 56,
51
+ },
52
+ menuItem: {
53
+ paddingHorizontal: theme.spacings['3'],
54
+ minHeight: 40,
55
+ },
56
+ },
57
+ },
58
+ },
59
+ stateLayer: {
60
+ variants: {
61
+ state: {
62
+ hovered: {
63
+ backgroundColor: theme.colors.stateLayer.hover.onSurface,
64
+ },
65
+ focused: {
66
+ backgroundColor: theme.colors.stateLayer.hover.onSurface,
67
+ },
68
+ selectedAndFocused: {
69
+ backgroundColor: theme.colors.stateLayer.focussed.onSurface,
70
+ },
71
+ },
72
+ },
73
+ },
74
+ }));
75
+
76
+ export const listItemStyles = getRegisteredComponentStylesWithFallback(
77
+ 'List_Item',
78
+ listItemStylesDefault,
79
+ );
@@ -1,34 +1,81 @@
1
- import { createContext, memo, type ReactElement, useMemo } from 'react';
2
- import type { ViewStyle } from 'react-native';
1
+ import {
2
+ Children,
3
+ cloneElement,
4
+ memo,
5
+ type ReactElement,
6
+ type ReactNode,
7
+ useCallback,
8
+ useContext,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from 'react';
13
+ import type { GestureResponderEvent, ViewStyle } from 'react-native';
3
14
 
15
+ import {
16
+ type DefaultListItemT,
17
+ List,
18
+ type ListItemProps,
19
+ type ListProps,
20
+ type ListValue,
21
+ } from '../List';
4
22
  import { Popover, type PopoverProps } from '../Popover';
5
- import { menuStyles } from './utils';
23
+ import { MenuContext, MenuRootContext, menuStyles } from './utils';
6
24
 
7
- export type Props = Omit<PopoverProps, 'setIsOpen' | 'onClose' | 'children'> & {
25
+ type MenuBaseProps = Omit<
26
+ PopoverProps,
27
+ 'setIsOpen' | 'onClose' | 'isOpen' | 'triggerRef' | 'children'
28
+ > & {
8
29
  style?: ViewStyle;
9
30
  closeOnSelect?: boolean;
10
- onClose: () => void;
11
31
  children: ReactElement | ReactElement[];
12
- backdropStyles?: ViewStyle;
32
+ disabled?: boolean;
33
+ allowDeselect?: boolean;
13
34
  };
14
35
 
15
- const emptyObj = {} as ViewStyle;
36
+ type SingleMenuProps = {
37
+ multiple?: false | undefined;
38
+ value?: ListValue<false>;
39
+ defaultValue?: ListValue<false>;
40
+ onChange?: (
41
+ value: ListValue<false>,
42
+ item: DefaultListItemT,
43
+ event?: GestureResponderEvent,
44
+ ) => void;
45
+ };
46
+
47
+ type MultipleMenuProps = {
48
+ multiple: true;
49
+ value?: ListValue<true>;
50
+ defaultValue?: ListValue<true>;
51
+ onChange?: (
52
+ value: ListValue<true>,
53
+ item: DefaultListItemT,
54
+ event?: GestureResponderEvent,
55
+ ) => void;
56
+ };
57
+
58
+ export type Props = MenuBaseProps & (SingleMenuProps | MultipleMenuProps);
16
59
 
17
60
  const Menu = ({
18
- isOpen,
19
- onClose,
20
61
  children,
21
62
  style: styleProp,
22
- backdropStyles = emptyObj,
23
63
  closeOnSelect = true,
64
+ value,
65
+ defaultValue,
66
+ onChange,
67
+ multiple,
68
+ disabled,
69
+ allowDeselect,
24
70
  ...rest
25
71
  }: Props) => {
26
- const { backdropStyle, style } = useMemo(() => {
72
+ const { isOpen, onClose, triggerRef } = useContext(MenuRootContext);
73
+
74
+ const { style } = useMemo(() => {
27
75
  return {
28
- backdropStyle: [menuStyles.backdrop, backdropStyles] as unknown as ViewStyle,
29
76
  style: [menuStyles.root, styleProp] as unknown as ViewStyle,
30
77
  };
31
- }, [backdropStyles, styleProp]);
78
+ }, [styleProp]);
32
79
 
33
80
  const contextValue = useMemo(
34
81
  () => ({
@@ -38,17 +85,97 @@ const Menu = ({
38
85
  [closeOnSelect, onClose],
39
86
  );
40
87
 
88
+ const listProps = {
89
+ multiple,
90
+ value,
91
+ defaultValue,
92
+ onChange,
93
+ disabled,
94
+ allowDeselect,
95
+ } as ListProps<DefaultListItemT>;
96
+
41
97
  return (
42
- <Popover isOpen={isOpen} onClose={onClose} style={style} {...rest}>
43
- <Popover.Overlay style={backdropStyle} />
44
- <MenuContext.Provider value={contextValue}>{children}</MenuContext.Provider>
98
+ <Popover isOpen={isOpen} onClose={onClose} style={style} triggerRef={triggerRef} {...rest}>
99
+ <List {...listProps}>
100
+ <MenuContext.Provider value={contextValue}>{children}</MenuContext.Provider>
101
+ </List>
45
102
  </Popover>
46
103
  );
47
104
  };
48
105
 
49
- export const MenuContext = createContext({
50
- closeOnSelect: true,
51
- onClose: () => {},
106
+ export type MenuRootProps = {
107
+ children: ReactNode;
108
+ };
109
+
110
+ export const MenuRoot = memo(({ children }: MenuRootProps) => {
111
+ const [isOpen, setIsOpen] = useState(false);
112
+ const triggerRef = useRef(null);
113
+
114
+ const onOpen = useCallback(() => setIsOpen(true), []);
115
+ const onClose = useCallback(() => setIsOpen(false), []);
116
+
117
+ const contextValue = useMemo(
118
+ () => ({ isOpen, onOpen, onClose, triggerRef }),
119
+ [isOpen, onOpen, onClose],
120
+ );
121
+
122
+ return <MenuRootContext.Provider value={contextValue}>{children}</MenuRootContext.Provider>;
52
123
  });
53
124
 
125
+ MenuRoot.displayName = 'Menu_Root';
126
+
127
+ export type MenuTriggerProps = {
128
+ children: ReactElement;
129
+ };
130
+
131
+ export const MenuTrigger = memo(({ children }: MenuTriggerProps) => {
132
+ const { onOpen, triggerRef } = useContext(MenuRootContext);
133
+
134
+ const onPress = useCallback(
135
+ (e: unknown) => {
136
+ // @ts-ignore
137
+ children?.props?.onPress?.(e);
138
+ onOpen();
139
+ },
140
+ [children?.props, onOpen],
141
+ );
142
+
143
+ return useMemo(
144
+ () =>
145
+ cloneElement(Children.only(children), {
146
+ // @ts-ignore
147
+ ref: triggerRef,
148
+ onPress,
149
+ }),
150
+ [children, triggerRef, onPress],
151
+ );
152
+ });
153
+
154
+ MenuTrigger.displayName = 'Menu_Trigger';
155
+
156
+ export type MenuItemProps = Omit<ListItemProps, 'children' | 'onPress'> & {
157
+ children: ReactNode;
158
+ onPress?: (event: GestureResponderEvent) => void;
159
+ };
160
+
161
+ export const MenuItem = memo(({ onPress, children, ...rest }: MenuItemProps) => {
162
+ const { closeOnSelect, onClose } = useContext(MenuContext);
163
+
164
+ const handlePress = useCallback(
165
+ (event: GestureResponderEvent) => {
166
+ onPress?.(event);
167
+ if (closeOnSelect) onClose();
168
+ },
169
+ [closeOnSelect, onClose, onPress],
170
+ );
171
+
172
+ return (
173
+ <List.Item variant="menuItem" {...rest} onPress={handlePress}>
174
+ {children}
175
+ </List.Item>
176
+ );
177
+ });
178
+
179
+ MenuItem.displayName = 'Menu_Item';
180
+
54
181
  export default memo(Menu);