@os-design-mobile/menu 1.0.75 → 1.0.77
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/package.json +18 -10
- package/src/@types/emotion.d.ts +7 -0
- package/src/Menu.tsx +69 -0
- package/src/MenuDivider.tsx +24 -0
- package/src/MenuGroup.tsx +164 -0
- package/src/MenuItem.tsx +129 -0
- package/src/index.ts +9 -0
package/package.json
CHANGED
@@ -1,12 +1,20 @@
|
|
1
1
|
{
|
2
2
|
"name": "@os-design-mobile/menu",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.77",
|
4
4
|
"license": "UNLICENSED",
|
5
5
|
"repository": "git@gitlab.com:os-team/libs/os-design-mobile.git",
|
6
6
|
"main": "dist/index.js",
|
7
7
|
"types": "dist/index.d.ts",
|
8
|
+
"react-native": "src/index.tsx",
|
8
9
|
"files": [
|
9
|
-
"dist"
|
10
|
+
"dist",
|
11
|
+
"src",
|
12
|
+
"!**/*.test.ts",
|
13
|
+
"!**/*.test.tsx",
|
14
|
+
"!**/__tests__",
|
15
|
+
"!**/*.stories.tsx",
|
16
|
+
"!**/*.stories.mdx",
|
17
|
+
"!**/*.example.tsx"
|
10
18
|
],
|
11
19
|
"scripts": {
|
12
20
|
"clean": "rimraf dist",
|
@@ -19,15 +27,15 @@
|
|
19
27
|
"access": "public"
|
20
28
|
},
|
21
29
|
"dependencies": {
|
22
|
-
"@os-design-mobile/button": "^1.0.
|
23
|
-
"@os-design-mobile/icons": "^1.0.
|
24
|
-
"@os-design-mobile/modal": "^1.0.
|
25
|
-
"@os-design-mobile/theming": "^1.0.
|
26
|
-
"@os-design/menu-utils": "^1.0.
|
27
|
-
"@os-design/use-forwarded-state": "^1.0.
|
30
|
+
"@os-design-mobile/button": "^1.0.51",
|
31
|
+
"@os-design-mobile/icons": "^1.0.49",
|
32
|
+
"@os-design-mobile/modal": "^1.0.81",
|
33
|
+
"@os-design-mobile/theming": "^1.0.35",
|
34
|
+
"@os-design/menu-utils": "^1.0.16",
|
35
|
+
"@os-design/use-forwarded-state": "^1.0.15"
|
28
36
|
},
|
29
37
|
"devDependencies": {
|
30
|
-
"@os-design-mobile/text": "^1.0.
|
38
|
+
"@os-design-mobile/text": "^1.0.47"
|
31
39
|
},
|
32
40
|
"peerDependencies": {
|
33
41
|
"@emotion/native": ">=11",
|
@@ -39,5 +47,5 @@
|
|
39
47
|
"react-native-safe-area-context": ">=3",
|
40
48
|
"react-native-svg": ">=12"
|
41
49
|
},
|
42
|
-
"gitHead": "
|
50
|
+
"gitHead": "ea2e9015ca3ac20795b3fc1303df9016e0c868db"
|
43
51
|
}
|
package/src/Menu.tsx
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
import React, { forwardRef, PropsWithChildren, useMemo } from 'react';
|
2
|
+
import { View } from 'react-native';
|
3
|
+
import Modal, { ModalProps } from '@os-design-mobile/modal';
|
4
|
+
import { MenuContext } from '@os-design/menu-utils';
|
5
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
6
|
+
import styled from '@emotion/native';
|
7
|
+
import { clr, useTheme } from '@os-design-mobile/theming';
|
8
|
+
import MenuGroup from './MenuGroup';
|
9
|
+
|
10
|
+
export interface MenuProps extends PropsWithChildren<ModalProps> {
|
11
|
+
/**
|
12
|
+
* Whether the menu closes when the user selects a menu item.
|
13
|
+
* @default true
|
14
|
+
*/
|
15
|
+
closeOnSelect?: boolean;
|
16
|
+
}
|
17
|
+
|
18
|
+
const StyledModal = styled(Modal)({
|
19
|
+
paddingLeft: 0,
|
20
|
+
paddingRight: 0,
|
21
|
+
});
|
22
|
+
|
23
|
+
/**
|
24
|
+
* The dropdown menu.
|
25
|
+
*/
|
26
|
+
const Menu = forwardRef<View, MenuProps>(
|
27
|
+
({ closeOnSelect = true, onClose = () => {}, children, ...rest }, ref) => {
|
28
|
+
const { theme } = useTheme();
|
29
|
+
const childrenCount = useMemo(
|
30
|
+
() => React.Children.count(children),
|
31
|
+
[children]
|
32
|
+
);
|
33
|
+
const menuContext = useMemo(
|
34
|
+
() => ({ closeOnSelect, onClose }),
|
35
|
+
[closeOnSelect, onClose]
|
36
|
+
);
|
37
|
+
|
38
|
+
return (
|
39
|
+
<MenuContext.Provider value={menuContext}>
|
40
|
+
<StyledModal footer={null} onClose={onClose} {...rest} ref={ref}>
|
41
|
+
<SafeAreaProvider>
|
42
|
+
{React.Children.map(children, (child, index) => {
|
43
|
+
if (!React.isValidElement(child) || child.type !== MenuGroup)
|
44
|
+
return child;
|
45
|
+
let style = {};
|
46
|
+
if (index < childrenCount - 1) {
|
47
|
+
style = {
|
48
|
+
paddingBottom: 0.4 * theme.fontSize,
|
49
|
+
borderBottomWidth: 1,
|
50
|
+
borderBottomColor: clr(theme.menuGroupColorDivider),
|
51
|
+
};
|
52
|
+
}
|
53
|
+
if (index > 0) {
|
54
|
+
style = {
|
55
|
+
marginTop: theme.modalBodyPaddingVertical * theme.fontSize,
|
56
|
+
};
|
57
|
+
}
|
58
|
+
return React.cloneElement<any>(child, { style });
|
59
|
+
})}
|
60
|
+
</SafeAreaProvider>
|
61
|
+
</StyledModal>
|
62
|
+
</MenuContext.Provider>
|
63
|
+
);
|
64
|
+
}
|
65
|
+
);
|
66
|
+
|
67
|
+
Menu.displayName = 'Menu';
|
68
|
+
|
69
|
+
export default Menu;
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import React, { forwardRef } from 'react';
|
2
|
+
import { View, ViewProps } from 'react-native';
|
3
|
+
import styled from '@emotion/native';
|
4
|
+
import { clr } from '@os-design-mobile/theming';
|
5
|
+
|
6
|
+
export type MenuDividerProps = ViewProps;
|
7
|
+
|
8
|
+
const StyledView = styled.View((p) => ({
|
9
|
+
paddingTop: p.theme.menuDividerIndent * p.theme.fontSize,
|
10
|
+
borderBottomWidth: 1,
|
11
|
+
borderBottomColor: clr(p.theme.menuDividerColor),
|
12
|
+
marginBottom: p.theme.menuDividerIndent * p.theme.fontSize,
|
13
|
+
}));
|
14
|
+
|
15
|
+
/**
|
16
|
+
* The divider of menu items.
|
17
|
+
*/
|
18
|
+
const MenuDivider = forwardRef<View, MenuDividerProps>((props, ref) => (
|
19
|
+
<StyledView {...props} ref={ref} />
|
20
|
+
));
|
21
|
+
|
22
|
+
MenuDivider.displayName = 'MenuDivider';
|
23
|
+
|
24
|
+
export default MenuDivider;
|
@@ -0,0 +1,164 @@
|
|
1
|
+
import styled from '@emotion/native';
|
2
|
+
import { clr } from '@os-design-mobile/theming';
|
3
|
+
import useForwardedState from '@os-design/use-forwarded-state';
|
4
|
+
import React, {
|
5
|
+
forwardRef,
|
6
|
+
PropsWithChildren,
|
7
|
+
useCallback,
|
8
|
+
useMemo,
|
9
|
+
} from 'react';
|
10
|
+
import { View, ViewProps } from 'react-native';
|
11
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
12
|
+
import MenuItem from './MenuItem';
|
13
|
+
|
14
|
+
interface BaseMenuGroupProps<T> extends PropsWithChildren<ViewProps> {
|
15
|
+
/**
|
16
|
+
* The title of the menu group.
|
17
|
+
* @default undefined
|
18
|
+
*/
|
19
|
+
title?: string;
|
20
|
+
/**
|
21
|
+
* The max number of options that the user can select. Zero means unlimited.
|
22
|
+
* Works only when multiple is true.
|
23
|
+
* @default 0
|
24
|
+
*/
|
25
|
+
maxSelectedItems?: number;
|
26
|
+
/**
|
27
|
+
* Selected menu items.
|
28
|
+
* @default undefined
|
29
|
+
*/
|
30
|
+
value?: T;
|
31
|
+
/**
|
32
|
+
* The default value.
|
33
|
+
* @default undefined
|
34
|
+
*/
|
35
|
+
defaultValue?: T;
|
36
|
+
/**
|
37
|
+
* The change event handler.
|
38
|
+
* @default undefined
|
39
|
+
*/
|
40
|
+
onChange?: (value: T) => void;
|
41
|
+
}
|
42
|
+
interface MenuGroupNotMultipleProps extends BaseMenuGroupProps<string | null> {
|
43
|
+
/**
|
44
|
+
* Is it possible to select multiple values.
|
45
|
+
* @default false
|
46
|
+
*/
|
47
|
+
multiple?: false;
|
48
|
+
}
|
49
|
+
interface MenuGroupMultipleProps extends BaseMenuGroupProps<string[]> {
|
50
|
+
/**
|
51
|
+
* Is it possible to select multiple values.
|
52
|
+
* @default false
|
53
|
+
*/
|
54
|
+
multiple: true;
|
55
|
+
}
|
56
|
+
export type MenuGroupProps = MenuGroupNotMultipleProps | MenuGroupMultipleProps;
|
57
|
+
|
58
|
+
const Title = styled.Text((p) => ({
|
59
|
+
fontWeight: '500',
|
60
|
+
fontSize: p.theme.sizes.small * p.theme.fontSize,
|
61
|
+
color: clr(p.theme.menuGroupColorTitle),
|
62
|
+
marginBottom: 0.4 * p.theme.fontSize,
|
63
|
+
paddingHorizontal: p.theme.modalBodyPaddingHorizontal * p.theme.fontSize,
|
64
|
+
}));
|
65
|
+
|
66
|
+
/**
|
67
|
+
* The group of menu items.
|
68
|
+
*/
|
69
|
+
const MenuGroup = forwardRef<View, MenuGroupProps>(
|
70
|
+
(
|
71
|
+
{
|
72
|
+
title,
|
73
|
+
multiple = false,
|
74
|
+
maxSelectedItems = 0,
|
75
|
+
value,
|
76
|
+
defaultValue,
|
77
|
+
onChange = () => {},
|
78
|
+
children,
|
79
|
+
...rest
|
80
|
+
},
|
81
|
+
ref
|
82
|
+
) => {
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
84
|
+
const [forwardedValue, setForwardedValue] = useForwardedState<any>({
|
85
|
+
value,
|
86
|
+
defaultValue,
|
87
|
+
onChange,
|
88
|
+
});
|
89
|
+
const safeAreaInsets = useSafeAreaInsets();
|
90
|
+
|
91
|
+
const onSelect = useCallback(
|
92
|
+
(v: string) => {
|
93
|
+
if (multiple) {
|
94
|
+
// Delete the value because it was already selected
|
95
|
+
if ((forwardedValue || []).includes(v)) {
|
96
|
+
setForwardedValue(
|
97
|
+
(forwardedValue || []).filter((item) => item !== v)
|
98
|
+
);
|
99
|
+
return;
|
100
|
+
}
|
101
|
+
|
102
|
+
// Add a new value if the number of selected items is less than max
|
103
|
+
if (
|
104
|
+
maxSelectedItems === 0 ||
|
105
|
+
(forwardedValue || []).length < maxSelectedItems
|
106
|
+
) {
|
107
|
+
setForwardedValue([...(forwardedValue || []), v]);
|
108
|
+
return;
|
109
|
+
}
|
110
|
+
return;
|
111
|
+
}
|
112
|
+
setForwardedValue(v);
|
113
|
+
},
|
114
|
+
[forwardedValue, maxSelectedItems, multiple, setForwardedValue]
|
115
|
+
);
|
116
|
+
|
117
|
+
const menuItems = useMemo(
|
118
|
+
() =>
|
119
|
+
React.Children.map(children, (child) => {
|
120
|
+
if (!React.isValidElement(child) || child.type !== MenuItem)
|
121
|
+
return child;
|
122
|
+
const { value: childValue, onPress: childOnPress } = child.props;
|
123
|
+
const selected =
|
124
|
+
(multiple && (forwardedValue || []).includes(childValue)) ||
|
125
|
+
(!multiple && forwardedValue === childValue);
|
126
|
+
return React.cloneElement<any>(child, {
|
127
|
+
key: childValue,
|
128
|
+
selected,
|
129
|
+
onPress: (e) => {
|
130
|
+
if (!childValue) return;
|
131
|
+
onSelect(childValue);
|
132
|
+
if (childOnPress) childOnPress(e);
|
133
|
+
},
|
134
|
+
});
|
135
|
+
}),
|
136
|
+
[children, forwardedValue, multiple, onSelect]
|
137
|
+
);
|
138
|
+
|
139
|
+
return (
|
140
|
+
<View {...rest} ref={ref}>
|
141
|
+
{title && (
|
142
|
+
<Title
|
143
|
+
numberOfLines={1}
|
144
|
+
style={{
|
145
|
+
...(safeAreaInsets.left > 0
|
146
|
+
? { paddingLeft: safeAreaInsets.left }
|
147
|
+
: {}),
|
148
|
+
...(safeAreaInsets.right > 0
|
149
|
+
? { paddingRight: safeAreaInsets.right }
|
150
|
+
: {}),
|
151
|
+
}}
|
152
|
+
>
|
153
|
+
{title}
|
154
|
+
</Title>
|
155
|
+
)}
|
156
|
+
{menuItems}
|
157
|
+
</View>
|
158
|
+
);
|
159
|
+
}
|
160
|
+
);
|
161
|
+
|
162
|
+
MenuGroup.displayName = 'MenuGroup';
|
163
|
+
|
164
|
+
export default MenuGroup;
|
package/src/MenuItem.tsx
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
import React, {
|
2
|
+
forwardRef,
|
3
|
+
PropsWithChildren,
|
4
|
+
useCallback,
|
5
|
+
useContext,
|
6
|
+
useEffect,
|
7
|
+
useRef,
|
8
|
+
} from 'react';
|
9
|
+
import { clr, ThemeOverrider, useTheme } from '@os-design-mobile/theming';
|
10
|
+
import { Check } from '@os-design-mobile/icons';
|
11
|
+
import Button, { ButtonProps } from '@os-design-mobile/button';
|
12
|
+
import { RectButton } from 'react-native-gesture-handler';
|
13
|
+
import styled from '@emotion/native';
|
14
|
+
import { MenuContext } from '@os-design/menu-utils';
|
15
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
16
|
+
|
17
|
+
export interface MenuItemProps
|
18
|
+
extends PropsWithChildren<Omit<ButtonProps, 'type' | 'size'>> {
|
19
|
+
/**
|
20
|
+
* Whether the menu item is selected.
|
21
|
+
* @default false
|
22
|
+
*/
|
23
|
+
selected?: boolean;
|
24
|
+
/**
|
25
|
+
* The value of the menu item.
|
26
|
+
* @default undefined
|
27
|
+
*/
|
28
|
+
value?: string;
|
29
|
+
}
|
30
|
+
|
31
|
+
const selectedStyles = (p) =>
|
32
|
+
p.selected
|
33
|
+
? {
|
34
|
+
backgroundColor: clr(p.theme.menuItemSelectedColorBg),
|
35
|
+
}
|
36
|
+
: {};
|
37
|
+
|
38
|
+
type StyledButtonProps = Required<Pick<MenuItemProps, 'selected'>>;
|
39
|
+
const StyledButton = styled(Button)<StyledButtonProps>(selectedStyles);
|
40
|
+
|
41
|
+
/**
|
42
|
+
* The base menu item.
|
43
|
+
*/
|
44
|
+
const MenuItem = forwardRef<RectButton, MenuItemProps>(
|
45
|
+
(
|
46
|
+
{
|
47
|
+
selected = false,
|
48
|
+
value,
|
49
|
+
right,
|
50
|
+
viewProps = {},
|
51
|
+
textProps = {},
|
52
|
+
onPress = () => {},
|
53
|
+
...rest
|
54
|
+
},
|
55
|
+
ref
|
56
|
+
) => {
|
57
|
+
const { closeOnSelect, onClose } = useContext(MenuContext);
|
58
|
+
const onPressRef = useRef<MenuItemProps['onPress']>();
|
59
|
+
const { theme } = useTheme();
|
60
|
+
const safeAreaInsets = useSafeAreaInsets();
|
61
|
+
|
62
|
+
useEffect(() => {
|
63
|
+
onPressRef.current = onPress;
|
64
|
+
}, [onPress]);
|
65
|
+
|
66
|
+
const clickHandler = useCallback(
|
67
|
+
(e) => {
|
68
|
+
if (onPressRef.current) onPressRef.current(e);
|
69
|
+
if (closeOnSelect) onClose();
|
70
|
+
},
|
71
|
+
[closeOnSelect, onClose]
|
72
|
+
);
|
73
|
+
|
74
|
+
return (
|
75
|
+
<ThemeOverrider
|
76
|
+
overrides={(t) => ({
|
77
|
+
buttonGhostColorText: t.colorText,
|
78
|
+
borderRadius: 0,
|
79
|
+
buttonHeight: theme.menuItemHeight,
|
80
|
+
buttonPaddingHorizontal: theme.menuItemPaddingHorizontal,
|
81
|
+
})}
|
82
|
+
>
|
83
|
+
<StyledButton
|
84
|
+
selected={selected}
|
85
|
+
right={
|
86
|
+
selected ? (
|
87
|
+
<ThemeOverrider
|
88
|
+
overrides={(t) => ({ colorText: t.menuItemSelectedColorIcon })}
|
89
|
+
>
|
90
|
+
<Check />
|
91
|
+
</ThemeOverrider>
|
92
|
+
) : (
|
93
|
+
right
|
94
|
+
)
|
95
|
+
}
|
96
|
+
type='ghost'
|
97
|
+
onPress={clickHandler}
|
98
|
+
viewProps={{
|
99
|
+
...viewProps,
|
100
|
+
style: {
|
101
|
+
...(safeAreaInsets.left > 0
|
102
|
+
? { paddingLeft: safeAreaInsets.left }
|
103
|
+
: {}),
|
104
|
+
...(safeAreaInsets.right > 0
|
105
|
+
? { paddingRight: safeAreaInsets.right }
|
106
|
+
: {}),
|
107
|
+
...(typeof viewProps?.style === 'object' ? viewProps.style : {}),
|
108
|
+
},
|
109
|
+
}}
|
110
|
+
textProps={{
|
111
|
+
...textProps,
|
112
|
+
style: {
|
113
|
+
flex: 1,
|
114
|
+
textAlign: 'left',
|
115
|
+
fontWeight: 'normal',
|
116
|
+
...(typeof textProps?.style === 'object' ? textProps.style : {}),
|
117
|
+
},
|
118
|
+
}}
|
119
|
+
{...rest}
|
120
|
+
ref={ref}
|
121
|
+
/>
|
122
|
+
</ThemeOverrider>
|
123
|
+
);
|
124
|
+
}
|
125
|
+
);
|
126
|
+
|
127
|
+
MenuItem.displayName = 'MenuItem';
|
128
|
+
|
129
|
+
export default MenuItem;
|
package/src/index.ts
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
export { default as Menu } from './Menu';
|
2
|
+
export { default as MenuDivider } from './MenuDivider';
|
3
|
+
export { default as MenuGroup } from './MenuGroup';
|
4
|
+
export { default as MenuItem } from './MenuItem';
|
5
|
+
|
6
|
+
export * from './Menu';
|
7
|
+
export * from './MenuDivider';
|
8
|
+
export * from './MenuGroup';
|
9
|
+
export * from './MenuItem';
|