@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 CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "@os-design-mobile/menu",
3
- "version": "1.0.75",
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.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",
26
- "@os-design/menu-utils": "^1.0.14",
27
- "@os-design/use-forwarded-state": "^1.0.13"
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.45"
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": "4cac95fe2f5cd6dc74885a83b43a730008e20721"
50
+ "gitHead": "ea2e9015ca3ac20795b3fc1303df9016e0c868db"
43
51
  }
@@ -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';