@bacon-pos/ui 0.5.1 → 0.6.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/lib/commonjs/components/Dropdown/Dropdown.js +282 -0
- package/lib/commonjs/components/Dropdown/Dropdown.js.map +1 -0
- package/lib/commonjs/components/Dropdown/Dropdown.stories.js +421 -0
- package/lib/commonjs/components/Dropdown/Dropdown.stories.js.map +1 -0
- package/lib/commonjs/components/Dropdown/Dropdown.test.js +113 -0
- package/lib/commonjs/components/Dropdown/Dropdown.test.js.map +1 -0
- package/lib/commonjs/components/Dropdown/index.js +19 -0
- package/lib/commonjs/components/Dropdown/index.js.map +1 -0
- package/lib/commonjs/components/Menu/Menu.js +214 -0
- package/lib/commonjs/components/Menu/Menu.js.map +1 -0
- package/lib/commonjs/components/Menu/Menu.test.js +71 -0
- package/lib/commonjs/components/Menu/Menu.test.js.map +1 -0
- package/lib/commonjs/components/Menu/MenuButton.js +73 -0
- package/lib/commonjs/components/Menu/MenuButton.js.map +1 -0
- package/lib/commonjs/components/Menu/MenuButton.stories.js +166 -0
- package/lib/commonjs/components/Menu/MenuButton.stories.js.map +1 -0
- package/lib/commonjs/components/Menu/SelectButton.js +143 -0
- package/lib/commonjs/components/Menu/SelectButton.js.map +1 -0
- package/lib/commonjs/components/Menu/SelectButton.stories.js +116 -0
- package/lib/commonjs/components/Menu/SelectButton.stories.js.map +1 -0
- package/lib/commonjs/components/Menu/SelectButton.test.js +80 -0
- package/lib/commonjs/components/Menu/SelectButton.test.js.map +1 -0
- package/lib/commonjs/components/Menu/index.js +27 -0
- package/lib/commonjs/components/Menu/index.js.map +1 -0
- package/lib/commonjs/components/Select/Select.js +246 -0
- package/lib/commonjs/components/Select/Select.js.map +1 -0
- package/lib/commonjs/components/Select/Select.stories.js +221 -0
- package/lib/commonjs/components/Select/Select.stories.js.map +1 -0
- package/lib/commonjs/components/Select/Select.test.js +165 -0
- package/lib/commonjs/components/Select/Select.test.js.map +1 -0
- package/lib/commonjs/components/Select/index.js +13 -0
- package/lib/commonjs/components/Select/index.js.map +1 -0
- package/lib/commonjs/components/UserChip/UserChip.js +121 -0
- package/lib/commonjs/components/UserChip/UserChip.js.map +1 -0
- package/lib/commonjs/components/UserChip/UserChip.stories.js +153 -0
- package/lib/commonjs/components/UserChip/UserChip.stories.js.map +1 -0
- package/lib/commonjs/components/UserChip/UserChip.test.js +59 -0
- package/lib/commonjs/components/UserChip/UserChip.test.js.map +1 -0
- package/lib/commonjs/components/UserChip/index.js +13 -0
- package/lib/commonjs/components/UserChip/index.js.map +1 -0
- package/lib/commonjs/components/index.js +57 -13
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/module/components/Dropdown/Dropdown.js +277 -0
- package/lib/module/components/Dropdown/Dropdown.js.map +1 -0
- package/lib/module/components/Dropdown/Dropdown.stories.js +417 -0
- package/lib/module/components/Dropdown/Dropdown.stories.js.map +1 -0
- package/lib/module/components/Dropdown/Dropdown.test.js +113 -0
- package/lib/module/components/Dropdown/Dropdown.test.js.map +1 -0
- package/lib/module/components/Dropdown/index.js +4 -0
- package/lib/module/components/Dropdown/index.js.map +1 -0
- package/lib/module/components/Menu/Menu.js +210 -0
- package/lib/module/components/Menu/Menu.js.map +1 -0
- package/lib/module/components/Menu/Menu.test.js +71 -0
- package/lib/module/components/Menu/Menu.test.js.map +1 -0
- package/lib/module/components/Menu/MenuButton.js +69 -0
- package/lib/module/components/Menu/MenuButton.js.map +1 -0
- package/lib/module/components/Menu/MenuButton.stories.js +162 -0
- package/lib/module/components/Menu/MenuButton.stories.js.map +1 -0
- package/lib/module/components/Menu/SelectButton.js +139 -0
- package/lib/module/components/Menu/SelectButton.js.map +1 -0
- package/lib/module/components/Menu/SelectButton.stories.js +112 -0
- package/lib/module/components/Menu/SelectButton.stories.js.map +1 -0
- package/lib/module/components/Menu/SelectButton.test.js +80 -0
- package/lib/module/components/Menu/SelectButton.test.js.map +1 -0
- package/lib/module/components/Menu/index.js +6 -0
- package/lib/module/components/Menu/index.js.map +1 -0
- package/lib/module/components/Select/Select.js +244 -0
- package/lib/module/components/Select/Select.js.map +1 -0
- package/lib/module/components/Select/Select.stories.js +217 -0
- package/lib/module/components/Select/Select.stories.js.map +1 -0
- package/lib/module/components/Select/Select.test.js +165 -0
- package/lib/module/components/Select/Select.test.js.map +1 -0
- package/lib/module/components/Select/index.js +4 -0
- package/lib/module/components/Select/index.js.map +1 -0
- package/lib/module/components/UserChip/UserChip.js +117 -0
- package/lib/module/components/UserChip/UserChip.js.map +1 -0
- package/lib/module/components/UserChip/UserChip.stories.js +149 -0
- package/lib/module/components/UserChip/UserChip.stories.js.map +1 -0
- package/lib/module/components/UserChip/UserChip.test.js +59 -0
- package/lib/module/components/UserChip/UserChip.test.js.map +1 -0
- package/lib/module/components/UserChip/index.js +4 -0
- package/lib/module/components/UserChip/index.js.map +1 -0
- package/lib/module/components/index.js +4 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/typescript/commonjs/components/Dropdown/Dropdown.d.ts +105 -0
- package/lib/typescript/commonjs/components/Dropdown/Dropdown.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Dropdown/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/Dropdown/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Menu/Menu.d.ts +66 -0
- package/lib/typescript/commonjs/components/Menu/Menu.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Menu/MenuButton.d.ts +46 -0
- package/lib/typescript/commonjs/components/Menu/MenuButton.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Menu/SelectButton.d.ts +60 -0
- package/lib/typescript/commonjs/components/Menu/SelectButton.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Menu/index.d.ts +7 -0
- package/lib/typescript/commonjs/components/Menu/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Select/Select.d.ts +73 -0
- package/lib/typescript/commonjs/components/Select/Select.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Select/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/Select/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/UserChip/UserChip.d.ts +33 -0
- package/lib/typescript/commonjs/components/UserChip/UserChip.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/UserChip/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/UserChip/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/index.d.ts +4 -0
- package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
- package/lib/typescript/module/components/Dropdown/Dropdown.d.ts +105 -0
- package/lib/typescript/module/components/Dropdown/Dropdown.d.ts.map +1 -0
- package/lib/typescript/module/components/Dropdown/index.d.ts +3 -0
- package/lib/typescript/module/components/Dropdown/index.d.ts.map +1 -0
- package/lib/typescript/module/components/Menu/Menu.d.ts +66 -0
- package/lib/typescript/module/components/Menu/Menu.d.ts.map +1 -0
- package/lib/typescript/module/components/Menu/MenuButton.d.ts +46 -0
- package/lib/typescript/module/components/Menu/MenuButton.d.ts.map +1 -0
- package/lib/typescript/module/components/Menu/SelectButton.d.ts +60 -0
- package/lib/typescript/module/components/Menu/SelectButton.d.ts.map +1 -0
- package/lib/typescript/module/components/Menu/index.d.ts +7 -0
- package/lib/typescript/module/components/Menu/index.d.ts.map +1 -0
- package/lib/typescript/module/components/Select/Select.d.ts +73 -0
- package/lib/typescript/module/components/Select/Select.d.ts.map +1 -0
- package/lib/typescript/module/components/Select/index.d.ts +3 -0
- package/lib/typescript/module/components/Select/index.d.ts.map +1 -0
- package/lib/typescript/module/components/UserChip/UserChip.d.ts +33 -0
- package/lib/typescript/module/components/UserChip/UserChip.d.ts.map +1 -0
- package/lib/typescript/module/components/UserChip/index.d.ts +3 -0
- package/lib/typescript/module/components/UserChip/index.d.ts.map +1 -0
- package/lib/typescript/module/components/index.d.ts +4 -0
- package/lib/typescript/module/components/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Dropdown/Dropdown.tsx +319 -0
- package/src/components/Dropdown/index.ts +2 -0
- package/src/components/Menu/Menu.tsx +249 -0
- package/src/components/Menu/MenuButton.tsx +98 -0
- package/src/components/Menu/SelectButton.tsx +171 -0
- package/src/components/Menu/index.ts +12 -0
- package/src/components/Select/Select.tsx +323 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/UserChip/UserChip.tsx +138 -0
- package/src/components/UserChip/index.ts +2 -0
- package/src/components/index.ts +4 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Animated,
|
|
4
|
+
Dimensions,
|
|
5
|
+
Easing,
|
|
6
|
+
Modal,
|
|
7
|
+
Platform,
|
|
8
|
+
Pressable,
|
|
9
|
+
View,
|
|
10
|
+
type LayoutChangeEvent,
|
|
11
|
+
type StyleProp,
|
|
12
|
+
type ViewStyle,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
|
|
15
|
+
export type OriginVertical = 'top' | 'center' | 'bottom';
|
|
16
|
+
export type OriginHorizontal = 'left' | 'center' | 'right';
|
|
17
|
+
|
|
18
|
+
/** A point on a box: a vertical and a horizontal edge/center. */
|
|
19
|
+
export interface MenuOrigin {
|
|
20
|
+
vertical: OriginVertical;
|
|
21
|
+
horizontal: OriginHorizontal;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const V_FRAC: Record<OriginVertical, number> = { top: 0, center: 0.5, bottom: 1 };
|
|
25
|
+
const H_FRAC: Record<OriginHorizontal, number> = { left: 0, center: 0.5, right: 1 };
|
|
26
|
+
|
|
27
|
+
/** State handed to the trigger renderer and (optionally) the content renderer. */
|
|
28
|
+
export interface MenuTriggerState {
|
|
29
|
+
open: boolean;
|
|
30
|
+
toggle: () => void;
|
|
31
|
+
close: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MenuProps {
|
|
35
|
+
/** Renders the trigger. Wire its press to `toggle`. */
|
|
36
|
+
renderTrigger: (state: MenuTriggerState) => ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* The overlay content (e.g. a `Dropdown`). May be a function receiving
|
|
39
|
+
* `{ close, triggerWidth, maxHeight }` — `maxHeight` is the auto-fit height
|
|
40
|
+
* (space available in the chosen direction) so the content can scroll.
|
|
41
|
+
*/
|
|
42
|
+
children:
|
|
43
|
+
| ReactNode
|
|
44
|
+
| ((state: { close: () => void; triggerWidth?: number; maxHeight?: number }) => ReactNode);
|
|
45
|
+
/** Point on the trigger the menu latches to. Defaults to `{ bottom, left }`. */
|
|
46
|
+
anchorOrigin?: MenuOrigin;
|
|
47
|
+
/** Point on the menu placed at the anchor. Defaults to `{ top, left }`. */
|
|
48
|
+
menuOrigin?: MenuOrigin;
|
|
49
|
+
/** Gap between trigger and menu along the vertical axis. Defaults to 8. */
|
|
50
|
+
offset?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Fit the menu to the space available and flip its direction when the
|
|
53
|
+
* preferred side doesn't have room. Provides a `maxHeight` to the content so it
|
|
54
|
+
* scrolls instead of overflowing the screen. Defaults to true.
|
|
55
|
+
*/
|
|
56
|
+
autoFit?: boolean;
|
|
57
|
+
/** Safe-area insets to keep clear (pass from `react-native-safe-area-context` if you use it). */
|
|
58
|
+
insets?: EdgeInsets;
|
|
59
|
+
/** Controlled open state. Omit for uncontrolled. */
|
|
60
|
+
open?: boolean;
|
|
61
|
+
/** Notified whenever the open state changes. */
|
|
62
|
+
onOpenChange?: (open: boolean) => void;
|
|
63
|
+
/** Padding kept from the screen edges when clamping. Defaults to 8. */
|
|
64
|
+
screenPadding?: number;
|
|
65
|
+
/** Style for the (measuring) wrapper around the trigger. Defaults to hugging the trigger. */
|
|
66
|
+
triggerStyle?: StyleProp<ViewStyle>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface EdgeInsets {
|
|
70
|
+
top: number;
|
|
71
|
+
right: number;
|
|
72
|
+
bottom: number;
|
|
73
|
+
left: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ZERO_INSETS: EdgeInsets = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
77
|
+
|
|
78
|
+
interface Rect {
|
|
79
|
+
x: number;
|
|
80
|
+
y: number;
|
|
81
|
+
width: number;
|
|
82
|
+
height: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function clamp(value: number, min: number, max: number): number {
|
|
86
|
+
if (max < min) return min;
|
|
87
|
+
return Math.min(Math.max(value, min), max);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Anchored overlay menu. Renders a trigger inline and, when open, a floating
|
|
92
|
+
* panel positioned relative to the trigger via two origins — `anchorOrigin`
|
|
93
|
+
* (point on the trigger) and `menuOrigin` (matching point on the menu). Tapping
|
|
94
|
+
* outside (or the hardware back button) dismisses it. Used to compose menu
|
|
95
|
+
* buttons, select fields, popovers, etc.
|
|
96
|
+
*/
|
|
97
|
+
export function Menu({
|
|
98
|
+
renderTrigger,
|
|
99
|
+
children,
|
|
100
|
+
anchorOrigin = { vertical: 'bottom', horizontal: 'left' },
|
|
101
|
+
menuOrigin = { vertical: 'top', horizontal: 'left' },
|
|
102
|
+
offset = 8,
|
|
103
|
+
autoFit = true,
|
|
104
|
+
insets = ZERO_INSETS,
|
|
105
|
+
open: openProp,
|
|
106
|
+
onOpenChange,
|
|
107
|
+
screenPadding = 8,
|
|
108
|
+
triggerStyle,
|
|
109
|
+
}: MenuProps) {
|
|
110
|
+
const triggerRef = useRef<View>(null);
|
|
111
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
|
112
|
+
const isControlled = openProp !== undefined;
|
|
113
|
+
const open = isControlled ? openProp : uncontrolledOpen;
|
|
114
|
+
|
|
115
|
+
const [rect, setRect] = useState<Rect | null>(null);
|
|
116
|
+
const [menuSize, setMenuSize] = useState<{ width: number; height: number } | null>(null);
|
|
117
|
+
// The content's natural (uncapped) height, captured once per open — used to
|
|
118
|
+
// decide whether it fits the preferred side (kept stable so the flip decision
|
|
119
|
+
// doesn't oscillate once a maxHeight cap is applied).
|
|
120
|
+
const [naturalHeight, setNaturalHeight] = useState<number | null>(null);
|
|
121
|
+
const anim = useRef(new Animated.Value(0)).current;
|
|
122
|
+
|
|
123
|
+
const setOpen = (next: boolean) => {
|
|
124
|
+
if (!isControlled) setUncontrolledOpen(next);
|
|
125
|
+
onOpenChange?.(next);
|
|
126
|
+
};
|
|
127
|
+
const close = () => setOpen(false);
|
|
128
|
+
const toggle = () => setOpen(!open);
|
|
129
|
+
|
|
130
|
+
// Measure the trigger whenever the menu opens.
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!open) {
|
|
133
|
+
setMenuSize(null);
|
|
134
|
+
setNaturalHeight(null);
|
|
135
|
+
anim.setValue(0);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
triggerRef.current?.measureInWindow((x, y, width, height) => {
|
|
139
|
+
setRect({ x, y, width, height });
|
|
140
|
+
});
|
|
141
|
+
}, [open, anim]);
|
|
142
|
+
|
|
143
|
+
// Fade + scale in once the menu has been measured (so it never flashes unplaced).
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (open && rect && menuSize) {
|
|
146
|
+
Animated.timing(anim, {
|
|
147
|
+
toValue: 1,
|
|
148
|
+
duration: 120,
|
|
149
|
+
easing: Easing.out(Easing.ease),
|
|
150
|
+
useNativeDriver: Platform.OS !== 'web',
|
|
151
|
+
}).start();
|
|
152
|
+
}
|
|
153
|
+
}, [open, rect, menuSize, anim]);
|
|
154
|
+
|
|
155
|
+
const onMenuLayout = (e: LayoutChangeEvent) => {
|
|
156
|
+
const { width, height } = e.nativeEvent.layout;
|
|
157
|
+
if (!menuSize || menuSize.width !== width || menuSize.height !== height) {
|
|
158
|
+
setMenuSize({ width, height });
|
|
159
|
+
}
|
|
160
|
+
// Capture the natural height once — before any maxHeight cap is applied.
|
|
161
|
+
if (naturalHeight === null) setNaturalHeight(height);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Auto-fit: choose a vertical direction with room and cap the height so the
|
|
165
|
+
// menu never runs off-screen. Only meaningful for a "drop" origin config
|
|
166
|
+
// (top↔bottom); otherwise we keep the origins as given.
|
|
167
|
+
const isDropConfig =
|
|
168
|
+
(anchorOrigin.vertical === 'bottom' && menuOrigin.vertical === 'top') ||
|
|
169
|
+
(anchorOrigin.vertical === 'top' && menuOrigin.vertical === 'bottom');
|
|
170
|
+
const preferredUp = anchorOrigin.vertical === 'top' && menuOrigin.vertical === 'bottom';
|
|
171
|
+
|
|
172
|
+
let effAnchor = anchorOrigin;
|
|
173
|
+
let effMenu = menuOrigin;
|
|
174
|
+
let autoMaxHeight: number | undefined;
|
|
175
|
+
|
|
176
|
+
if (autoFit && isDropConfig && rect && naturalHeight !== null) {
|
|
177
|
+
const screen = Dimensions.get('window');
|
|
178
|
+
const spaceBelow = screen.height - insets.bottom - (rect.y + rect.height) - offset - screenPadding;
|
|
179
|
+
const spaceAbove = rect.y - insets.top - offset - screenPadding;
|
|
180
|
+
let up = preferredUp;
|
|
181
|
+
if (!preferredUp) up = naturalHeight > spaceBelow && spaceAbove > spaceBelow;
|
|
182
|
+
else up = !(naturalHeight > spaceAbove && spaceBelow > spaceAbove);
|
|
183
|
+
const chosen = up ? spaceAbove : spaceBelow;
|
|
184
|
+
if (naturalHeight > chosen && chosen > 0) autoMaxHeight = chosen;
|
|
185
|
+
effAnchor = { horizontal: anchorOrigin.horizontal, vertical: up ? 'top' : 'bottom' };
|
|
186
|
+
effMenu = { horizontal: menuOrigin.horizontal, vertical: up ? 'bottom' : 'top' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let top = 0;
|
|
190
|
+
let left = 0;
|
|
191
|
+
if (rect && menuSize) {
|
|
192
|
+
const screen = Dimensions.get('window');
|
|
193
|
+
const anchorX = rect.x + H_FRAC[effAnchor.horizontal] * rect.width;
|
|
194
|
+
const anchorY = rect.y + V_FRAC[effAnchor.vertical] * rect.height;
|
|
195
|
+
const offsetY =
|
|
196
|
+
effMenu.vertical === 'top' ? offset : effMenu.vertical === 'bottom' ? -offset : 0;
|
|
197
|
+
left = anchorX - H_FRAC[effMenu.horizontal] * menuSize.width;
|
|
198
|
+
top = anchorY - V_FRAC[effMenu.vertical] * menuSize.height + offsetY;
|
|
199
|
+
left = clamp(left, screenPadding + insets.left, screen.width - insets.right - menuSize.width - screenPadding);
|
|
200
|
+
top = clamp(top, screenPadding + insets.top, screen.height - insets.bottom - menuSize.height - screenPadding);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const placed = Boolean(rect && menuSize);
|
|
204
|
+
const content =
|
|
205
|
+
typeof children === 'function'
|
|
206
|
+
? children({ close, triggerWidth: rect?.width, maxHeight: autoMaxHeight })
|
|
207
|
+
: children;
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<>
|
|
211
|
+
<View
|
|
212
|
+
ref={triggerRef}
|
|
213
|
+
collapsable={false}
|
|
214
|
+
style={[{ alignSelf: 'flex-start' }, triggerStyle]}
|
|
215
|
+
>
|
|
216
|
+
{renderTrigger({ open, toggle, close })}
|
|
217
|
+
</View>
|
|
218
|
+
|
|
219
|
+
<Modal
|
|
220
|
+
visible={open}
|
|
221
|
+
transparent
|
|
222
|
+
animationType="none"
|
|
223
|
+
onRequestClose={close}
|
|
224
|
+
statusBarTranslucent
|
|
225
|
+
>
|
|
226
|
+
<Pressable
|
|
227
|
+
accessibilityLabel="Close menu"
|
|
228
|
+
onPress={close}
|
|
229
|
+
style={{ flex: 1 }}
|
|
230
|
+
>
|
|
231
|
+
<Animated.View
|
|
232
|
+
onLayout={onMenuLayout}
|
|
233
|
+
// Stop presses inside the menu from bubbling to the backdrop.
|
|
234
|
+
onStartShouldSetResponder={() => true}
|
|
235
|
+
style={{
|
|
236
|
+
position: 'absolute',
|
|
237
|
+
top,
|
|
238
|
+
left,
|
|
239
|
+
opacity: placed ? anim : 0,
|
|
240
|
+
transform: [{ scale: placed ? anim.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) : 1 }],
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
{content}
|
|
244
|
+
</Animated.View>
|
|
245
|
+
</Pressable>
|
|
246
|
+
</Modal>
|
|
247
|
+
</>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Dropdown, type DropdownEntry } from '../Dropdown/Dropdown';
|
|
2
|
+
import { IconButton, type IconButtonShape, type IconButtonSize, type IconButtonVariant } from '../IconButton/IconButton';
|
|
3
|
+
import type { IconName } from '../Icon/icons.generated';
|
|
4
|
+
import { Menu, type MenuOrigin } from './Menu';
|
|
5
|
+
|
|
6
|
+
export interface MenuButtonProps {
|
|
7
|
+
/** The menu rows (see `Dropdown`). */
|
|
8
|
+
options: DropdownEntry[];
|
|
9
|
+
/** Called with the key of the selected row. */
|
|
10
|
+
onSelect?: (key: string) => void;
|
|
11
|
+
/** Trigger icon. Defaults to `more` (the "More" button). */
|
|
12
|
+
icon?: IconName;
|
|
13
|
+
/** Accessibility label for the trigger. Defaults to "More options". */
|
|
14
|
+
accessibilityLabel?: string;
|
|
15
|
+
/** Trigger visual. Defaults to `secondary` (white + hairline border). */
|
|
16
|
+
variant?: IconButtonVariant;
|
|
17
|
+
/** Trigger size. Defaults to `md`. */
|
|
18
|
+
size?: IconButtonSize;
|
|
19
|
+
/** Trigger shape. Defaults to `round`. */
|
|
20
|
+
shape?: IconButtonShape;
|
|
21
|
+
/** Disable the trigger. */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** Point on the trigger the menu latches to. Defaults to `{ bottom, right }`. */
|
|
24
|
+
anchorOrigin?: MenuOrigin;
|
|
25
|
+
/** Point on the menu placed at the anchor. Defaults to `{ top, right }`. */
|
|
26
|
+
menuOrigin?: MenuOrigin;
|
|
27
|
+
/** Gap between trigger and menu. Defaults to 8. */
|
|
28
|
+
offset?: number;
|
|
29
|
+
/** Menu width. Defaults to the `Dropdown` default (350). */
|
|
30
|
+
menuWidth?: number;
|
|
31
|
+
/** Draw dividers between rows. Defaults to the `Dropdown` default (true). */
|
|
32
|
+
showDividers?: boolean;
|
|
33
|
+
/** Close the menu after a row is selected. Defaults to true. */
|
|
34
|
+
closeOnSelect?: boolean;
|
|
35
|
+
/** Controlled open state. */
|
|
36
|
+
open?: boolean;
|
|
37
|
+
/** Notified whenever the open state changes. */
|
|
38
|
+
onOpenChange?: (open: boolean) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A "More"-style menu button — an `IconButton` that opens an anchored `Dropdown`.
|
|
43
|
+
* The first composed component built from `IconButton` + `Icon` + `Dropdown` +
|
|
44
|
+
* `Menu`. Placement is controlled by the two-origin model (`anchorOrigin` on the
|
|
45
|
+
* trigger, `menuOrigin` on the menu). Dismisses on outside press / back button.
|
|
46
|
+
*/
|
|
47
|
+
export function MenuButton({
|
|
48
|
+
options,
|
|
49
|
+
onSelect,
|
|
50
|
+
icon = 'more',
|
|
51
|
+
accessibilityLabel = 'More options',
|
|
52
|
+
variant = 'secondary',
|
|
53
|
+
size = 'md',
|
|
54
|
+
shape = 'round',
|
|
55
|
+
disabled = false,
|
|
56
|
+
anchorOrigin = { vertical: 'bottom', horizontal: 'right' },
|
|
57
|
+
menuOrigin = { vertical: 'top', horizontal: 'right' },
|
|
58
|
+
offset = 8,
|
|
59
|
+
menuWidth,
|
|
60
|
+
showDividers,
|
|
61
|
+
closeOnSelect = true,
|
|
62
|
+
open,
|
|
63
|
+
onOpenChange,
|
|
64
|
+
}: MenuButtonProps) {
|
|
65
|
+
return (
|
|
66
|
+
<Menu
|
|
67
|
+
anchorOrigin={anchorOrigin}
|
|
68
|
+
menuOrigin={menuOrigin}
|
|
69
|
+
offset={offset}
|
|
70
|
+
open={open}
|
|
71
|
+
onOpenChange={onOpenChange}
|
|
72
|
+
renderTrigger={({ toggle }) => (
|
|
73
|
+
<IconButton
|
|
74
|
+
icon={icon}
|
|
75
|
+
accessibilityLabel={accessibilityLabel}
|
|
76
|
+
variant={variant}
|
|
77
|
+
size={size}
|
|
78
|
+
shape={shape}
|
|
79
|
+
disabled={disabled}
|
|
80
|
+
onPress={toggle}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{({ close, maxHeight }) => (
|
|
85
|
+
<Dropdown
|
|
86
|
+
options={options}
|
|
87
|
+
width={menuWidth}
|
|
88
|
+
maxHeight={maxHeight}
|
|
89
|
+
showDividers={showDividers}
|
|
90
|
+
onSelect={(key) => {
|
|
91
|
+
onSelect?.(key);
|
|
92
|
+
if (closeOnSelect) close();
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
</Menu>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useTheme } from '../../theme';
|
|
3
|
+
import { Button, type ButtonSize, type ButtonVariant } from '../Button/Button';
|
|
4
|
+
import { Dropdown, type DropdownEntry } from '../Dropdown/Dropdown';
|
|
5
|
+
import { Icon } from '../Icon/Icon';
|
|
6
|
+
import type { IconName } from '../Icon/icons.generated';
|
|
7
|
+
import { Menu, type MenuOrigin } from './Menu';
|
|
8
|
+
|
|
9
|
+
/** A selectable option. Its `icon` shows both in the row and on the trigger when chosen. */
|
|
10
|
+
export interface SelectButtonOption {
|
|
11
|
+
key: string;
|
|
12
|
+
label: string;
|
|
13
|
+
icon?: IconName;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SelectButtonProps {
|
|
18
|
+
/** The options to choose from. */
|
|
19
|
+
options: SelectButtonOption[];
|
|
20
|
+
/** Selected key (controlled). */
|
|
21
|
+
value?: string;
|
|
22
|
+
/** Selected key for the uncontrolled case. */
|
|
23
|
+
defaultValue?: string;
|
|
24
|
+
/** Called with the newly selected key. */
|
|
25
|
+
onChange?: (key: string) => void;
|
|
26
|
+
/** Trigger label when nothing is selected. */
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
/** Trigger leading icon when nothing is selected. */
|
|
29
|
+
placeholderIcon?: IconName;
|
|
30
|
+
/** Show the selected option's icon on the trigger. Defaults to true. */
|
|
31
|
+
showSelectedIcon?: boolean;
|
|
32
|
+
/** Show a trailing chevron on the trigger. Defaults to false (matches the mockup). */
|
|
33
|
+
chevron?: boolean;
|
|
34
|
+
/** Trigger visual. Defaults to `secondary`. */
|
|
35
|
+
variant?: ButtonVariant;
|
|
36
|
+
/** Trigger size. Defaults to `sm`. */
|
|
37
|
+
size?: ButtonSize;
|
|
38
|
+
/** Stretch the trigger to the parent width. */
|
|
39
|
+
fullWidth?: boolean;
|
|
40
|
+
/** Disable the trigger. */
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/** Point on the trigger the menu latches to. Defaults to `{ bottom, left }`. */
|
|
43
|
+
anchorOrigin?: MenuOrigin;
|
|
44
|
+
/** Point on the menu placed at the anchor. Defaults to `{ top, left }`. */
|
|
45
|
+
menuOrigin?: MenuOrigin;
|
|
46
|
+
/** Gap between trigger and menu. Defaults to 8. */
|
|
47
|
+
offset?: number;
|
|
48
|
+
/** Menu width. Defaults to the `Dropdown` default (350). */
|
|
49
|
+
menuWidth?: number;
|
|
50
|
+
/** Draw dividers between rows. Defaults to the `Dropdown` default (true). */
|
|
51
|
+
showDividers?: boolean;
|
|
52
|
+
/** Close the menu after selecting. Defaults to true. */
|
|
53
|
+
closeOnSelect?: boolean;
|
|
54
|
+
/** Controlled open state. */
|
|
55
|
+
open?: boolean;
|
|
56
|
+
/** Notified whenever the open state changes. */
|
|
57
|
+
onOpenChange?: (open: boolean) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Button foreground per variant — mirrors Button so the trigger icon matches the label. */
|
|
61
|
+
function triggerForeground(theme: ReturnType<typeof useTheme>, variant: ButtonVariant): string {
|
|
62
|
+
const { action, state } = theme.colors;
|
|
63
|
+
switch (variant) {
|
|
64
|
+
case 'secondary':
|
|
65
|
+
return action.secondaryForeground;
|
|
66
|
+
case 'inverse':
|
|
67
|
+
return action.inverseForeground;
|
|
68
|
+
case 'destructive':
|
|
69
|
+
return state.destructiveSubtleForeground;
|
|
70
|
+
case 'success':
|
|
71
|
+
return state.successForeground;
|
|
72
|
+
case 'successSubtle':
|
|
73
|
+
return state.successSubtleForeground;
|
|
74
|
+
default:
|
|
75
|
+
return action.primaryForeground;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Select-style button — a `Button` (label + leading icon) that opens an anchored
|
|
81
|
+
* `Dropdown` to change its value; the current option is marked with a check.
|
|
82
|
+
* A composed component built from `Button` + `Menu` + `Dropdown`. Controlled via
|
|
83
|
+
* `value` / `onChange`, or uncontrolled via `defaultValue`.
|
|
84
|
+
*/
|
|
85
|
+
export function SelectButton({
|
|
86
|
+
options,
|
|
87
|
+
value: valueProp,
|
|
88
|
+
defaultValue,
|
|
89
|
+
onChange,
|
|
90
|
+
placeholder = 'Select',
|
|
91
|
+
placeholderIcon,
|
|
92
|
+
showSelectedIcon = true,
|
|
93
|
+
chevron = false,
|
|
94
|
+
variant = 'secondary',
|
|
95
|
+
size = 'sm',
|
|
96
|
+
fullWidth = false,
|
|
97
|
+
disabled = false,
|
|
98
|
+
anchorOrigin = { vertical: 'bottom', horizontal: 'left' },
|
|
99
|
+
menuOrigin = { vertical: 'top', horizontal: 'left' },
|
|
100
|
+
offset = 8,
|
|
101
|
+
menuWidth,
|
|
102
|
+
showDividers,
|
|
103
|
+
closeOnSelect = true,
|
|
104
|
+
open,
|
|
105
|
+
onOpenChange,
|
|
106
|
+
}: SelectButtonProps) {
|
|
107
|
+
const theme = useTheme();
|
|
108
|
+
const isControlled = valueProp !== undefined;
|
|
109
|
+
const [uncontrolled, setUncontrolled] = useState(defaultValue);
|
|
110
|
+
const value = isControlled ? valueProp : uncontrolled;
|
|
111
|
+
|
|
112
|
+
const selected = options.find((o) => o.key === value);
|
|
113
|
+
const label = selected?.label ?? placeholder;
|
|
114
|
+
const triggerIcon = selected?.icon ?? placeholderIcon;
|
|
115
|
+
const fg = triggerForeground(theme, variant);
|
|
116
|
+
const iconSize = size === 'sm' ? 20 : 24;
|
|
117
|
+
|
|
118
|
+
const setValue = (key: string) => {
|
|
119
|
+
if (!isControlled) setUncontrolled(key);
|
|
120
|
+
onChange?.(key);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const rows: DropdownEntry[] = options.map((o) => ({
|
|
124
|
+
key: o.key,
|
|
125
|
+
label: o.label,
|
|
126
|
+
disabled: o.disabled,
|
|
127
|
+
leading: o.icon ? { kind: 'icon', name: o.icon } : undefined,
|
|
128
|
+
trailing:
|
|
129
|
+
o.key === value ? { kind: 'icon', name: 'check', color: theme.colors.feedback.success } : undefined,
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Menu
|
|
134
|
+
anchorOrigin={anchorOrigin}
|
|
135
|
+
menuOrigin={menuOrigin}
|
|
136
|
+
offset={offset}
|
|
137
|
+
open={open}
|
|
138
|
+
onOpenChange={onOpenChange}
|
|
139
|
+
triggerStyle={fullWidth ? { alignSelf: 'stretch' } : undefined}
|
|
140
|
+
renderTrigger={({ toggle }) => (
|
|
141
|
+
<Button
|
|
142
|
+
label={label}
|
|
143
|
+
variant={variant}
|
|
144
|
+
size={size}
|
|
145
|
+
fullWidth={fullWidth}
|
|
146
|
+
disabled={disabled}
|
|
147
|
+
onPress={toggle}
|
|
148
|
+
leftIcon={
|
|
149
|
+
showSelectedIcon && triggerIcon ? (
|
|
150
|
+
<Icon name={triggerIcon} size={iconSize} color={fg} />
|
|
151
|
+
) : undefined
|
|
152
|
+
}
|
|
153
|
+
rightIcon={chevron ? <Icon name="chevron-down" size={iconSize} color={fg} /> : undefined}
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
{({ close, maxHeight }) => (
|
|
158
|
+
<Dropdown
|
|
159
|
+
options={rows}
|
|
160
|
+
width={menuWidth}
|
|
161
|
+
maxHeight={maxHeight}
|
|
162
|
+
showDividers={showDividers}
|
|
163
|
+
onSelect={(key) => {
|
|
164
|
+
setValue(key);
|
|
165
|
+
if (closeOnSelect) close();
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
</Menu>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { Menu } from './Menu';
|
|
2
|
+
export type {
|
|
3
|
+
MenuProps,
|
|
4
|
+
MenuOrigin,
|
|
5
|
+
MenuTriggerState,
|
|
6
|
+
OriginVertical,
|
|
7
|
+
OriginHorizontal,
|
|
8
|
+
} from './Menu';
|
|
9
|
+
export { MenuButton } from './MenuButton';
|
|
10
|
+
export type { MenuButtonProps } from './MenuButton';
|
|
11
|
+
export { SelectButton } from './SelectButton';
|
|
12
|
+
export type { SelectButtonProps, SelectButtonOption } from './SelectButton';
|