@utilitywarehouse/hearth-react-native 0.8.1 → 0.9.0
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/.storybook/preview.tsx +1 -0
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +24 -0
- package/build/components/Banner/Banner.js +49 -10
- package/build/components/Banner/Banner.props.d.ts +4 -9
- package/build/components/BottomSheet/BottomSheetHandle.js +8 -0
- package/build/components/Card/Card.props.d.ts +1 -0
- package/build/components/Card/CardRoot.d.ts +1 -1
- package/build/components/Card/CardRoot.js +28 -1
- package/build/components/HighlightBanner/HighlightBanner.props.d.ts +1 -1
- package/build/components/List/List.js +1 -1
- package/build/components/List/ListAction/ListActionTrailingIcon.js +2 -2
- package/build/components/Menu/Menu.context.d.ts +5 -0
- package/build/components/Menu/Menu.context.js +9 -0
- package/build/components/Menu/Menu.d.ts +4 -0
- package/build/components/Menu/Menu.js +25 -0
- package/build/components/Menu/Menu.props.d.ts +21 -0
- package/build/components/Menu/Menu.props.js +1 -0
- package/build/components/Menu/MenuItem.d.ts +18 -0
- package/build/components/Menu/MenuItem.js +115 -0
- package/build/components/Menu/MenuItem.props.d.ts +27 -0
- package/build/components/Menu/MenuItem.props.js +1 -0
- package/build/components/Menu/MenuTrigger.d.ts +9 -0
- package/build/components/Menu/MenuTrigger.js +11 -0
- package/build/components/Menu/MenuTrigger.props.d.ts +12 -0
- package/build/components/Menu/MenuTrigger.props.js +1 -0
- package/build/components/Menu/index.d.ts +7 -0
- package/build/components/Menu/index.js +4 -0
- package/build/components/Modal/Modal.d.ts +1 -1
- package/build/components/Modal/Modal.js +32 -30
- package/build/components/Modal/Modal.props.d.ts +1 -0
- package/build/components/Modal/Modal.web.d.ts +1 -1
- package/build/components/Modal/Modal.web.js +25 -25
- package/build/components/RadioCard/RadioCardGroup.context.d.ts +12 -0
- package/build/components/RadioCard/RadioCardGroup.context.js +3 -0
- package/build/components/RadioCard/RadioCardGroup.js +15 -10
- package/build/components/RadioCard/RadioCardLabel.d.ts +1 -1
- package/build/components/RadioCard/RadioCardLabel.js +7 -1
- package/build/components/RadioCard/RadioCardRoot.js +13 -0
- package/build/components/index.d.ts +1 -0
- package/build/components/index.js +1 -0
- package/build/core/themes.d.ts +40 -0
- package/build/core/themes.js +20 -0
- package/build/tokens/components/dark/index.d.ts +3 -1
- package/build/tokens/components/dark/index.js +3 -1
- package/build/tokens/components/dark/input.d.ts +3 -0
- package/build/tokens/components/dark/input.js +3 -0
- package/build/tokens/components/dark/modal.d.ts +7 -4
- package/build/tokens/components/dark/modal.js +7 -4
- package/build/tokens/components/dark/rating.d.ts +8 -0
- package/build/tokens/components/dark/rating.js +7 -0
- package/build/tokens/components/dark/table.d.ts +0 -3
- package/build/tokens/components/dark/table.js +0 -3
- package/build/tokens/components/dark/time-picker.d.ts +29 -0
- package/build/tokens/components/dark/time-picker.js +28 -0
- package/build/tokens/components/dark/timeline.d.ts +27 -0
- package/build/tokens/components/dark/timeline.js +26 -0
- package/build/tokens/components/light/index.d.ts +3 -1
- package/build/tokens/components/light/index.js +3 -1
- package/build/tokens/components/light/input.d.ts +3 -0
- package/build/tokens/components/light/input.js +3 -0
- package/build/tokens/components/light/modal.d.ts +7 -4
- package/build/tokens/components/light/modal.js +7 -4
- package/build/tokens/components/light/rating.d.ts +8 -0
- package/build/tokens/components/light/rating.js +7 -0
- package/build/tokens/components/light/table.d.ts +0 -3
- package/build/tokens/components/light/table.js +0 -3
- package/build/tokens/components/light/time-picker.d.ts +29 -0
- package/build/tokens/components/light/time-picker.js +28 -0
- package/build/tokens/components/light/timeline.d.ts +27 -0
- package/build/tokens/components/light/timeline.js +26 -0
- package/docs/adding-shadows.mdx +43 -0
- package/docs/components/AllComponents.web.tsx +33 -0
- package/docs/components/BackToTopButton.tsx +1 -1
- package/package.json +3 -3
- package/src/components/Banner/Banner.docs.mdx +20 -11
- package/src/components/Banner/Banner.props.ts +4 -9
- package/src/components/Banner/Banner.stories.tsx +17 -4
- package/src/components/Banner/Banner.tsx +92 -37
- package/src/components/BottomSheet/BottomSheetHandle.tsx +12 -0
- package/src/components/Card/Card.docs.mdx +20 -1
- package/src/components/Card/Card.props.ts +9 -0
- package/src/components/Card/Card.stories.tsx +39 -0
- package/src/components/Card/CardRoot.tsx +29 -0
- package/src/components/Checkbox/CheckboxGroup.stories.tsx +19 -1
- package/src/components/DatePickerInput/DatePickerInput.docs.mdx +1 -1
- package/src/components/HighlightBanner/HighlightBanner.docs.mdx +1 -1
- package/src/components/HighlightBanner/HighlightBanner.props.ts +1 -0
- package/src/components/HighlightBanner/HighlightBanner.stories.tsx +15 -1
- package/src/components/List/List.tsx +5 -3
- package/src/components/List/ListAction/ListActionTrailingIcon.tsx +2 -2
- package/src/components/Menu/Menu.context.ts +15 -0
- package/src/components/Menu/Menu.docs.mdx +158 -0
- package/src/components/Menu/Menu.props.ts +24 -0
- package/src/components/Menu/Menu.stories.tsx +292 -0
- package/src/components/Menu/Menu.tsx +54 -0
- package/src/components/Menu/MenuItem.props.ts +29 -0
- package/src/components/Menu/MenuItem.tsx +145 -0
- package/src/components/Menu/MenuTrigger.props.ts +14 -0
- package/src/components/Menu/MenuTrigger.tsx +20 -0
- package/src/components/Menu/index.ts +7 -0
- package/src/components/Modal/Modal.docs.mdx +34 -5
- package/src/components/Modal/Modal.props.ts +1 -0
- package/src/components/Modal/Modal.stories.tsx +46 -0
- package/src/components/Modal/Modal.tsx +37 -33
- package/src/components/Modal/Modal.web.tsx +27 -27
- package/src/components/Radio/RadioGroup.stories.tsx +18 -0
- package/src/components/RadioCard/RadioCardGroup.context.ts +16 -0
- package/src/components/RadioCard/RadioCardGroup.stories.tsx +24 -0
- package/src/components/RadioCard/RadioCardGroup.tsx +28 -19
- package/src/components/RadioCard/RadioCardLabel.tsx +12 -1
- package/src/components/RadioCard/RadioCardRoot.tsx +15 -0
- package/src/components/index.ts +1 -0
- package/src/core/themes.ts +20 -0
- package/src/tokens/components/dark/index.ts +3 -1
- package/src/tokens/components/dark/input.ts +3 -0
- package/src/tokens/components/dark/modal.ts +7 -4
- package/src/tokens/components/dark/rating.ts +8 -0
- package/src/tokens/components/dark/table.ts +0 -3
- package/src/tokens/components/dark/time-picker.ts +29 -0
- package/src/tokens/components/dark/timeline.ts +27 -0
- package/src/tokens/components/light/index.ts +3 -1
- package/src/tokens/components/light/input.ts +3 -0
- package/src/tokens/components/light/modal.ts +7 -4
- package/src/tokens/components/light/rating.ts +8 -0
- package/src/tokens/components/light/table.ts +0 -3
- package/src/tokens/components/light/time-picker.ts +29 -0
- package/src/tokens/components/light/timeline.ts +27 -0
- package/build/tokens/components/dark/dialog.d.ts +0 -25
- package/build/tokens/components/dark/dialog.js +0 -24
- package/build/tokens/components/light/dialog.d.ts +0 -25
- package/build/tokens/components/light/dialog.js +0 -24
- package/src/tokens/components/dark/dialog.ts +0 -25
- package/src/tokens/components/light/dialog.ts +0 -25
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta, Story } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import { BackToTopButton, ViewFigmaButton } from '../../../docs/components';
|
|
3
|
+
import * as Stories from './Menu.stories';
|
|
4
|
+
|
|
5
|
+
<Meta title="Components / Menu" />
|
|
6
|
+
|
|
7
|
+
<ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens" />
|
|
8
|
+
|
|
9
|
+
<BackToTopButton />
|
|
10
|
+
|
|
11
|
+
# Menu
|
|
12
|
+
|
|
13
|
+
The `Menu` component provides a bottom sheet modal with a list of actions. It's perfect for contextual menus, action sheets, and option lists. The component uses `BottomSheetModal` under the hood and includes `MenuItem` components that can display icons, text, and support different color schemes.
|
|
14
|
+
|
|
15
|
+
- [Playground](#playground)
|
|
16
|
+
- [Usage](#usage)
|
|
17
|
+
- [Props](#props)
|
|
18
|
+
- [Features](#features)
|
|
19
|
+
- [Examples](#examples)
|
|
20
|
+
- [Basic Menu](#basic-menu)
|
|
21
|
+
- [With Destructive Action](#with-destructive-action)
|
|
22
|
+
- [Icon on Right](#icon-on-right)
|
|
23
|
+
- [Without Icons](#without-icons)
|
|
24
|
+
- [With Disabled Items](#with-disabled-items)
|
|
25
|
+
- [Without Heading](#without-heading)
|
|
26
|
+
- [Accessibility](#accessibility)
|
|
27
|
+
|
|
28
|
+
## Playground
|
|
29
|
+
|
|
30
|
+
<Canvas of={Stories.Playground} />
|
|
31
|
+
|
|
32
|
+
<Controls of={Stories.Playground} />
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### Basic Usage
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { useRef } from 'react';
|
|
40
|
+
import { Menu, MenuItem, MenuTrigger } from '@utilitywarehouse/hearth-react-native';
|
|
41
|
+
import { EditSmallIcon, TrashSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
42
|
+
|
|
43
|
+
const MyComponent = () => {
|
|
44
|
+
const menuRef = useRef(null);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
<MenuTrigger onPress={() => menuRef.current?.present()}>
|
|
49
|
+
<Button>Open Menu</Button>
|
|
50
|
+
</MenuTrigger>
|
|
51
|
+
|
|
52
|
+
<Menu ref={menuRef} heading="Actions">
|
|
53
|
+
<MenuItem icon={EditSmallIcon} text="Edit" onPress={() => console.log('Edit')} />
|
|
54
|
+
<MenuItem
|
|
55
|
+
icon={TrashSmallIcon}
|
|
56
|
+
text="Delete"
|
|
57
|
+
colorScheme="destructive"
|
|
58
|
+
onPress={() => console.log('Delete')}
|
|
59
|
+
/>
|
|
60
|
+
</Menu>
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Props
|
|
67
|
+
|
|
68
|
+
### Menu
|
|
69
|
+
|
|
70
|
+
| Prop | Type | Default | Description |
|
|
71
|
+
| ------------------ | -------------------------------- | ------- | --------------------------------------------------- |
|
|
72
|
+
| `heading` | `string` | - | Heading text displayed at the top of the menu |
|
|
73
|
+
| `children` | `ReactNode` | - | Menu items to display |
|
|
74
|
+
| `bottomSheetProps` | `Partial<BottomSheetModalProps>` | - | Optional bottom sheet modal props for customization |
|
|
75
|
+
|
|
76
|
+
### MenuItem
|
|
77
|
+
|
|
78
|
+
| Prop | Type | Default | Description |
|
|
79
|
+
| -------------- | ---------------------------------------- | -------------- | ---------------------------------- |
|
|
80
|
+
| `icon` | `ComponentType` | - | Icon component to display |
|
|
81
|
+
| `iconPosition` | `'left' \| 'right'` | `'left'` | Position of the icon |
|
|
82
|
+
| `text` | `string` | - | Text to display in the menu item |
|
|
83
|
+
| `colorScheme` | `'functional' \| 'destructive'` | `'functional'` | Color scheme for the menu item |
|
|
84
|
+
| `disabled` | `boolean` | `false` | Whether the menu item is disabled |
|
|
85
|
+
| `onPress` | `(event: GestureResponderEvent) => void` | - | Callback when menu item is pressed |
|
|
86
|
+
|
|
87
|
+
### MenuTrigger
|
|
88
|
+
|
|
89
|
+
| Prop | Type | Description |
|
|
90
|
+
| ---------- | -------------- | ------------------------------------------------------------------- |
|
|
91
|
+
| `children` | `ReactElement` | The child element that triggers the menu (must be a single element) |
|
|
92
|
+
| `onPress` | `() => void` | Function that opens the menu |
|
|
93
|
+
|
|
94
|
+
## Features
|
|
95
|
+
|
|
96
|
+
### Icon Positioning
|
|
97
|
+
|
|
98
|
+
Icons can be positioned on either the left or right side of the menu item text using the `iconPosition` prop.
|
|
99
|
+
|
|
100
|
+
### Color Schemes
|
|
101
|
+
|
|
102
|
+
Menu items support two color schemes:
|
|
103
|
+
|
|
104
|
+
- `functional` (default): Uses primary text and icon colors
|
|
105
|
+
- `destructive`: Uses danger/destructive colors for actions like delete
|
|
106
|
+
|
|
107
|
+
### Disabled State
|
|
108
|
+
|
|
109
|
+
Menu items can be disabled using the `disabled` prop, which applies reduced opacity and prevents interaction.
|
|
110
|
+
|
|
111
|
+
### Auto-closing
|
|
112
|
+
|
|
113
|
+
Menu items automatically close the menu when pressed, making it easy to handle actions without manual menu dismissal.
|
|
114
|
+
|
|
115
|
+
## Examples
|
|
116
|
+
|
|
117
|
+
### Basic Menu
|
|
118
|
+
|
|
119
|
+
A simple menu with icon-based actions:
|
|
120
|
+
|
|
121
|
+
<Canvas of={Stories.BasicMenu} />
|
|
122
|
+
|
|
123
|
+
### With Destructive Action
|
|
124
|
+
|
|
125
|
+
Menu with a destructive action styled differently:
|
|
126
|
+
|
|
127
|
+
<Canvas of={Stories.WithDestructiveAction} />
|
|
128
|
+
|
|
129
|
+
### Icon on Right
|
|
130
|
+
|
|
131
|
+
Menu items with icons positioned on the right side:
|
|
132
|
+
|
|
133
|
+
<Canvas of={Stories.IconOnRight} />
|
|
134
|
+
|
|
135
|
+
### Without Icons
|
|
136
|
+
|
|
137
|
+
Simple text-only menu items:
|
|
138
|
+
|
|
139
|
+
<Canvas of={Stories.WithoutIcons} />
|
|
140
|
+
|
|
141
|
+
### With Disabled Items
|
|
142
|
+
|
|
143
|
+
Menu with some items disabled:
|
|
144
|
+
|
|
145
|
+
<Canvas of={Stories.WithDisabledItems} />
|
|
146
|
+
|
|
147
|
+
### Without Heading
|
|
148
|
+
|
|
149
|
+
Menu without a heading section:
|
|
150
|
+
|
|
151
|
+
<Canvas of={Stories.WithoutHeading} />
|
|
152
|
+
|
|
153
|
+
## Accessibility
|
|
154
|
+
|
|
155
|
+
- Menu items have `accessibilityRole="button"` for proper screen reader support
|
|
156
|
+
- Disabled state is communicated via `accessibilityState`
|
|
157
|
+
- Keyboard navigation is supported on web platforms
|
|
158
|
+
- All interactive elements are properly focusable
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BottomSheetModalProps } from '@gorhom/bottom-sheet';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface MenuMethods {
|
|
5
|
+
present: () => void;
|
|
6
|
+
dismiss: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MenuProps {
|
|
10
|
+
/**
|
|
11
|
+
* Heading text displayed at the top of the menu
|
|
12
|
+
*/
|
|
13
|
+
heading?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Menu items to display
|
|
16
|
+
*/
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
/**
|
|
19
|
+
* Optional bottom sheet modal props to customise the menu behavior
|
|
20
|
+
*/
|
|
21
|
+
bottomSheetProps?: Partial<BottomSheetModalProps>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default MenuProps;
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-native';
|
|
2
|
+
import {
|
|
3
|
+
DownloadSmallIcon,
|
|
4
|
+
EditSmallIcon,
|
|
5
|
+
SettingsMediumIcon,
|
|
6
|
+
ShareSmallIcon,
|
|
7
|
+
TickSmallIcon,
|
|
8
|
+
TrashSmallIcon,
|
|
9
|
+
} from '@utilitywarehouse/hearth-react-native-icons';
|
|
10
|
+
import { useRef } from 'react';
|
|
11
|
+
import { Platform, View } from 'react-native';
|
|
12
|
+
import { Button } from '../Button';
|
|
13
|
+
import { Menu, MenuItem, MenuTrigger } from './';
|
|
14
|
+
import type { MenuMethods } from './Menu.props';
|
|
15
|
+
|
|
16
|
+
import { ViewWrap } from '../../../docs/components';
|
|
17
|
+
|
|
18
|
+
const meta: Meta<typeof Menu> = {
|
|
19
|
+
title: 'Stories / Menu',
|
|
20
|
+
component: Menu,
|
|
21
|
+
parameters: {
|
|
22
|
+
layout: 'centered',
|
|
23
|
+
noScroll: true,
|
|
24
|
+
},
|
|
25
|
+
argTypes: {},
|
|
26
|
+
args: {
|
|
27
|
+
heading: 'Menu Options',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default meta;
|
|
32
|
+
type Story = StoryObj<typeof meta>;
|
|
33
|
+
|
|
34
|
+
export const Playground: Story = {
|
|
35
|
+
render: ({ ...args }) => {
|
|
36
|
+
const menuRef = useRef<MenuMethods>(null);
|
|
37
|
+
|
|
38
|
+
const openMenu = () => {
|
|
39
|
+
menuRef.current?.present();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : { flex: 1 }}>
|
|
44
|
+
<ViewWrap>
|
|
45
|
+
<MenuTrigger onPress={openMenu}>
|
|
46
|
+
<Button>Open Menu</Button>
|
|
47
|
+
</MenuTrigger>
|
|
48
|
+
|
|
49
|
+
<Menu ref={menuRef} {...args}>
|
|
50
|
+
<MenuItem
|
|
51
|
+
icon={EditSmallIcon}
|
|
52
|
+
text="Edit"
|
|
53
|
+
onPress={() => console.log('Edit pressed')}
|
|
54
|
+
/>
|
|
55
|
+
<MenuItem
|
|
56
|
+
icon={ShareSmallIcon}
|
|
57
|
+
text="Share"
|
|
58
|
+
onPress={() => console.log('Share pressed')}
|
|
59
|
+
/>
|
|
60
|
+
<MenuItem
|
|
61
|
+
icon={DownloadSmallIcon}
|
|
62
|
+
text="Download"
|
|
63
|
+
onPress={() => console.log('Download pressed')}
|
|
64
|
+
/>
|
|
65
|
+
<MenuItem
|
|
66
|
+
icon={TrashSmallIcon}
|
|
67
|
+
text="Delete"
|
|
68
|
+
colorScheme="destructive"
|
|
69
|
+
onPress={() => console.log('Delete pressed')}
|
|
70
|
+
/>
|
|
71
|
+
</Menu>
|
|
72
|
+
</ViewWrap>
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const BasicMenu = () => {
|
|
79
|
+
const menuRef = useRef<MenuMethods>(null);
|
|
80
|
+
|
|
81
|
+
const openMenu = () => {
|
|
82
|
+
menuRef.current?.present();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : { flex: 1 }}>
|
|
87
|
+
<ViewWrap>
|
|
88
|
+
<MenuTrigger onPress={openMenu}>
|
|
89
|
+
<Button>Actions</Button>
|
|
90
|
+
</MenuTrigger>
|
|
91
|
+
|
|
92
|
+
<Menu ref={menuRef} heading="Actions">
|
|
93
|
+
<MenuItem icon={EditSmallIcon} text="Edit" onPress={() => console.log('Edit pressed')} />
|
|
94
|
+
<MenuItem
|
|
95
|
+
icon={ShareSmallIcon}
|
|
96
|
+
text="Share"
|
|
97
|
+
onPress={() => console.log('Share pressed')}
|
|
98
|
+
/>
|
|
99
|
+
<MenuItem
|
|
100
|
+
icon={DownloadSmallIcon}
|
|
101
|
+
text="Download"
|
|
102
|
+
onPress={() => console.log('Download pressed')}
|
|
103
|
+
/>
|
|
104
|
+
</Menu>
|
|
105
|
+
</ViewWrap>
|
|
106
|
+
</View>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const WithDestructiveAction = () => {
|
|
111
|
+
const menuRef = useRef<MenuMethods>(null);
|
|
112
|
+
|
|
113
|
+
const openMenu = () => {
|
|
114
|
+
menuRef.current?.present();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
|
|
119
|
+
<ViewWrap>
|
|
120
|
+
<MenuTrigger onPress={openMenu}>
|
|
121
|
+
<Button>File Options</Button>
|
|
122
|
+
</MenuTrigger>
|
|
123
|
+
|
|
124
|
+
<Menu ref={menuRef} heading="File Options">
|
|
125
|
+
<MenuItem
|
|
126
|
+
icon={EditSmallIcon}
|
|
127
|
+
text="Rename"
|
|
128
|
+
onPress={() => console.log('Rename pressed')}
|
|
129
|
+
/>
|
|
130
|
+
<MenuItem
|
|
131
|
+
icon={ShareSmallIcon}
|
|
132
|
+
text="Share"
|
|
133
|
+
onPress={() => console.log('Share pressed')}
|
|
134
|
+
/>
|
|
135
|
+
<MenuItem
|
|
136
|
+
icon={DownloadSmallIcon}
|
|
137
|
+
text="Download"
|
|
138
|
+
onPress={() => console.log('Download pressed')}
|
|
139
|
+
/>
|
|
140
|
+
<MenuItem
|
|
141
|
+
icon={TrashSmallIcon}
|
|
142
|
+
text="Delete"
|
|
143
|
+
colorScheme="destructive"
|
|
144
|
+
onPress={() => console.log('Delete pressed')}
|
|
145
|
+
/>
|
|
146
|
+
</Menu>
|
|
147
|
+
</ViewWrap>
|
|
148
|
+
</View>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const IconOnRight = () => {
|
|
153
|
+
const menuRef = useRef<MenuMethods>(null);
|
|
154
|
+
|
|
155
|
+
const openMenu = () => {
|
|
156
|
+
menuRef.current?.present();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
|
|
161
|
+
<ViewWrap>
|
|
162
|
+
<MenuTrigger onPress={openMenu}>
|
|
163
|
+
<Button>Settings</Button>
|
|
164
|
+
</MenuTrigger>
|
|
165
|
+
|
|
166
|
+
<Menu ref={menuRef} heading="Settings">
|
|
167
|
+
<MenuItem
|
|
168
|
+
icon={TickSmallIcon}
|
|
169
|
+
iconPosition="right"
|
|
170
|
+
text="Enable Notifications"
|
|
171
|
+
onPress={() => console.log('Toggle notifications')}
|
|
172
|
+
/>
|
|
173
|
+
<MenuItem
|
|
174
|
+
icon={TickSmallIcon}
|
|
175
|
+
iconPosition="right"
|
|
176
|
+
text="Dark Mode"
|
|
177
|
+
onPress={() => console.log('Toggle dark mode')}
|
|
178
|
+
/>
|
|
179
|
+
<MenuItem
|
|
180
|
+
icon={SettingsMediumIcon}
|
|
181
|
+
iconPosition="right"
|
|
182
|
+
text="Advanced Settings"
|
|
183
|
+
onPress={() => console.log('Open advanced settings')}
|
|
184
|
+
/>
|
|
185
|
+
</Menu>
|
|
186
|
+
</ViewWrap>
|
|
187
|
+
</View>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const WithoutIcons = () => {
|
|
192
|
+
const menuRef = useRef<MenuMethods>(null);
|
|
193
|
+
|
|
194
|
+
const openMenu = () => {
|
|
195
|
+
menuRef.current?.present();
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
|
|
200
|
+
<ViewWrap>
|
|
201
|
+
<MenuTrigger onPress={openMenu}>
|
|
202
|
+
<Button>Sort By</Button>
|
|
203
|
+
</MenuTrigger>
|
|
204
|
+
|
|
205
|
+
<Menu ref={menuRef} heading="Sort By">
|
|
206
|
+
<MenuItem text="Name" onPress={() => console.log('Sort by name')} />
|
|
207
|
+
<MenuItem text="Date" onPress={() => console.log('Sort by date')} />
|
|
208
|
+
<MenuItem text="Size" onPress={() => console.log('Sort by size')} />
|
|
209
|
+
<MenuItem text="Type" onPress={() => console.log('Sort by type')} />
|
|
210
|
+
</Menu>
|
|
211
|
+
</ViewWrap>
|
|
212
|
+
</View>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const WithDisabledItems = () => {
|
|
217
|
+
const menuRef = useRef<MenuMethods>(null);
|
|
218
|
+
|
|
219
|
+
const openMenu = () => {
|
|
220
|
+
menuRef.current?.present();
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
|
|
225
|
+
<ViewWrap>
|
|
226
|
+
<MenuTrigger onPress={openMenu}>
|
|
227
|
+
<Button>Document Actions</Button>
|
|
228
|
+
</MenuTrigger>
|
|
229
|
+
|
|
230
|
+
<Menu ref={menuRef} heading="Document Actions">
|
|
231
|
+
<MenuItem icon={EditSmallIcon} text="Edit" onPress={() => console.log('Edit pressed')} />
|
|
232
|
+
<MenuItem
|
|
233
|
+
icon={ShareSmallIcon}
|
|
234
|
+
text="Share"
|
|
235
|
+
disabled
|
|
236
|
+
onPress={() => console.log('Share pressed')}
|
|
237
|
+
/>
|
|
238
|
+
<MenuItem
|
|
239
|
+
icon={DownloadSmallIcon}
|
|
240
|
+
text="Download"
|
|
241
|
+
disabled
|
|
242
|
+
onPress={() => console.log('Download pressed')}
|
|
243
|
+
/>
|
|
244
|
+
<MenuItem
|
|
245
|
+
icon={TrashSmallIcon}
|
|
246
|
+
text="Delete"
|
|
247
|
+
colorScheme="destructive"
|
|
248
|
+
onPress={() => console.log('Delete pressed')}
|
|
249
|
+
/>
|
|
250
|
+
</Menu>
|
|
251
|
+
</ViewWrap>
|
|
252
|
+
</View>
|
|
253
|
+
);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const WithoutHeading = () => {
|
|
257
|
+
const menuRef = useRef<MenuMethods>(null);
|
|
258
|
+
|
|
259
|
+
const openMenu = () => {
|
|
260
|
+
menuRef.current?.present();
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<View style={Platform.OS === 'web' ? { width: 400, height: 400 } : {}}>
|
|
265
|
+
<ViewWrap>
|
|
266
|
+
<MenuTrigger onPress={openMenu}>
|
|
267
|
+
<Button>Quick Actions</Button>
|
|
268
|
+
</MenuTrigger>
|
|
269
|
+
|
|
270
|
+
<Menu ref={menuRef}>
|
|
271
|
+
<MenuItem icon={EditSmallIcon} text="Edit" onPress={() => console.log('Edit pressed')} />
|
|
272
|
+
<MenuItem
|
|
273
|
+
icon={ShareSmallIcon}
|
|
274
|
+
text="Share"
|
|
275
|
+
onPress={() => console.log('Share pressed')}
|
|
276
|
+
/>
|
|
277
|
+
<MenuItem
|
|
278
|
+
icon={DownloadSmallIcon}
|
|
279
|
+
text="Download"
|
|
280
|
+
onPress={() => console.log('Download pressed')}
|
|
281
|
+
/>
|
|
282
|
+
<MenuItem
|
|
283
|
+
icon={TrashSmallIcon}
|
|
284
|
+
text="Delete"
|
|
285
|
+
colorScheme="destructive"
|
|
286
|
+
onPress={() => console.log('Delete pressed')}
|
|
287
|
+
/>
|
|
288
|
+
</Menu>
|
|
289
|
+
</ViewWrap>
|
|
290
|
+
</View>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
3
|
+
import { BodyText } from '../BodyText';
|
|
4
|
+
import { BottomSheetModal, BottomSheetProps, BottomSheetScrollView } from '../BottomSheet';
|
|
5
|
+
import { MenuContext } from './Menu.context';
|
|
6
|
+
import type MenuProps from './Menu.props';
|
|
7
|
+
import type { MenuMethods } from './Menu.props';
|
|
8
|
+
|
|
9
|
+
const Menu = forwardRef<MenuMethods, MenuProps>(({ heading, children, bottomSheetProps }, ref) => {
|
|
10
|
+
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
|
11
|
+
|
|
12
|
+
useImperativeHandle(
|
|
13
|
+
ref,
|
|
14
|
+
() => ({
|
|
15
|
+
present: () => bottomSheetModalRef.current?.present(),
|
|
16
|
+
dismiss: () => bottomSheetModalRef.current?.dismiss(),
|
|
17
|
+
}),
|
|
18
|
+
[]
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const handleClose = useCallback(() => {
|
|
22
|
+
bottomSheetModalRef.current?.dismiss();
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const contextValue = useMemo(() => ({ close: handleClose }), [handleClose]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<BottomSheetModal
|
|
29
|
+
ref={bottomSheetModalRef}
|
|
30
|
+
{...(bottomSheetProps as Partial<BottomSheetProps>)}
|
|
31
|
+
>
|
|
32
|
+
<BottomSheetScrollView contentContainerStyle={styles.container}>
|
|
33
|
+
<MenuContext.Provider value={contextValue}>
|
|
34
|
+
{heading && (
|
|
35
|
+
<BodyText size="md" weight="semibold">
|
|
36
|
+
{heading}
|
|
37
|
+
</BodyText>
|
|
38
|
+
)}
|
|
39
|
+
{children}
|
|
40
|
+
</MenuContext.Provider>
|
|
41
|
+
</BottomSheetScrollView>
|
|
42
|
+
</BottomSheetModal>
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
Menu.displayName = 'Menu';
|
|
47
|
+
|
|
48
|
+
const styles = StyleSheet.create(theme => ({
|
|
49
|
+
container: {
|
|
50
|
+
gap: theme.components.bottomSheet.gap,
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
export default Menu;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { PressableProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface MenuItemProps extends Omit<PressableProps, 'children'> {
|
|
5
|
+
/**
|
|
6
|
+
* Icon component to display
|
|
7
|
+
*/
|
|
8
|
+
icon?: ComponentType;
|
|
9
|
+
/**
|
|
10
|
+
* Position of the icon
|
|
11
|
+
* @default 'left'
|
|
12
|
+
*/
|
|
13
|
+
iconPosition?: 'left' | 'right';
|
|
14
|
+
/**
|
|
15
|
+
* Text to display in the menu item
|
|
16
|
+
*/
|
|
17
|
+
text: string;
|
|
18
|
+
/**
|
|
19
|
+
* Color scheme for the menu item
|
|
20
|
+
* @default 'functional'
|
|
21
|
+
*/
|
|
22
|
+
colorScheme?: 'functional' | 'destructive';
|
|
23
|
+
/**
|
|
24
|
+
* Whether the menu item is disabled
|
|
25
|
+
*/
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default MenuItemProps;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { createPressable } from '@gluestack-ui/pressable';
|
|
2
|
+
import { Pressable } from 'react-native';
|
|
3
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
4
|
+
import { BodyText } from '../BodyText';
|
|
5
|
+
import { Icon } from '../Icon';
|
|
6
|
+
import { useMenuContext } from './Menu.context';
|
|
7
|
+
import type MenuItemProps from './MenuItem.props';
|
|
8
|
+
|
|
9
|
+
const MenuItemRoot = ({
|
|
10
|
+
icon,
|
|
11
|
+
iconPosition = 'left',
|
|
12
|
+
text,
|
|
13
|
+
colorScheme = 'functional',
|
|
14
|
+
disabled = false,
|
|
15
|
+
onPress,
|
|
16
|
+
states = {},
|
|
17
|
+
...props
|
|
18
|
+
}: MenuItemProps & { states?: { active?: boolean; disabled?: boolean } }) => {
|
|
19
|
+
const { active } = states;
|
|
20
|
+
const { close } = useMenuContext();
|
|
21
|
+
|
|
22
|
+
styles.useVariants({ colorScheme, disabled, iconPosition, active });
|
|
23
|
+
|
|
24
|
+
const handlePress = (event: any) => {
|
|
25
|
+
if (disabled) return;
|
|
26
|
+
onPress?.(event);
|
|
27
|
+
close();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Pressable
|
|
32
|
+
{...props}
|
|
33
|
+
onPress={handlePress}
|
|
34
|
+
disabled={disabled}
|
|
35
|
+
style={styles.container}
|
|
36
|
+
accessibilityRole="button"
|
|
37
|
+
accessibilityState={{ disabled }}
|
|
38
|
+
>
|
|
39
|
+
{!!icon && <Icon as={icon} style={styles.icon} />}
|
|
40
|
+
<BodyText size="lg" style={styles.text}>
|
|
41
|
+
{text}
|
|
42
|
+
</BodyText>
|
|
43
|
+
</Pressable>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const MenuItem = createPressable({ Root: MenuItemRoot });
|
|
48
|
+
|
|
49
|
+
MenuItem.displayName = 'MenuItem';
|
|
50
|
+
|
|
51
|
+
const styles = StyleSheet.create(theme => ({
|
|
52
|
+
container: {
|
|
53
|
+
flexDirection: 'row',
|
|
54
|
+
alignItems: 'center',
|
|
55
|
+
paddingVertical: theme.components.menu.item.padding,
|
|
56
|
+
paddingHorizontal: theme.components.menu.mobile.item.padding,
|
|
57
|
+
gap: theme.components.menu.item.gap,
|
|
58
|
+
borderRadius: theme.components.menu.item.borderRadius,
|
|
59
|
+
variants: {
|
|
60
|
+
active: {
|
|
61
|
+
true: {
|
|
62
|
+
backgroundColor: theme.color.interactive.functional.surface.subtle.active,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
disabled: {
|
|
66
|
+
true: {
|
|
67
|
+
opacity: theme.opacity.disabled,
|
|
68
|
+
cursor: 'auto',
|
|
69
|
+
},
|
|
70
|
+
false: {
|
|
71
|
+
_web: {
|
|
72
|
+
_hover: {
|
|
73
|
+
backgroundColor: theme.color.interactive.functional.surface.subtle.hover,
|
|
74
|
+
},
|
|
75
|
+
_active: {
|
|
76
|
+
backgroundColor: theme.color.interactive.functional.surface.subtle.active,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
iconPosition: {
|
|
82
|
+
left: {
|
|
83
|
+
flexDirection: 'row',
|
|
84
|
+
},
|
|
85
|
+
right: {
|
|
86
|
+
flexDirection: 'row-reverse',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
colorScheme: {
|
|
90
|
+
functional: {},
|
|
91
|
+
destructive: {},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
compoundVariants: [
|
|
95
|
+
{
|
|
96
|
+
colorScheme: 'destructive',
|
|
97
|
+
active: true,
|
|
98
|
+
styles: {
|
|
99
|
+
backgroundColor: theme.color.interactive.destructive.surface.subtle.active,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
colorScheme: 'destructive',
|
|
104
|
+
disabled: false,
|
|
105
|
+
styles: {
|
|
106
|
+
_web: {
|
|
107
|
+
_hover: {
|
|
108
|
+
backgroundColor: theme.color.interactive.destructive.surface.subtle.hover,
|
|
109
|
+
},
|
|
110
|
+
_active: {
|
|
111
|
+
backgroundColor: theme.color.interactive.destructive.surface.subtle.active,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
text: {
|
|
119
|
+
flex: 1,
|
|
120
|
+
variants: {
|
|
121
|
+
colorScheme: {
|
|
122
|
+
functional: {
|
|
123
|
+
color: theme.color.text.primary,
|
|
124
|
+
},
|
|
125
|
+
destructive: {
|
|
126
|
+
color: theme.color.interactive.destructive.foreground.subtle,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
icon: {
|
|
132
|
+
variants: {
|
|
133
|
+
colorScheme: {
|
|
134
|
+
functional: {
|
|
135
|
+
color: theme.color.icon.primary,
|
|
136
|
+
},
|
|
137
|
+
destructive: {
|
|
138
|
+
color: theme.color.interactive.destructive.foreground.subtle,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
export default MenuItem;
|