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.
- package/components/Button/Button.tsx +3 -1
- package/components/Card/Card.tsx +1 -1
- package/components/Checkbox/CheckboxBase.ios.tsx +1 -4
- package/components/Checkbox/CheckboxBase.tsx +2 -7
- package/components/DatePicker/DateCalendar.tsx +4 -4
- package/components/DatePicker/DatePickerModal.tsx +2 -1
- package/components/DatePicker/utils.ts +2 -0
- package/components/DatePickerInline/DatePickerDockedHeader.tsx +3 -3
- package/components/DatePickerInline/DatePickerInline.tsx +1 -1
- package/components/DatePickerInline/DatePickerInlineBase.tsx +2 -2
- package/components/DatePickerInline/DatePickerInlineHeader.tsx +43 -17
- package/components/DatePickerInline/HeaderItem.tsx +2 -2
- package/components/DatePickerInline/MonthPicker.tsx +58 -64
- package/components/DatePickerInline/Swiper.native.tsx +2 -2
- package/components/DatePickerInline/Swiper.tsx +3 -3
- package/components/DatePickerInline/YearPicker.tsx +108 -119
- package/components/DatePickerInline/{DatePickerContext.tsx → store.tsx} +7 -3
- package/components/DatePickerInline/types.ts +1 -1
- package/components/Divider/Divider.tsx +192 -0
- package/components/Divider/index.tsx +11 -0
- package/components/Drawer/DrawerItemGroup.tsx +3 -7
- package/components/IconButton/IconButton.tsx +2 -12
- package/components/List/List.tsx +275 -0
- package/components/List/context.tsx +26 -0
- package/components/List/index.ts +8 -0
- package/components/List/types.ts +117 -0
- package/components/List/utils.ts +79 -0
- package/components/Menu/Menu.tsx +146 -19
- package/components/Menu/index.tsx +9 -7
- package/components/Menu/utils.ts +21 -70
- 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/RadioButton/RadioButtonAndroid.tsx +38 -54
- package/components/RadioButton/RadioButtonIOS.tsx +2 -16
- package/components/Select/Select.tsx +307 -501
- package/components/Select/context.tsx +39 -32
- package/components/Select/types.ts +63 -56
- package/components/Select/utils.ts +19 -44
- package/components/Text/textFactory.tsx +17 -5
- package/components/TimePicker/TimeInput.tsx +2 -7
- package/components/TimePicker/utils.ts +0 -4
- package/components/TouchableRipple/TouchableRipple.native.tsx +36 -5
- package/components/TouchableRipple/TouchableRipple.tsx +121 -163
- package/components/TouchableRipple/rippleFromForegroundColor.ts +21 -0
- package/package.json +6 -3
- package/components/HorizontalDivider/HorizontalDivider.tsx +0 -103
- package/components/HorizontalDivider/index.tsx +0 -9
- package/components/ListItem/ListItem.tsx +0 -138
- package/components/ListItem/ListItemDescription.tsx +0 -25
- package/components/ListItem/ListItemTitle.tsx +0 -25
- package/components/ListItem/index.tsx +0 -14
- package/components/ListItem/utils.ts +0 -115
- package/components/Menu/MenuDivider.tsx +0 -13
- package/components/Menu/MenuItem.tsx +0 -128
- package/components/VerticalDivider/VerticalDivider.tsx +0 -100
- 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
|
+
);
|
package/components/Menu/Menu.tsx
CHANGED
|
@@ -1,34 +1,81 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
allowDeselect?: boolean;
|
|
13
34
|
};
|
|
14
35
|
|
|
15
|
-
|
|
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 {
|
|
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
|
-
}, [
|
|
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
|
-
<
|
|
44
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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);
|