@os-design-mobile/menu 1.0.75 → 1.0.76

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "@os-design-mobile/menu",
3
- "version": "1.0.75",
3
+ "version": "1.0.76",
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",
18
+ "!**/*.emotion.d.ts"
10
19
  ],
11
20
  "scripts": {
12
21
  "clean": "rimraf dist",
@@ -19,15 +28,15 @@
19
28
  "access": "public"
20
29
  },
21
30
  "dependencies": {
22
- "@os-design-mobile/button": "^1.0.49",
23
- "@os-design-mobile/icons": "^1.0.47",
24
- "@os-design-mobile/modal": "^1.0.79",
25
- "@os-design-mobile/theming": "^1.0.33",
31
+ "@os-design-mobile/button": "^1.0.50",
32
+ "@os-design-mobile/icons": "^1.0.48",
33
+ "@os-design-mobile/modal": "^1.0.80",
34
+ "@os-design-mobile/theming": "^1.0.34",
26
35
  "@os-design/menu-utils": "^1.0.14",
27
36
  "@os-design/use-forwarded-state": "^1.0.13"
28
37
  },
29
38
  "devDependencies": {
30
- "@os-design-mobile/text": "^1.0.45"
39
+ "@os-design-mobile/text": "^1.0.46"
31
40
  },
32
41
  "peerDependencies": {
33
42
  "@emotion/native": ">=11",
@@ -39,5 +48,5 @@
39
48
  "react-native-safe-area-context": ">=3",
40
49
  "react-native-svg": ">=12"
41
50
  },
42
- "gitHead": "4cac95fe2f5cd6dc74885a83b43a730008e20721"
51
+ "gitHead": "b5e7688a2d775c461838b1044ecaf29ddd52b3cd"
43
52
  }
@@ -0,0 +1,7 @@
1
+ import '@emotion/react';
2
+ import { Theme as BaseTheme } from '@os-design-mobile/theming';
3
+
4
+ declare module '@emotion/react' {
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
6
+ export interface Theme extends BaseTheme {}
7
+ }
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;
@@ -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';