@oxyhq/bloom 0.72.2 → 0.73.1
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/avatar-group/AvatarGroup.web.js +22 -21
- package/lib/commonjs/avatar-group/AvatarGroup.web.js.map +1 -1
- package/lib/commonjs/bottom-sheet/index.web.js +11 -6
- package/lib/commonjs/bottom-sheet/index.web.js.map +1 -1
- package/lib/commonjs/button/Button.js +104 -42
- package/lib/commonjs/button/Button.js.map +1 -1
- package/lib/commonjs/button/Button.web.js +11 -3
- package/lib/commonjs/button/Button.web.js.map +1 -1
- package/lib/commonjs/context-menu/index.web.js +20 -22
- package/lib/commonjs/context-menu/index.web.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js +66 -70
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +49 -51
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/menu/index.web.js +15 -11
- package/lib/commonjs/menu/index.web.js.map +1 -1
- package/lib/commonjs/overlay/index.js +133 -7
- package/lib/commonjs/overlay/index.js.map +1 -1
- package/lib/commonjs/overlay/stack.js +170 -0
- package/lib/commonjs/overlay/stack.js.map +1 -0
- package/lib/commonjs/overlay/use-overlay-layer.js +37 -0
- package/lib/commonjs/overlay/use-overlay-layer.js.map +1 -0
- package/lib/commonjs/popover/index.web.js +31 -34
- package/lib/commonjs/popover/index.web.js.map +1 -1
- package/lib/commonjs/prompt-input/PromptInput.js +33 -29
- package/lib/commonjs/prompt-input/PromptInput.js.map +1 -1
- package/lib/commonjs/select/index.web.js +20 -22
- package/lib/commonjs/select/index.web.js.map +1 -1
- package/lib/commonjs/styles/z-index.js +33 -34
- package/lib/commonjs/styles/z-index.js.map +1 -1
- package/lib/commonjs/surfaces/SurfaceHost.js +3 -6
- package/lib/commonjs/surfaces/SurfaceHost.js.map +1 -1
- package/lib/commonjs/toast/ToastHost.js +9 -5
- package/lib/commonjs/toast/ToastHost.js.map +1 -1
- package/lib/commonjs/tooltip/index.js +9 -6
- package/lib/commonjs/tooltip/index.js.map +1 -1
- package/lib/commonjs/tooltip/index.web.js +8 -2
- package/lib/commonjs/tooltip/index.web.js.map +1 -1
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +5 -5
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/module/avatar-group/AvatarGroup.web.js +21 -20
- package/lib/module/avatar-group/AvatarGroup.web.js.map +1 -1
- package/lib/module/bottom-sheet/index.web.js +11 -6
- package/lib/module/bottom-sheet/index.web.js.map +1 -1
- package/lib/module/button/Button.js +104 -42
- package/lib/module/button/Button.js.map +1 -1
- package/lib/module/button/Button.web.js +11 -3
- package/lib/module/button/Button.web.js.map +1 -1
- package/lib/module/context-menu/index.web.js +21 -23
- package/lib/module/context-menu/index.web.js.map +1 -1
- package/lib/module/dialog/Dialog.js +67 -71
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +50 -52
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/menu/index.web.js +16 -12
- package/lib/module/menu/index.web.js.map +1 -1
- package/lib/module/overlay/index.js +99 -8
- package/lib/module/overlay/index.js.map +1 -1
- package/lib/module/overlay/stack.js +161 -0
- package/lib/module/overlay/stack.js.map +1 -0
- package/lib/module/overlay/use-overlay-layer.js +33 -0
- package/lib/module/overlay/use-overlay-layer.js.map +1 -0
- package/lib/module/popover/index.web.js +32 -34
- package/lib/module/popover/index.web.js.map +1 -1
- package/lib/module/prompt-input/PromptInput.js +33 -29
- package/lib/module/prompt-input/PromptInput.js.map +1 -1
- package/lib/module/select/index.web.js +21 -23
- package/lib/module/select/index.web.js.map +1 -1
- package/lib/module/styles/z-index.js +32 -30
- package/lib/module/styles/z-index.js.map +1 -1
- package/lib/module/surfaces/SurfaceHost.js +3 -6
- package/lib/module/surfaces/SurfaceHost.js.map +1 -1
- package/lib/module/toast/ToastHost.js +10 -6
- package/lib/module/toast/ToastHost.js.map +1 -1
- package/lib/module/tooltip/index.js +9 -6
- package/lib/module/tooltip/index.js.map +1 -1
- package/lib/module/tooltip/index.web.js +8 -2
- package/lib/module/tooltip/index.web.js.map +1 -1
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +5 -5
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/typescript/commonjs/avatar-group/AvatarGroup.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/Button.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/Button.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/types.d.ts +8 -0
- package/lib/typescript/commonjs/button/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/context-menu/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/types.d.ts +0 -10
- package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/menu/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/index.d.ts +31 -6
- package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/stack.d.ts +122 -0
- package/lib/typescript/commonjs/overlay/stack.d.ts.map +1 -0
- package/lib/typescript/commonjs/overlay/use-overlay-layer.d.ts +7 -0
- package/lib/typescript/commonjs/overlay/use-overlay-layer.d.ts.map +1 -0
- package/lib/typescript/commonjs/popover/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/prompt-input/PromptInput.d.ts.map +1 -1
- package/lib/typescript/commonjs/select/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/styles/z-index.d.ts +42 -29
- package/lib/typescript/commonjs/styles/z-index.d.ts.map +1 -1
- package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/ToastHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/tooltip/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/lib/typescript/module/avatar-group/AvatarGroup.web.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/index.web.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.web.d.ts.map +1 -1
- package/lib/typescript/module/button/types.d.ts +8 -0
- package/lib/typescript/module/button/types.d.ts.map +1 -1
- package/lib/typescript/module/context-menu/index.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/types.d.ts +0 -10
- package/lib/typescript/module/dialog/types.d.ts.map +1 -1
- package/lib/typescript/module/menu/index.web.d.ts.map +1 -1
- package/lib/typescript/module/overlay/index.d.ts +31 -6
- package/lib/typescript/module/overlay/index.d.ts.map +1 -1
- package/lib/typescript/module/overlay/stack.d.ts +122 -0
- package/lib/typescript/module/overlay/stack.d.ts.map +1 -0
- package/lib/typescript/module/overlay/use-overlay-layer.d.ts +7 -0
- package/lib/typescript/module/overlay/use-overlay-layer.d.ts.map +1 -0
- package/lib/typescript/module/popover/index.web.d.ts.map +1 -1
- package/lib/typescript/module/prompt-input/PromptInput.d.ts.map +1 -1
- package/lib/typescript/module/select/index.web.d.ts.map +1 -1
- package/lib/typescript/module/styles/z-index.d.ts +42 -29
- package/lib/typescript/module/styles/z-index.d.ts.map +1 -1
- package/lib/typescript/module/surfaces/SurfaceHost.d.ts.map +1 -1
- package/lib/typescript/module/toast/ToastHost.d.ts.map +1 -1
- package/lib/typescript/module/tooltip/index.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/Button.test.tsx +166 -1
- package/src/__tests__/Button.web.test.tsx +16 -0
- package/src/__tests__/ToastHostWebFork.test.ts +11 -3
- package/src/__tests__/overlay-stack-order.test.tsx +270 -0
- package/src/avatar-group/AvatarGroup.web.tsx +35 -31
- package/src/bottom-sheet/index.web.tsx +10 -5
- package/src/button/Button.tsx +123 -40
- package/src/button/Button.web.tsx +11 -3
- package/src/button/types.ts +8 -0
- package/src/context-menu/index.web.tsx +33 -30
- package/src/dialog/Dialog.tsx +9 -18
- package/src/dialog/Dialog.web.tsx +62 -60
- package/src/dialog/types.ts +0 -10
- package/src/menu/index.web.tsx +33 -22
- package/src/overlay/OverlayStacking.stories.tsx +207 -0
- package/src/overlay/index.tsx +114 -9
- package/src/overlay/stack.ts +166 -0
- package/src/overlay/use-overlay-layer.ts +39 -0
- package/src/popover/index.web.tsx +47 -44
- package/src/prompt-input/PromptInput.tsx +36 -29
- package/src/select/index.web.tsx +35 -32
- package/src/styles/z-index.ts +32 -34
- package/src/surfaces/SurfaceHost.tsx +3 -10
- package/src/toast/ToastHost.tsx +9 -6
- package/src/tooltip/index.tsx +14 -7
- package/src/tooltip/index.web.tsx +8 -2
- package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +4 -4
package/src/button/Button.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useMemo, memo } from 'react';
|
|
1
|
+
import React, { useMemo, memo, type ComponentType } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
ActivityIndicator,
|
|
4
4
|
Animated,
|
|
@@ -7,9 +7,12 @@ import {
|
|
|
7
7
|
StyleSheet,
|
|
8
8
|
Text,
|
|
9
9
|
View,
|
|
10
|
+
type PressableProps,
|
|
11
|
+
type StyleProp,
|
|
10
12
|
type TextStyle,
|
|
11
13
|
type ViewStyle,
|
|
12
14
|
} from 'react-native';
|
|
15
|
+
import { styled } from 'react-native-css';
|
|
13
16
|
|
|
14
17
|
import { useTheme } from '../theme/use-theme';
|
|
15
18
|
import { usePressAnimation } from '../hooks/usePressAnimation';
|
|
@@ -69,6 +72,73 @@ const PILL_RADIUS = 999;
|
|
|
69
72
|
const PRESS_SCALE = 0.97;
|
|
70
73
|
const SCALE_VARIANTS = new Set<string>(['primary', 'secondary', 'inverse']);
|
|
71
74
|
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// The button renders ONE node — the same shape the web fork renders (one
|
|
77
|
+
// `<button>`).
|
|
78
|
+
//
|
|
79
|
+
// It used to be two: an unstyled `Animated.View` holding the press-scale
|
|
80
|
+
// transform, wrapping the `Pressable` that carried `className`, `style` and
|
|
81
|
+
// every visual. That wrapper — not the pressable — was the flex child of
|
|
82
|
+
// whatever laid the button out, and it hugged its content, so a caller's
|
|
83
|
+
// `className="flex-1"` (and `style={{ alignSelf }}`, and every other layout
|
|
84
|
+
// declaration) applied INSIDE a box that never grew. Nothing errored; the same
|
|
85
|
+
// call site behaved on web and did nothing on native, and consumers worked
|
|
86
|
+
// around it by wrapping every Bloom `Button` in their own `<View>`.
|
|
87
|
+
//
|
|
88
|
+
// Collapsing the two nodes settles "which node does a class belong on?" by
|
|
89
|
+
// removing the choice: layout and visuals are one box here exactly as they are
|
|
90
|
+
// in the DOM, so a layout class and a visual class cannot land in different
|
|
91
|
+
// places. The touch target now scales with the press (0.97 — ~1px on a 40dp
|
|
92
|
+
// button, and the press is registered before it starts), the same trade
|
|
93
|
+
// `PressableScale` made when it dropped its own two-node structure.
|
|
94
|
+
//
|
|
95
|
+
// `styled()` comes from Bloom's own `react-native-css` dependency rather than
|
|
96
|
+
// passing `className` as a bare prop and hoping the consumer's NativeWind
|
|
97
|
+
// interop recognises a component Bloom built at module scope. Same reasoning,
|
|
98
|
+
// and the same silent failure if skipped, as `typography/styled-text.ts`.
|
|
99
|
+
//
|
|
100
|
+
// Both wrappers are built ONCE at module scope so the element type is stable
|
|
101
|
+
// across renders — one built during render remounts the subtree every time.
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Exactly the prop surface `Button` hands to the pressable, and no wider.
|
|
106
|
+
* `Animated.createAnimatedComponent` maps `WithAnimatedValue` over every prop of
|
|
107
|
+
* the component it wraps, and doing that to the whole of `PressableProps`
|
|
108
|
+
* overflows the checker (`TS2590: union type that is too complex to represent`).
|
|
109
|
+
* Narrowing is also honest: the function form of `style` is deliberately
|
|
110
|
+
* excluded — see the `useInteractionState` comment in the component body for why
|
|
111
|
+
* Bloom never uses it.
|
|
112
|
+
*/
|
|
113
|
+
type ButtonPressableProps = Pick<
|
|
114
|
+
PressableProps,
|
|
115
|
+
| 'accessibilityHint'
|
|
116
|
+
| 'accessibilityLabel'
|
|
117
|
+
| 'accessibilityRole'
|
|
118
|
+
| 'accessibilityState'
|
|
119
|
+
| 'children'
|
|
120
|
+
| 'className'
|
|
121
|
+
| 'disabled'
|
|
122
|
+
| 'hitSlop'
|
|
123
|
+
| 'onPress'
|
|
124
|
+
| 'onPressIn'
|
|
125
|
+
| 'onPressOut'
|
|
126
|
+
| 'testID'
|
|
127
|
+
> & { style?: StyleProp<ViewStyle> };
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The same `Pressable` object, typed down to {@link ButtonPressableProps}.
|
|
131
|
+
* `styled()` derives its mapping type from a dot-path union over the wrapped
|
|
132
|
+
* component's ENTIRE prop type, and the one over `PressableProps` also overflows
|
|
133
|
+
* the checker. Nothing changes at runtime — this is the identical component.
|
|
134
|
+
*/
|
|
135
|
+
const ButtonPressable: ComponentType<ButtonPressableProps> = Pressable;
|
|
136
|
+
|
|
137
|
+
const StyledPressable: ComponentType<ButtonPressableProps> = styled(ButtonPressable, {
|
|
138
|
+
className: 'style',
|
|
139
|
+
});
|
|
140
|
+
const AnimatedPressable = Animated.createAnimatedComponent(StyledPressable);
|
|
141
|
+
|
|
72
142
|
const ButtonComponent: React.FC<ButtonProps> = ({
|
|
73
143
|
onPress,
|
|
74
144
|
children,
|
|
@@ -170,6 +240,16 @@ const ButtonComponent: React.FC<ButtonProps> = ({
|
|
|
170
240
|
styles.borderRadius = PILL_RADIUS;
|
|
171
241
|
break;
|
|
172
242
|
case 'icon':
|
|
243
|
+
// Resolved tokens, not `className="bg-background border border-border"`.
|
|
244
|
+
// The class form made the caller's `className` and the icon's own chrome
|
|
245
|
+
// compete for one slot — `<IconButton className="flex-1" />` replaced the
|
|
246
|
+
// default and shipped a transparent, borderless icon button — and it left
|
|
247
|
+
// the chrome dependent on the consumer's Tailwind pipeline resolving
|
|
248
|
+
// Bloom's color names. These are the SAME tokens `Button.web.tsx`'s
|
|
249
|
+
// `resolveVariantStyle('icon')` already writes inline.
|
|
250
|
+
styles.backgroundColor = theme.colors.background;
|
|
251
|
+
styles.borderWidth = 1;
|
|
252
|
+
styles.borderColor = theme.colors.border;
|
|
173
253
|
styles.borderRadius = PILL_RADIUS;
|
|
174
254
|
styles.padding = 8;
|
|
175
255
|
styles.width = sizeConfig.minHeight;
|
|
@@ -220,7 +300,6 @@ const ButtonComponent: React.FC<ButtonProps> = ({
|
|
|
220
300
|
|
|
221
301
|
const defaultHitSlop = variant === 'icon' ? ICON_HIT_SLOP : undefined;
|
|
222
302
|
const resolvedActiveOpacity = activeOpacity ?? (variant === 'icon' ? 0.7 : 0.8);
|
|
223
|
-
const resolvedClassName = className ?? (variant === 'icon' ? 'bg-background border border-border' : undefined);
|
|
224
303
|
|
|
225
304
|
const content = (
|
|
226
305
|
<>
|
|
@@ -233,44 +312,48 @@ const ButtonComponent: React.FC<ButtonProps> = ({
|
|
|
233
312
|
);
|
|
234
313
|
|
|
235
314
|
return (
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
315
|
+
<AnimatedPressable
|
|
316
|
+
className={className}
|
|
317
|
+
style={[
|
|
318
|
+
baseStyles,
|
|
319
|
+
disabled && !loading && { opacity: 0.5 },
|
|
320
|
+
pressed && !hasScaleFeedback && !isInteractionBlocked && { opacity: resolvedActiveOpacity },
|
|
321
|
+
// Before the caller's `style`, so `style` keeps winning the whole array
|
|
322
|
+
// the way it always has (and the way the web fork spreads it last). The
|
|
323
|
+
// cost is that a caller who sets `transform` on a scale variant replaces
|
|
324
|
+
// the press scale instead of composing with it — when the transform sat
|
|
325
|
+
// on a separate wrapper node the two multiplied.
|
|
326
|
+
hasScaleFeedback ? { transform: [{ scale: scaleAnim }] } : null,
|
|
327
|
+
style,
|
|
328
|
+
]}
|
|
329
|
+
onPress={isInteractionBlocked ? undefined : onPress}
|
|
330
|
+
onPressIn={handlePressIn}
|
|
331
|
+
onPressOut={handlePressOut}
|
|
332
|
+
disabled={isInteractionBlocked}
|
|
333
|
+
hitSlop={hitSlop ?? defaultHitSlop}
|
|
334
|
+
accessibilityLabel={accessibilityLabel}
|
|
335
|
+
accessibilityHint={accessibilityHint}
|
|
336
|
+
accessibilityRole="button"
|
|
337
|
+
accessibilityState={loading ? { disabled: isInteractionBlocked, busy: true } : { disabled: isInteractionBlocked }}
|
|
338
|
+
testID={testID}
|
|
339
|
+
>
|
|
340
|
+
{loading ? (
|
|
341
|
+
<>
|
|
342
|
+
<View
|
|
343
|
+
style={styles.loadingHiddenContent}
|
|
344
|
+
importantForAccessibility="no-hide-descendants"
|
|
345
|
+
accessibilityElementsHidden
|
|
346
|
+
>
|
|
347
|
+
{content}
|
|
348
|
+
</View>
|
|
349
|
+
<View style={styles.loadingOverlay}>
|
|
350
|
+
<ActivityIndicator size="small" color={loadingColor ?? resolvedTextColor} />
|
|
351
|
+
</View>
|
|
352
|
+
</>
|
|
353
|
+
) : (
|
|
354
|
+
content
|
|
355
|
+
)}
|
|
356
|
+
</AnimatedPressable>
|
|
274
357
|
);
|
|
275
358
|
};
|
|
276
359
|
|
|
@@ -18,9 +18,17 @@ import type { ButtonProps, ButtonSize, ButtonVariant } from './types';
|
|
|
18
18
|
export type { ButtonProps, ButtonVariant, ButtonSize } from './types';
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
|
-
// Geometry — mirrors the native (`Button.tsx`) impl
|
|
22
|
-
//
|
|
23
|
-
//
|
|
21
|
+
// Geometry — mirrors the native (`Button.tsx`) impl so the web button is
|
|
22
|
+
// pixel-consistent with native. Sizes below are the resolved primitives; the
|
|
23
|
+
// shadcn-style aliases (`sm | md | lg | icon`) are normalized first.
|
|
24
|
+
//
|
|
25
|
+
// ONE deliberate exception, in `containerStyle`: `text`/`link` clear
|
|
26
|
+
// `minHeight` here so the control hugs its label and sits inline (which is what
|
|
27
|
+
// makes `asChild` anchors work), while native keeps the size config's value to
|
|
28
|
+
// preserve the touch target. The 4/8 padding override those variants get is NOT
|
|
29
|
+
// an exception — native applies exactly the same one, so `text` is a compact
|
|
30
|
+
// affordance and `ghost` a full-size button without a background on both
|
|
31
|
+
// platforms. Only the variant COLOR table below is shared between them.
|
|
24
32
|
// ---------------------------------------------------------------------------
|
|
25
33
|
|
|
26
34
|
type NativeSize = 'small' | 'medium' | 'large';
|
package/src/button/types.ts
CHANGED
|
@@ -11,6 +11,14 @@ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
|
11
11
|
* the set. On native they fall back to a sensible equivalent
|
|
12
12
|
* (`outline → secondary`, `link → text`, `destructive → primary` tinted with
|
|
13
13
|
* the negative token) so passing them never crashes a native consumer.
|
|
14
|
+
*
|
|
15
|
+
* `text` (and its web alias `link`) is DELIBERATELY not a transparent `ghost`:
|
|
16
|
+
* it is the compact inline affordance, so it overrides the size config's padding
|
|
17
|
+
* down to 4/8 on BOTH platforms. `ghost` keeps the full padding — it is a
|
|
18
|
+
* regular button that happens to have no background. The one geometry that does
|
|
19
|
+
* differ by platform is `minHeight`: web clears it for `text`/`link` so the
|
|
20
|
+
* control hugs its label and sits inline, native keeps the size config's value
|
|
21
|
+
* because a finger needs the target and a cursor does not.
|
|
14
22
|
*/
|
|
15
23
|
export type ButtonVariant =
|
|
16
24
|
| 'primary'
|
|
@@ -18,10 +18,9 @@ import { Pressable, StyleSheet, View } from 'react-native';
|
|
|
18
18
|
|
|
19
19
|
import { useTheme } from '../theme/use-theme';
|
|
20
20
|
import { Text } from '../typography';
|
|
21
|
-
import { Backdrop } from '../overlay';
|
|
21
|
+
import { Backdrop, OverlayRoot } from '../overlay';
|
|
22
22
|
import { Portal } from '../portal/index.web';
|
|
23
23
|
import { useInteractionState } from '../hooks/useInteractionState';
|
|
24
|
-
import { createOverlayZIndex } from '../styles/z-index';
|
|
25
24
|
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
26
25
|
import {
|
|
27
26
|
resolveDropdownPlacement,
|
|
@@ -40,7 +39,6 @@ import type {
|
|
|
40
39
|
ItemContextValue,
|
|
41
40
|
} from './types';
|
|
42
41
|
|
|
43
|
-
const contextMenuZIndex = createOverlayZIndex();
|
|
44
42
|
const VIEWPORT_GUTTER = 8;
|
|
45
43
|
|
|
46
44
|
// ---------------------------------------------------------------------------
|
|
@@ -211,31 +209,38 @@ export function ContextMenuContent({ children, style }: ContextMenuContentProps)
|
|
|
211
209
|
|
|
212
210
|
return (
|
|
213
211
|
<Portal>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
212
|
+
{/* `OverlayRoot` takes this surface's place in the open-order overlay
|
|
213
|
+
stack, so it paints above anything opened before it (see
|
|
214
|
+
`src/overlay/stack.ts`). It is `box-none`, so the area outside the
|
|
215
|
+
panel stays click-through and the backdrop below still takes its own
|
|
216
|
+
presses. */}
|
|
217
|
+
<OverlayRoot>
|
|
218
|
+
<Backdrop
|
|
219
|
+
style={styles.backdrop}
|
|
220
|
+
onPress={close}
|
|
221
|
+
accessibilityLabel="Close context menu"
|
|
222
|
+
/>
|
|
223
|
+
<View
|
|
224
|
+
ref={attachMenu}
|
|
225
|
+
style={[
|
|
226
|
+
styles.dropdown,
|
|
227
|
+
{
|
|
228
|
+
// The raw click point is the pre-measurement anchor; the layout
|
|
229
|
+
// effect above replaces it before the browser paints.
|
|
230
|
+
top: placement?.top ?? position.y,
|
|
231
|
+
left: placement?.left ?? position.x,
|
|
232
|
+
backgroundColor: theme.isDark
|
|
233
|
+
? theme.colors.backgroundSecondary
|
|
234
|
+
: theme.colors.background,
|
|
235
|
+
borderColor: theme.colors.borderLight,
|
|
236
|
+
...bloomShadowStyle('m'),
|
|
237
|
+
},
|
|
238
|
+
style,
|
|
239
|
+
]}
|
|
240
|
+
>
|
|
241
|
+
{children}
|
|
242
|
+
</View>
|
|
243
|
+
</OverlayRoot>
|
|
239
244
|
</Portal>
|
|
240
245
|
);
|
|
241
246
|
}
|
|
@@ -374,13 +379,11 @@ const styles = StyleSheet.create({
|
|
|
374
379
|
left: 0,
|
|
375
380
|
right: 0,
|
|
376
381
|
bottom: 0,
|
|
377
|
-
zIndex: contextMenuZIndex.backdrop,
|
|
378
382
|
// Opt back in from the Portal root's `pointer-events: none`.
|
|
379
383
|
pointerEvents: 'auto',
|
|
380
384
|
},
|
|
381
385
|
dropdown: {
|
|
382
386
|
position: WEB_POSITION_FIXED,
|
|
383
|
-
zIndex: contextMenuZIndex.surface,
|
|
384
387
|
borderRadius: 8,
|
|
385
388
|
padding: 4,
|
|
386
389
|
borderWidth: 1,
|
package/src/dialog/Dialog.tsx
CHANGED
|
@@ -26,8 +26,7 @@ import Animated, {
|
|
|
26
26
|
} from 'react-native-reanimated';
|
|
27
27
|
|
|
28
28
|
import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
|
|
29
|
-
import { Backdrop } from '../overlay';
|
|
30
|
-
import { Z_INDEX, Z_INDEX_LAYER_STEP } from '../styles/z-index';
|
|
29
|
+
import { Backdrop, OverlayRoot } from '../overlay';
|
|
31
30
|
import { useTheme } from '../theme/use-theme';
|
|
32
31
|
import { Context, useDialogControl } from './context';
|
|
33
32
|
import { DialogBody } from './DialogContent';
|
|
@@ -117,7 +116,6 @@ function CenteredOrSideDialog({
|
|
|
117
116
|
scrollable,
|
|
118
117
|
morph,
|
|
119
118
|
placement,
|
|
120
|
-
layer,
|
|
121
119
|
width = DEFAULT_SIDE_WIDTH,
|
|
122
120
|
inset,
|
|
123
121
|
dismissOnBackdrop = true,
|
|
@@ -297,7 +295,6 @@ function CenteredOrSideDialog({
|
|
|
297
295
|
open={sideOpen}
|
|
298
296
|
onDismiss={handleDismiss}
|
|
299
297
|
side={placement}
|
|
300
|
-
layer={layer}
|
|
301
298
|
width={width}
|
|
302
299
|
inset={inset}
|
|
303
300
|
dismissOnBackdrop={dismissOnBackdrop}
|
|
@@ -393,7 +390,6 @@ function SideSheet({
|
|
|
393
390
|
open,
|
|
394
391
|
onDismiss,
|
|
395
392
|
side,
|
|
396
|
-
layer,
|
|
397
393
|
width,
|
|
398
394
|
inset,
|
|
399
395
|
dismissOnBackdrop,
|
|
@@ -417,7 +413,6 @@ function SideSheet({
|
|
|
417
413
|
open: boolean;
|
|
418
414
|
onDismiss: () => void;
|
|
419
415
|
side: 'left' | 'right';
|
|
420
|
-
layer?: number;
|
|
421
416
|
width: number;
|
|
422
417
|
inset?: DialogInset;
|
|
423
418
|
dismissOnBackdrop: boolean;
|
|
@@ -518,15 +513,10 @@ function SideSheet({
|
|
|
518
513
|
if (!mounted) return null;
|
|
519
514
|
|
|
520
515
|
return (
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
// Per-layer offset so a side dialog stacked on top of another (surface
|
|
526
|
-
// stack) paints above it. Layer 0 → offset 0 → unchanged.
|
|
527
|
-
{ zIndex: Z_INDEX.fullscreen + Z_INDEX_LAYER_STEP * (layer ?? 0) },
|
|
528
|
-
containerStyle,
|
|
529
|
-
]}
|
|
516
|
+
// `OverlayRoot` takes this drawer's place in the open-order overlay stack.
|
|
517
|
+
// It mounts here, past the `mounted` guard, so the rank tracks OPENING.
|
|
518
|
+
<OverlayRoot
|
|
519
|
+
style={[sideStyles.root, containerStyle]}
|
|
530
520
|
{...(containerClassName ? ({ className: containerClassName } as Record<string, string>) : {})}
|
|
531
521
|
>
|
|
532
522
|
<Backdrop
|
|
@@ -575,13 +565,14 @@ function SideSheet({
|
|
|
575
565
|
<View style={{ padding: contentPadding }}>{children}</View>
|
|
576
566
|
)}
|
|
577
567
|
</Animated.View>
|
|
578
|
-
</
|
|
568
|
+
</OverlayRoot>
|
|
579
569
|
);
|
|
580
570
|
}
|
|
581
571
|
|
|
582
572
|
const sideStyles = StyleSheet.create({
|
|
583
|
-
// `zIndex`
|
|
584
|
-
//
|
|
573
|
+
// No `zIndex` here: the drawer's depth relative to other surfaces is the
|
|
574
|
+
// overlay stack's decision, applied by `OverlayRoot` (see
|
|
575
|
+
// `src/overlay/stack.ts`).
|
|
585
576
|
root: {
|
|
586
577
|
...StyleSheet.absoluteFill,
|
|
587
578
|
},
|
|
@@ -25,7 +25,7 @@ import { RemoveScrollBar } from 'react-remove-scroll-bar';
|
|
|
25
25
|
|
|
26
26
|
import { Backdrop, OverlayRoot } from '../overlay';
|
|
27
27
|
import { Portal } from '../portal/index.web';
|
|
28
|
-
import {
|
|
28
|
+
import { Z_INDEX } from '../styles/z-index';
|
|
29
29
|
import { WEB_POSITION_FIXED, type WebCssStyle } from '../styles/web-view-style';
|
|
30
30
|
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
31
31
|
import { useTheme } from '../theme/use-theme';
|
|
@@ -170,7 +170,6 @@ function CenterOrSideDialog({
|
|
|
170
170
|
actions,
|
|
171
171
|
header,
|
|
172
172
|
placement,
|
|
173
|
-
layer,
|
|
174
173
|
width = DEFAULT_SIDE_WIDTH,
|
|
175
174
|
maxWidth = DEFAULT_CENTER_MAX_WIDTH,
|
|
176
175
|
inset,
|
|
@@ -192,11 +191,6 @@ function CenterOrSideDialog({
|
|
|
192
191
|
// stylesheet is present by the time the animated surface mounts).
|
|
193
192
|
useDialogCss();
|
|
194
193
|
|
|
195
|
-
// Per-layer overlay z-indices, so a dialog presented on top of another (via
|
|
196
|
-
// the surface stack) paints above it. Defaults to layer 0 — the offset is 0,
|
|
197
|
-
// so a lone dialog's z is byte-for-byte unchanged.
|
|
198
|
-
const dialogZIndex = useMemo(() => createOverlayZIndex(layer ?? 0), [layer]);
|
|
199
|
-
|
|
200
194
|
// Controlled mode is opt-in: when `open` is a boolean the host owns the
|
|
201
195
|
// visible state; otherwise the legacy imperative `control` path drives it.
|
|
202
196
|
const isControlled = controlledOpen !== undefined;
|
|
@@ -342,46 +336,51 @@ function CenterOrSideDialog({
|
|
|
342
336
|
<Context.Provider value={context}>
|
|
343
337
|
<ClosingContext.Provider value={isClosing}>
|
|
344
338
|
<RemoveScrollBar />
|
|
345
|
-
{/*
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
<
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
description={description}
|
|
371
|
-
actions={actions}
|
|
372
|
-
header={header}
|
|
373
|
-
style={style}
|
|
374
|
-
maxWidth={maxWidth}
|
|
375
|
-
contentPadding={contentPadding}
|
|
376
|
-
maxHeightRatio={maxHeightRatio}
|
|
377
|
-
scrollable={scrollable}
|
|
378
|
-
morph={morph}
|
|
379
|
-
surfaceZIndex={dialogZIndex.surface}
|
|
380
|
-
isClosing={isClosing}
|
|
339
|
+
{/* `OverlayRoot` takes this dialog's place in the open-order
|
|
340
|
+
overlay stack. It mounts here, inside the `isOpen` guard, which
|
|
341
|
+
is what makes the rank track OPENING rather than mounting — the
|
|
342
|
+
dialog is declared once at the app root and stays mounted for
|
|
343
|
+
the app's lifetime. */}
|
|
344
|
+
<OverlayRoot>
|
|
345
|
+
{/* The press target IS the full-viewport box, so it uses
|
|
346
|
+
`Backdrop` (which opts back in from the Portal root's
|
|
347
|
+
`pointer-events: none` via the `pointerEvents` PROP — the style
|
|
348
|
+
form is dropped before it reaches the DOM, see `src/overlay`)
|
|
349
|
+
and lays the panel out inside itself. */}
|
|
350
|
+
<Backdrop
|
|
351
|
+
onPress={() => close()}
|
|
352
|
+
disabled={!dismissOnBackdrop}
|
|
353
|
+
accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
|
|
354
|
+
// The fade rides on the LAYERS, never on the press target: an
|
|
355
|
+
// opacity animation on the blur's ancestor composites the group in
|
|
356
|
+
// isolation and leaves `backdrop-filter` nothing to sample.
|
|
357
|
+
progress={backdropFade}
|
|
358
|
+
style={{
|
|
359
|
+
position: WEB_POSITION_FIXED,
|
|
360
|
+
alignItems: 'center',
|
|
361
|
+
justifyContent: 'center',
|
|
362
|
+
paddingHorizontal: 20,
|
|
363
|
+
}}
|
|
381
364
|
>
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
365
|
+
<DialogPanel
|
|
366
|
+
testID={testID}
|
|
367
|
+
label={label}
|
|
368
|
+
title={title}
|
|
369
|
+
description={description}
|
|
370
|
+
actions={actions}
|
|
371
|
+
header={header}
|
|
372
|
+
style={style}
|
|
373
|
+
maxWidth={maxWidth}
|
|
374
|
+
contentPadding={contentPadding}
|
|
375
|
+
maxHeightRatio={maxHeightRatio}
|
|
376
|
+
scrollable={scrollable}
|
|
377
|
+
morph={morph}
|
|
378
|
+
isClosing={isClosing}
|
|
379
|
+
>
|
|
380
|
+
{children}
|
|
381
|
+
</DialogPanel>
|
|
382
|
+
</Backdrop>
|
|
383
|
+
</OverlayRoot>
|
|
385
384
|
</ClosingContext.Provider>
|
|
386
385
|
</Context.Provider>
|
|
387
386
|
</Portal>
|
|
@@ -407,8 +406,6 @@ function CenterOrSideDialog({
|
|
|
407
406
|
inset={inset}
|
|
408
407
|
dismissOnBackdrop={dismissOnBackdrop}
|
|
409
408
|
contentPadding={contentPadding}
|
|
410
|
-
backdropZIndex={dialogZIndex.backdrop}
|
|
411
|
-
surfaceZIndex={dialogZIndex.surface}
|
|
412
409
|
onDismiss={close}
|
|
413
410
|
panelStyle={panelStyle}
|
|
414
411
|
panelClassName={panelClassName}
|
|
@@ -437,7 +434,6 @@ function DialogPanel({
|
|
|
437
434
|
maxHeightRatio,
|
|
438
435
|
scrollable,
|
|
439
436
|
morph,
|
|
440
|
-
surfaceZIndex,
|
|
441
437
|
isClosing,
|
|
442
438
|
children,
|
|
443
439
|
}: {
|
|
@@ -453,7 +449,6 @@ function DialogPanel({
|
|
|
453
449
|
maxHeightRatio?: number;
|
|
454
450
|
scrollable?: boolean;
|
|
455
451
|
morph?: boolean;
|
|
456
|
-
surfaceZIndex: number;
|
|
457
452
|
isClosing: boolean;
|
|
458
453
|
children?: React.ReactNode;
|
|
459
454
|
}) {
|
|
@@ -518,7 +513,12 @@ function DialogPanel({
|
|
|
518
513
|
// Design-system overlay elevation (`shadow-m`) as a `boxShadow` — RN-Web
|
|
519
514
|
// deprecated the `shadow*` style props.
|
|
520
515
|
...bloomShadowStyle('m'),
|
|
521
|
-
|
|
516
|
+
// Above this surface's OWN backdrop, and nothing more: `OverlayRoot`
|
|
517
|
+
// sets a z-index and a fixed position, so it is a stacking context and
|
|
518
|
+
// this value is scoped inside it. Where this dialog sits relative to
|
|
519
|
+
// OTHER surfaces is the overlay stack's decision, made once on
|
|
520
|
+
// `OverlayRoot` (see `src/overlay/stack.ts`) — never a number here.
|
|
521
|
+
zIndex: Z_INDEX.raised,
|
|
522
522
|
},
|
|
523
523
|
isClosing ? ZOOM_FADE_OUT : ZOOM_FADE_IN,
|
|
524
524
|
// Drives `height` (and `maxWidth`) only while a morph is in flight; at
|
|
@@ -612,8 +612,6 @@ function SheetSurface({
|
|
|
612
612
|
inset,
|
|
613
613
|
dismissOnBackdrop,
|
|
614
614
|
contentPadding,
|
|
615
|
-
backdropZIndex,
|
|
616
|
-
surfaceZIndex,
|
|
617
615
|
onDismiss,
|
|
618
616
|
panelStyle,
|
|
619
617
|
panelClassName,
|
|
@@ -635,8 +633,6 @@ function SheetSurface({
|
|
|
635
633
|
inset?: DialogInset;
|
|
636
634
|
dismissOnBackdrop: boolean;
|
|
637
635
|
contentPadding: number;
|
|
638
|
-
backdropZIndex: number;
|
|
639
|
-
surfaceZIndex: number;
|
|
640
636
|
onDismiss: () => void;
|
|
641
637
|
panelStyle?: StyleProp<ViewStyle>;
|
|
642
638
|
panelClassName?: string;
|
|
@@ -714,7 +710,7 @@ function SheetSurface({
|
|
|
714
710
|
|
|
715
711
|
return (
|
|
716
712
|
<OverlayRoot
|
|
717
|
-
style={[sheetStyles.root,
|
|
713
|
+
style={[sheetStyles.root, containerStyle]}
|
|
718
714
|
{...(containerClassName ? ({ className: containerClassName } as Record<string, string>) : {})}
|
|
719
715
|
>
|
|
720
716
|
<Backdrop
|
|
@@ -744,7 +740,12 @@ function SheetSurface({
|
|
|
744
740
|
sheetStyles.panel,
|
|
745
741
|
{
|
|
746
742
|
backgroundColor: theme.colors.background,
|
|
747
|
-
|
|
743
|
+
// Above this surface's OWN backdrop, and nothing more: `OverlayRoot`
|
|
744
|
+
// sets a z-index and a fixed position, so it is a stacking context and
|
|
745
|
+
// this value is scoped inside it. Where this dialog sits relative to
|
|
746
|
+
// OTHER surfaces is the overlay stack's decision, made once on
|
|
747
|
+
// `OverlayRoot` (see `src/overlay/stack.ts`) — never a number here.
|
|
748
|
+
zIndex: Z_INDEX.raised,
|
|
748
749
|
pointerEvents: 'auto',
|
|
749
750
|
// Design-system overlay elevation (`shadow-m`) as a `boxShadow`.
|
|
750
751
|
...bloomShadowStyle('m'),
|
|
@@ -866,9 +867,10 @@ export function AutoMountedDialog({
|
|
|
866
867
|
// The annotation supplies the contextual type instead, so the literals narrow and
|
|
867
868
|
// a genuine mistake still fails.
|
|
868
869
|
const sheetStyles: Record<'root' | 'backdrop' | 'panel', ViewStyle> = {
|
|
869
|
-
// `zIndex`
|
|
870
|
-
// the
|
|
871
|
-
//
|
|
870
|
+
// No overlay `zIndex` here: `SheetSurface` renders an `OverlayRoot`, which
|
|
871
|
+
// assigns this drawer's depth from the open-order overlay stack, so a drawer
|
|
872
|
+
// opened on top of another surface paints above it (see
|
|
873
|
+
// `src/overlay/stack.ts`).
|
|
872
874
|
root: {
|
|
873
875
|
position: WEB_POSITION_FIXED,
|
|
874
876
|
top: 0,
|
package/src/dialog/types.ts
CHANGED
|
@@ -350,16 +350,6 @@ export type DialogProps = React.PropsWithChildren<{
|
|
|
350
350
|
containerClassName?: string;
|
|
351
351
|
/** Accessibility label, applied to the dialog role on web. */
|
|
352
352
|
label?: string;
|
|
353
|
-
/**
|
|
354
|
-
* Stacking layer index within a coordinated surface stack. Multiplies
|
|
355
|
-
* `Z_INDEX_LAYER_STEP` onto this dialog's overlay z-indices (backdrop +
|
|
356
|
-
* panel) so a dialog presented on top of another paints above it. Defaults to
|
|
357
|
-
* `0` — the single-dialog case, byte-for-byte unchanged. Set by the shared
|
|
358
|
-
* surface stack (`src/surfaces`); direct consumers rarely need it. No-op for
|
|
359
|
-
* the `bottom` placement, which already stacks in mount order (native RN
|
|
360
|
-
* `<Modal>` / the web portal), so `layer` is harmless there.
|
|
361
|
-
*/
|
|
362
|
-
layer?: number;
|
|
363
353
|
}>;
|
|
364
354
|
|
|
365
355
|
/**
|