@oxyhq/bloom 0.56.0 → 0.57.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/bottom-sheet/index.web.js +10 -10
- package/lib/commonjs/bottom-sheet/index.web.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.js +2 -3
- package/lib/commonjs/dialog/Dialog.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +9 -19
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/dialog/DialogHeader.js +4 -6
- package/lib/commonjs/dialog/DialogHeader.js.map +1 -1
- package/lib/commonjs/overlay/index.js +108 -0
- package/lib/commonjs/overlay/index.js.map +1 -0
- package/lib/commonjs/portal/index.js +4 -1
- package/lib/commonjs/portal/index.js.map +1 -1
- package/lib/commonjs/tab-bar/TabBarBase.js +10 -6
- package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
- package/lib/commonjs/toast/ToastHost.js +13 -5
- package/lib/commonjs/toast/ToastHost.js.map +1 -1
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +189 -196
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/module/bottom-sheet/index.web.js +10 -10
- package/lib/module/bottom-sheet/index.web.js.map +1 -1
- package/lib/module/dialog/Dialog.js +2 -3
- package/lib/module/dialog/Dialog.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +10 -20
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/dialog/DialogHeader.js +4 -6
- package/lib/module/dialog/DialogHeader.js.map +1 -1
- package/lib/module/overlay/index.js +102 -0
- package/lib/module/overlay/index.js.map +1 -0
- package/lib/module/portal/index.js +4 -1
- package/lib/module/portal/index.js.map +1 -1
- package/lib/module/tab-bar/TabBarBase.js +10 -6
- package/lib/module/tab-bar/TabBarBase.js.map +1 -1
- package/lib/module/toast/ToastHost.js +13 -5
- package/lib/module/toast/ToastHost.js.map +1 -1
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +178 -185
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/index.d.ts +67 -0
- package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/toast/ToastHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/index.web.d.ts.map +1 -1
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/overlay/index.d.ts +67 -0
- package/lib/typescript/module/overlay/index.d.ts.map +1 -0
- package/lib/typescript/module/toast/ToastHost.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/package.json +12 -1
- package/src/__tests__/TabBarBlur.test.tsx +6 -1
- package/src/__tests__/overlay-pointer-events.test.tsx +124 -0
- package/src/__tests__/pointer-events-style-form.test.ts +58 -0
- package/src/bottom-sheet/index.web.tsx +13 -8
- package/src/dialog/Dialog.tsx +1 -1
- package/src/dialog/Dialog.web.tsx +14 -16
- package/src/dialog/DialogHeader.tsx +2 -2
- package/src/overlay/index.tsx +115 -0
- package/src/portal/index.tsx +4 -2
- package/src/tab-bar/TabBarBase.tsx +8 -6
- package/src/toast/ToastHost.tsx +10 -5
- package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +15 -20
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared plumbing every portaled surface needs: an interactive ROOT and a
|
|
3
|
+
* press-to-dismiss BACKDROP. Dialog, BottomSheet, the image gallery, menus and
|
|
4
|
+
* toasts each used to hand-roll both, and each one got the web contract subtly
|
|
5
|
+
* wrong in its own way.
|
|
6
|
+
*
|
|
7
|
+
* ## The contract these two components encode
|
|
8
|
+
*
|
|
9
|
+
* Bloom's web `Portal` renders into `#bloom-portal-root`, which is
|
|
10
|
+
* `position: fixed; inset: 0; pointer-events: none` — an idle portal must let
|
|
11
|
+
* clicks reach the app underneath. `pointer-events` INHERITS in CSS, so every
|
|
12
|
+
* portaled descendant starts out click-through and has to opt back in.
|
|
13
|
+
*
|
|
14
|
+
* The opt-in only works through the `pointerEvents` **prop**. Passing it inside
|
|
15
|
+
* a `style` object (`style={{ pointerEvents: 'box-none' }}`) does NOT reach the
|
|
16
|
+
* DOM: react-native-web resolves the RN-only `box-none`/`box-only` values in
|
|
17
|
+
* `createDOMProps`, mapping the PROP onto its own class pair
|
|
18
|
+
* (`self { none !important }` + `> * { auto }`), while a style-object value is
|
|
19
|
+
* not valid CSS and is dropped. `pointerEvents: 'auto'`/`'none'` do survive as
|
|
20
|
+
* styles, which is what makes this so easy to get wrong — the two RN-only
|
|
21
|
+
* values silently do nothing. The symptom is brutal and silent: the whole
|
|
22
|
+
* surface — backdrop AND panel — renders perfectly and is completely
|
|
23
|
+
* click-through. Backdrop taps don't dismiss, buttons don't press, and the
|
|
24
|
+
* clicks land on whatever is behind the overlay (so a tap "through" a viewer
|
|
25
|
+
* navigates the page underneath). Only keyboard paths (Escape) keep working,
|
|
26
|
+
* which is what makes it look like a dismissal bug rather than a hit-testing
|
|
27
|
+
* one.
|
|
28
|
+
*
|
|
29
|
+
* Use `<OverlayRoot>` for the surface's outermost node and `<Backdrop>` for its
|
|
30
|
+
* dimming layer; do not re-implement either with raw `View`s.
|
|
31
|
+
*/
|
|
32
|
+
import { memo, type ReactNode } from 'react';
|
|
33
|
+
import { Platform, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
34
|
+
|
|
35
|
+
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
36
|
+
|
|
37
|
+
export interface OverlayRootProps {
|
|
38
|
+
children?: ReactNode;
|
|
39
|
+
style?: StyleProp<ViewStyle>;
|
|
40
|
+
testID?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Outermost node of a portaled surface. Fills the viewport and re-enables
|
|
45
|
+
* pointer events for its own children while empty gaps stay click-through
|
|
46
|
+
* (`box-none`), so a surface that only covers part of the screen never steals
|
|
47
|
+
* clicks from the app behind it.
|
|
48
|
+
*/
|
|
49
|
+
export function OverlayRoot({ children, style, testID }: OverlayRootProps) {
|
|
50
|
+
return (
|
|
51
|
+
<View pointerEvents="box-none" style={[styles.root, style]} testID={testID}>
|
|
52
|
+
{children}
|
|
53
|
+
</View>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
OverlayRoot.displayName = 'OverlayRoot';
|
|
58
|
+
|
|
59
|
+
export interface BackdropProps {
|
|
60
|
+
/** Dismiss handler. Omit (or pass `disabled`) for a backdrop that only dims. */
|
|
61
|
+
onPress?: () => void;
|
|
62
|
+
/** `true` keeps the dim but makes it inert — a blocking dialog, a busy state. */
|
|
63
|
+
disabled?: boolean;
|
|
64
|
+
/** Extra style (dim colour, animated opacity). Rendered over `absoluteFill`. */
|
|
65
|
+
style?: StyleProp<ViewStyle>;
|
|
66
|
+
/** Rendered inside the backdrop — e.g. a blur layer that must dim with it. */
|
|
67
|
+
children?: ReactNode;
|
|
68
|
+
accessibilityLabel?: string;
|
|
69
|
+
testID?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Full-bleed dimming layer that dismisses the surface when pressed. Always
|
|
74
|
+
* takes pointer events (that is its whole job), so it must render UNDER the
|
|
75
|
+
* surface's panel in the tree, never over it.
|
|
76
|
+
*/
|
|
77
|
+
export const Backdrop = memo(function Backdrop({
|
|
78
|
+
onPress,
|
|
79
|
+
disabled = false,
|
|
80
|
+
style,
|
|
81
|
+
children,
|
|
82
|
+
accessibilityLabel = 'Dismiss',
|
|
83
|
+
testID,
|
|
84
|
+
}: BackdropProps) {
|
|
85
|
+
const inert = disabled || !onPress;
|
|
86
|
+
return (
|
|
87
|
+
<Pressable
|
|
88
|
+
pointerEvents="auto"
|
|
89
|
+
onPress={inert ? undefined : onPress}
|
|
90
|
+
disabled={inert}
|
|
91
|
+
// A dimming layer is not a focus stop on web: Escape and the panel's own
|
|
92
|
+
// controls are the keyboard paths out. It stays labelled for screen
|
|
93
|
+
// readers that surface it as the dismiss affordance.
|
|
94
|
+
focusable={false}
|
|
95
|
+
accessibilityRole={inert ? undefined : 'button'}
|
|
96
|
+
accessibilityLabel={inert ? undefined : accessibilityLabel}
|
|
97
|
+
testID={testID}
|
|
98
|
+
style={[StyleSheet.absoluteFill, style]}
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
</Pressable>
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const styles = StyleSheet.create({
|
|
106
|
+
root: {
|
|
107
|
+
// Web: the portal root is `fixed; inset: 0`, but a plain absolute child of
|
|
108
|
+
// it anchors to the document flow in some browsers once the page scrolls —
|
|
109
|
+
// pin this box to the viewport itself. Native: absolute fill inside the
|
|
110
|
+
// Outlet, which is already full-screen.
|
|
111
|
+
...(Platform.OS === 'web'
|
|
112
|
+
? { position: WEB_POSITION_FIXED, top: 0, left: 0, right: 0, bottom: 0 }
|
|
113
|
+
: StyleSheet.absoluteFillObject),
|
|
114
|
+
},
|
|
115
|
+
});
|
package/src/portal/index.tsx
CHANGED
|
@@ -63,7 +63,7 @@ function createPortalGroup() {
|
|
|
63
63
|
function Outlet() {
|
|
64
64
|
const ctx = useContext(Context);
|
|
65
65
|
if (Platform.OS === 'web') {
|
|
66
|
-
return <View style={styles.portalOutlet}>{ctx.outlet}</View>;
|
|
66
|
+
return <View pointerEvents="box-none" style={styles.portalOutlet}>{ctx.outlet}</View>;
|
|
67
67
|
}
|
|
68
68
|
return ctx.outlet;
|
|
69
69
|
}
|
|
@@ -123,7 +123,9 @@ const styles = StyleSheet.create({
|
|
|
123
123
|
left: 0,
|
|
124
124
|
right: 0,
|
|
125
125
|
bottom: 0,
|
|
126
|
-
|
|
126
|
+
// `box-none` rides on the PROP above — react-native-web only resolves that
|
|
127
|
+
// RN-only value from the prop path, so as a style entry it is silently
|
|
128
|
+
// inert and this full-viewport outlet swallows every click in the app.
|
|
127
129
|
zIndex: Z_INDEX.portalRoot,
|
|
128
130
|
},
|
|
129
131
|
});
|
|
@@ -440,7 +440,7 @@ function TabBarBody({
|
|
|
440
440
|
);
|
|
441
441
|
|
|
442
442
|
return (
|
|
443
|
-
<View {...viewProps} style={[styles.root, style]}>
|
|
443
|
+
<View {...viewProps} pointerEvents="box-none" style={[styles.root, style]}>
|
|
444
444
|
{/* Progressive blur rising from the screen's bottom edge behind the pill.
|
|
445
445
|
Rendered CONDITIONALLY, and as nothing at all when off: the band is
|
|
446
446
|
full-bleed and 114pt tall at a zero bottom inset, so it blurs whatever
|
|
@@ -462,7 +462,7 @@ function TabBarBody({
|
|
|
462
462
|
}}
|
|
463
463
|
/>
|
|
464
464
|
)}
|
|
465
|
-
<View style={[styles.barWrap, { marginBottom: bottomOffset }, constrainedWrapStyle]}>
|
|
465
|
+
<View pointerEvents="box-none" style={[styles.barWrap, { marginBottom: bottomOffset }, constrainedWrapStyle]}>
|
|
466
466
|
<GestureDetector gesture={gesture}>
|
|
467
467
|
<Animated.View style={barStyle}>
|
|
468
468
|
<Surface theme={theme} style={shapeStyle} />
|
|
@@ -642,17 +642,19 @@ export function createTabBarButton(Glyph: ComponentType<TabBarGlyphProps>) {
|
|
|
642
642
|
|
|
643
643
|
const styles = StyleSheet.create({
|
|
644
644
|
root: {
|
|
645
|
+
// `pointerEvents="box-none"` is passed as a PROP on the views using these
|
|
646
|
+
// styles, NOT as a style entry: react-native-web resolves the RN-only
|
|
647
|
+
// `box-none` value only from the prop path, so as a style it silently does
|
|
648
|
+
// nothing and this full-width bottom band swallows every click in it. RNW
|
|
649
|
+
// logs a deprecation notice for the prop exactly once per session — the
|
|
650
|
+
// trade against a dead strip across the bottom of every page is not close.
|
|
645
651
|
position: 'absolute',
|
|
646
652
|
left: 0,
|
|
647
653
|
right: 0,
|
|
648
654
|
bottom: 0,
|
|
649
|
-
// In the style object, not as a prop: RN-Web deprecated the `pointerEvents`
|
|
650
|
-
// prop and warns on every render when it is used.
|
|
651
|
-
pointerEvents: 'box-none',
|
|
652
655
|
},
|
|
653
656
|
barWrap: {
|
|
654
657
|
marginHorizontal: BAR_MARGIN,
|
|
655
|
-
pointerEvents: 'box-none',
|
|
656
658
|
},
|
|
657
659
|
highlight: {
|
|
658
660
|
position: 'absolute',
|
package/src/toast/ToastHost.tsx
CHANGED
|
@@ -19,13 +19,14 @@
|
|
|
19
19
|
* swipe-to-dismiss would never receive touches. This provides its own, exactly
|
|
20
20
|
* as `bottom-sheet/index.web.tsx` does. Web consumers need no setup.
|
|
21
21
|
*
|
|
22
|
-
* `
|
|
22
|
+
* `OverlayRoot`'s `box-none` keeps the empty area click-through while the rows
|
|
23
23
|
* themselves stay interactive.
|
|
24
24
|
*/
|
|
25
25
|
import * as React from 'react';
|
|
26
26
|
import { StyleSheet } from 'react-native';
|
|
27
27
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
28
28
|
|
|
29
|
+
import { OverlayRoot } from '../overlay';
|
|
29
30
|
import { Portal } from '../portal/index.web';
|
|
30
31
|
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
31
32
|
import { Z_INDEX } from '../styles/z-index';
|
|
@@ -33,9 +34,14 @@ import type { ToastHostProps } from './types';
|
|
|
33
34
|
|
|
34
35
|
export function ToastHost({ children, ToasterOverlayWrapper }: ToastHostProps) {
|
|
35
36
|
const content = (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// `OverlayRoot` carries the pointer-events opt-in as a PROP; as a style
|
|
38
|
+
// entry it never reached the DOM, so the rows inherited the portal root's
|
|
39
|
+
// `pointer-events: none` and no toast could be pressed or swiped away.
|
|
40
|
+
<OverlayRoot style={styles.host}>
|
|
41
|
+
<GestureHandlerRootView style={StyleSheet.absoluteFill}>
|
|
42
|
+
{children}
|
|
43
|
+
</GestureHandlerRootView>
|
|
44
|
+
</OverlayRoot>
|
|
39
45
|
);
|
|
40
46
|
|
|
41
47
|
return (
|
|
@@ -62,6 +68,5 @@ const styles = StyleSheet.create({
|
|
|
62
68
|
// Inside `#bloom-portal-root` (which owns the document layer at 999999),
|
|
63
69
|
// this keeps toasts above Dialog's surface (60) and tooltips (70).
|
|
64
70
|
zIndex: Z_INDEX.toast,
|
|
65
|
-
pointerEvents: 'box-none',
|
|
66
71
|
},
|
|
67
72
|
});
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
GestureDetector,
|
|
29
29
|
} from 'react-native-gesture-handler';
|
|
30
30
|
import { useTheme } from '../theme/use-theme';
|
|
31
|
+
import { Backdrop, OverlayRoot } from '../overlay';
|
|
31
32
|
import { Portal } from '../portal';
|
|
32
33
|
import { PressableWithHover } from '../pressable-with-hover';
|
|
33
34
|
import {
|
|
@@ -423,19 +424,6 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
423
424
|
|
|
424
425
|
React.useImperativeHandle(ref, () => ({ open }), [open]);
|
|
425
426
|
|
|
426
|
-
// Tapping the backdrop dismisses. Uses the same Gesture system as every
|
|
427
|
-
// other interaction in this component (image tap, pinch, pan, double-tap)
|
|
428
|
-
// instead of a plain RN Pressable, so the backdrop isn't the one interaction
|
|
429
|
-
// mixing two different event-handling systems under the same
|
|
430
|
-
// GestureHandlerRootView.
|
|
431
|
-
const backdropTapGesture = useMemo(
|
|
432
|
-
() =>
|
|
433
|
-
Gesture.Tap().onEnd(() => {
|
|
434
|
-
runOnJS(handleDismiss)();
|
|
435
|
-
}),
|
|
436
|
-
[handleDismiss]
|
|
437
|
-
);
|
|
438
|
-
|
|
439
427
|
const panGesture = useMemo(
|
|
440
428
|
() =>
|
|
441
429
|
Gesture.Pan()
|
|
@@ -756,9 +744,16 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
756
744
|
}, [handleDismiss, isOpen, pageTo]);
|
|
757
745
|
|
|
758
746
|
const renderContent = () => (
|
|
759
|
-
<
|
|
760
|
-
<
|
|
761
|
-
|
|
747
|
+
<OverlayRoot style={styles.modalContainer}>
|
|
748
|
+
<GestureHandlerRootView style={StyleSheet.absoluteFill}>
|
|
749
|
+
{/* Shared `Backdrop` (a Pressable) rather than a `Gesture.Tap()`: the
|
|
750
|
+
viewer renders through the web Portal, whose root is
|
|
751
|
+
`pointer-events: none`, and a gesture handler on a node that never
|
|
752
|
+
receives pointer events simply never fires — tapping the backdrop
|
|
753
|
+
did nothing on web while Escape still closed. `Backdrop` opts back
|
|
754
|
+
in via the `pointerEvents` PROP, the only form that reaches the DOM
|
|
755
|
+
(see `src/overlay`). */}
|
|
756
|
+
<Backdrop onPress={handleDismiss} accessibilityLabel="Close image viewer">
|
|
762
757
|
<AnimatedBlurView
|
|
763
758
|
intensity={80}
|
|
764
759
|
tint={theme.isDark ? 'dark' : 'light'}
|
|
@@ -769,8 +764,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
769
764
|
style={[StyleSheet.absoluteFill, { backgroundColor: theme.colors.overlay }, backdropStyle]}
|
|
770
765
|
/>
|
|
771
766
|
</AnimatedBlurView>
|
|
772
|
-
</
|
|
773
|
-
</GestureDetector>
|
|
767
|
+
</Backdrop>
|
|
774
768
|
|
|
775
769
|
<GestureDetector gesture={panGesture}>
|
|
776
770
|
<Animated.View
|
|
@@ -961,8 +955,9 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
961
955
|
</Animated.View>
|
|
962
956
|
) : null}
|
|
963
957
|
</Animated.View>
|
|
964
|
-
|
|
965
|
-
|
|
958
|
+
</GestureDetector>
|
|
959
|
+
</GestureHandlerRootView>
|
|
960
|
+
</OverlayRoot>
|
|
966
961
|
);
|
|
967
962
|
|
|
968
963
|
if (!isOpen) return null;
|