@oxyhq/bloom 0.57.1 → 0.58.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/BottomSheetBase.js +15 -9
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +17 -23
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/overlay/index.js +44 -7
- package/lib/commonjs/overlay/index.js.map +1 -1
- package/lib/commonjs/provider/index.js +6 -0
- package/lib/commonjs/provider/index.js.map +1 -1
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +27 -16
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +16 -10
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +17 -23
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/overlay/index.js +45 -7
- package/lib/module/overlay/index.js.map +1 -1
- package/lib/module/provider/index.js +6 -0
- package/lib/module/provider/index.js.map +1 -1
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +27 -16
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.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 +31 -6
- package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/provider/index.d.ts +6 -0
- package/lib/typescript/commonjs/provider/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.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 +31 -6
- package/lib/typescript/module/overlay/index.d.ts.map +1 -1
- package/lib/typescript/module/provider/index.d.ts +6 -0
- package/lib/typescript/module/provider/index.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/overlay-pointer-events.test.tsx +17 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +18 -9
- package/src/dialog/Dialog.web.tsx +20 -24
- package/src/overlay/index.tsx +60 -7
- package/src/provider/index.tsx +6 -0
- package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +32 -17
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The shared plumbing every portaled surface needs: an interactive ROOT and a
|
|
3
3
|
* press-to-dismiss BACKDROP. Dialog, BottomSheet, the image gallery, menus and
|
|
4
4
|
* toasts each used to hand-roll both, and each one got the web contract subtly
|
|
5
|
-
* wrong in its own way
|
|
5
|
+
* wrong in its own way — and looked different while doing it (the image viewer
|
|
6
|
+
* blurred, everything else only dimmed).
|
|
6
7
|
*
|
|
7
8
|
* ## The contract these two components encode
|
|
8
9
|
*
|
|
@@ -26,11 +27,27 @@
|
|
|
26
27
|
* which is what makes it look like a dismissal bug rather than a hit-testing
|
|
27
28
|
* one.
|
|
28
29
|
*
|
|
30
|
+
* A backdrop also only dismisses what is actually ON TOP of it: a full-screen
|
|
31
|
+
* layer rendered ABOVE the backdrop (a pager, a zoom container) receives the
|
|
32
|
+
* press first and swallows it. Either that layer opts out (`pointerEvents
|
|
33
|
+
*="box-none"`) or it owns the dismiss itself — the backdrop being present is
|
|
34
|
+
* not enough.
|
|
35
|
+
*
|
|
36
|
+
* EXPO/EXPO-ROUTER APPS ONLY for `BloomProvider` — see `src/provider`; these
|
|
37
|
+
* two components are universal.
|
|
38
|
+
*
|
|
29
39
|
* Use `<OverlayRoot>` for the surface's outermost node and `<Backdrop>` for its
|
|
30
40
|
* dimming layer; do not re-implement either with raw `View`s.
|
|
31
41
|
*/
|
|
32
42
|
import { type ReactNode } from 'react';
|
|
33
43
|
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
44
|
+
/**
|
|
45
|
+
* One blur radius for every Bloom overlay. Surfaces differ in what they show,
|
|
46
|
+
* not in how the app behind them recedes.
|
|
47
|
+
*/
|
|
48
|
+
export declare const BACKDROP_BLUR_INTENSITY = 40;
|
|
49
|
+
/** Dim laid over the blur — the blur alone does not give enough contrast. */
|
|
50
|
+
export declare const BACKDROP_DIM_OPACITY = 0.45;
|
|
34
51
|
export interface OverlayRootProps {
|
|
35
52
|
children?: ReactNode;
|
|
36
53
|
style?: StyleProp<ViewStyle>;
|
|
@@ -51,17 +68,25 @@ export interface BackdropProps {
|
|
|
51
68
|
onPress?: () => void;
|
|
52
69
|
/** `true` keeps the dim but makes it inert — a blocking dialog, a busy state. */
|
|
53
70
|
disabled?: boolean;
|
|
54
|
-
/**
|
|
71
|
+
/** Blur radius behind the dim. `0` renders the dim alone. */
|
|
72
|
+
blurIntensity?: number;
|
|
73
|
+
/** Blur tint. Backdrops dim the app, so `dark` is the default on every theme. */
|
|
74
|
+
blurTint?: 'light' | 'dark' | 'default';
|
|
75
|
+
/** Dim colour over the blur. */
|
|
76
|
+
dimColor?: string;
|
|
77
|
+
/** Dim opacity, 0–1. */
|
|
78
|
+
dimOpacity?: number;
|
|
79
|
+
/** Extra style on the press target — animated opacity, insets, z-index. */
|
|
55
80
|
style?: StyleProp<ViewStyle>;
|
|
56
|
-
/** Rendered
|
|
81
|
+
/** Rendered ON TOP of the dim, inside the press target (Dialog's panel does this). */
|
|
57
82
|
children?: ReactNode;
|
|
58
83
|
accessibilityLabel?: string;
|
|
59
84
|
testID?: string;
|
|
60
85
|
}
|
|
61
86
|
/**
|
|
62
|
-
* Full-bleed
|
|
63
|
-
*
|
|
64
|
-
*
|
|
87
|
+
* Full-bleed blur + dim that dismisses the surface when pressed. Always takes
|
|
88
|
+
* pointer events (that is its whole job), so anything that must stay pressable
|
|
89
|
+
* goes INSIDE it as `children`, never as a sibling rendered over it.
|
|
65
90
|
*/
|
|
66
91
|
export declare const Backdrop: import("react").NamedExoticComponent<BackdropProps>;
|
|
67
92
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/overlay/index.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/overlay/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7C,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAItB;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,6EAA6E;AAC7E,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,gBAAgB,2CAMxE;yBANe,WAAW;;;AAU3B,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IACxC,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,sFAAsF;IACtF,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,qDA6CnB,CAAC"}
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* Nesting extra contexts costs nothing at runtime — the win is that scope is no
|
|
19
19
|
* longer a per-app decision.
|
|
20
20
|
*
|
|
21
|
+
* EXPO/EXPO-ROUTER APPS ONLY. The scroll-restoration provider it mounts imports
|
|
22
|
+
* `expo-router` (its web implementation keys offsets on the focused route), so
|
|
23
|
+
* a Vite/SPA consumer that has no expo-router cannot resolve this module. Those
|
|
24
|
+
* apps keep mounting `BloomThemeProvider` (and any other piece they need)
|
|
25
|
+
* directly — that is not a lesser path, just the one without a router.
|
|
26
|
+
*
|
|
21
27
|
* NOT included, on purpose — these are OUTLETS, not state, and their placement
|
|
22
28
|
* in the tree is a real app decision (z-order, safe areas, and mounting a
|
|
23
29
|
* second one duplicates every surface it renders):
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/provider/index.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/provider/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAG5E,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC;IACnF,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wFAAwF;IACxF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,aAAa,EACb,OAAc,EACd,GAAG,UAAU,EACd,EAAE,kBAAkB,2CAcpB;yBAnBe,aAAa"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ZoomableImageGallery.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/ZoomableImageGallery.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAmEjF,OAAO,KAAK,EAGV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"ZoomableImageGallery.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/ZoomableImageGallery.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAmEjF,OAAO,KAAK,EAGV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,SAAS,CAAC;AAs5BjB,eAAO,MAAM,oBAAoB,8GAA4B,CAAC;AAyK9D,eAAe,oBAAoB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BottomSheetBase.d.ts","sourceRoot":"","sources":["../../../../src/bottom-sheet/BottomSheetBase.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAMH,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,KAAK,SAAS,EACjB,MAAM,cAAc,CAAC;AAItB,OAAiB,EACb,KAAK,aAAa,EAGlB,KAAK,WAAW,EAMnB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"BottomSheetBase.d.ts","sourceRoot":"","sources":["../../../../src/bottom-sheet/BottomSheetBase.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAMH,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,KAAK,SAAS,EACjB,MAAM,cAAc,CAAC;AAItB,OAAiB,EACb,KAAK,aAAa,EAGlB,KAAK,WAAW,EAMnB,MAAM,yBAAyB,CAAC;AAyBjC,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;KAAE,KAAK,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IAC7F,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IACjH;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;IAC5C,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;;;;;OAcG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IACxC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B;;;;OAIG;IACH,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACjD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IAClC,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,gFAAgF;IAChF,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B;;;OAGG;IACH,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB;IAC1D,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC;CACrD;AAID,eAAO,MAAM,eAAe,6FA4mB1B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dialog.web.d.ts","sourceRoot":"","sources":["../../../../src/dialog/Dialog.web.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAOL,4BAA4B,EAM7B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,YAAY,EAGZ,WAAW,EACZ,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Dialog.web.d.ts","sourceRoot":"","sources":["../../../../src/dialog/Dialog.web.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAOL,4BAA4B,EAM7B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,YAAY,EAGZ,WAAW,EACZ,MAAM,SAAS,CAAC;AA4DjB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,WAAW,2CAazD;AA+ZD,OAAO,EAAE,4BAA4B,EAAE,CAAC;AAoPxC;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,WAAW,EACX,OAAO,EACP,SAAS,GACV,EAAE;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB,2CAgBA;AAiCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,gBAAgB,mZAK5B,CAAC"}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The shared plumbing every portaled surface needs: an interactive ROOT and a
|
|
3
3
|
* press-to-dismiss BACKDROP. Dialog, BottomSheet, the image gallery, menus and
|
|
4
4
|
* toasts each used to hand-roll both, and each one got the web contract subtly
|
|
5
|
-
* wrong in its own way
|
|
5
|
+
* wrong in its own way — and looked different while doing it (the image viewer
|
|
6
|
+
* blurred, everything else only dimmed).
|
|
6
7
|
*
|
|
7
8
|
* ## The contract these two components encode
|
|
8
9
|
*
|
|
@@ -26,11 +27,27 @@
|
|
|
26
27
|
* which is what makes it look like a dismissal bug rather than a hit-testing
|
|
27
28
|
* one.
|
|
28
29
|
*
|
|
30
|
+
* A backdrop also only dismisses what is actually ON TOP of it: a full-screen
|
|
31
|
+
* layer rendered ABOVE the backdrop (a pager, a zoom container) receives the
|
|
32
|
+
* press first and swallows it. Either that layer opts out (`pointerEvents
|
|
33
|
+
*="box-none"`) or it owns the dismiss itself — the backdrop being present is
|
|
34
|
+
* not enough.
|
|
35
|
+
*
|
|
36
|
+
* EXPO/EXPO-ROUTER APPS ONLY for `BloomProvider` — see `src/provider`; these
|
|
37
|
+
* two components are universal.
|
|
38
|
+
*
|
|
29
39
|
* Use `<OverlayRoot>` for the surface's outermost node and `<Backdrop>` for its
|
|
30
40
|
* dimming layer; do not re-implement either with raw `View`s.
|
|
31
41
|
*/
|
|
32
42
|
import { type ReactNode } from 'react';
|
|
33
43
|
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
44
|
+
/**
|
|
45
|
+
* One blur radius for every Bloom overlay. Surfaces differ in what they show,
|
|
46
|
+
* not in how the app behind them recedes.
|
|
47
|
+
*/
|
|
48
|
+
export declare const BACKDROP_BLUR_INTENSITY = 40;
|
|
49
|
+
/** Dim laid over the blur — the blur alone does not give enough contrast. */
|
|
50
|
+
export declare const BACKDROP_DIM_OPACITY = 0.45;
|
|
34
51
|
export interface OverlayRootProps {
|
|
35
52
|
children?: ReactNode;
|
|
36
53
|
style?: StyleProp<ViewStyle>;
|
|
@@ -51,17 +68,25 @@ export interface BackdropProps {
|
|
|
51
68
|
onPress?: () => void;
|
|
52
69
|
/** `true` keeps the dim but makes it inert — a blocking dialog, a busy state. */
|
|
53
70
|
disabled?: boolean;
|
|
54
|
-
/**
|
|
71
|
+
/** Blur radius behind the dim. `0` renders the dim alone. */
|
|
72
|
+
blurIntensity?: number;
|
|
73
|
+
/** Blur tint. Backdrops dim the app, so `dark` is the default on every theme. */
|
|
74
|
+
blurTint?: 'light' | 'dark' | 'default';
|
|
75
|
+
/** Dim colour over the blur. */
|
|
76
|
+
dimColor?: string;
|
|
77
|
+
/** Dim opacity, 0–1. */
|
|
78
|
+
dimOpacity?: number;
|
|
79
|
+
/** Extra style on the press target — animated opacity, insets, z-index. */
|
|
55
80
|
style?: StyleProp<ViewStyle>;
|
|
56
|
-
/** Rendered
|
|
81
|
+
/** Rendered ON TOP of the dim, inside the press target (Dialog's panel does this). */
|
|
57
82
|
children?: ReactNode;
|
|
58
83
|
accessibilityLabel?: string;
|
|
59
84
|
testID?: string;
|
|
60
85
|
}
|
|
61
86
|
/**
|
|
62
|
-
* Full-bleed
|
|
63
|
-
*
|
|
64
|
-
*
|
|
87
|
+
* Full-bleed blur + dim that dismisses the surface when pressed. Always takes
|
|
88
|
+
* pointer events (that is its whole job), so anything that must stay pressable
|
|
89
|
+
* goes INSIDE it as `children`, never as a sibling rendered over it.
|
|
65
90
|
*/
|
|
66
91
|
export declare const Backdrop: import("react").NamedExoticComponent<BackdropProps>;
|
|
67
92
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/overlay/index.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/overlay/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7C,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAItB;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,6EAA6E;AAC7E,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,gBAAgB,2CAMxE;yBANe,WAAW;;;AAU3B,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IACxC,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,sFAAsF;IACtF,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,qDA6CnB,CAAC"}
|
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* Nesting extra contexts costs nothing at runtime — the win is that scope is no
|
|
19
19
|
* longer a per-app decision.
|
|
20
20
|
*
|
|
21
|
+
* EXPO/EXPO-ROUTER APPS ONLY. The scroll-restoration provider it mounts imports
|
|
22
|
+
* `expo-router` (its web implementation keys offsets on the focused route), so
|
|
23
|
+
* a Vite/SPA consumer that has no expo-router cannot resolve this module. Those
|
|
24
|
+
* apps keep mounting `BloomThemeProvider` (and any other piece they need)
|
|
25
|
+
* directly — that is not a lesser path, just the one without a router.
|
|
26
|
+
*
|
|
21
27
|
* NOT included, on purpose — these are OUTLETS, not state, and their placement
|
|
22
28
|
* in the tree is a real app decision (z-order, safe areas, and mounting a
|
|
23
29
|
* second one duplicates every surface it renders):
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/provider/index.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/provider/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAG5E,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC;IACnF,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wFAAwF;IACxF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,aAAa,EACb,OAAc,EACd,GAAG,UAAU,EACd,EAAE,kBAAkB,2CAcpB;yBAnBe,aAAa"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ZoomableImageGallery.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/ZoomableImageGallery.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAmEjF,OAAO,KAAK,EAGV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"ZoomableImageGallery.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/ZoomableImageGallery.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAmEjF,OAAO,KAAK,EAGV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,SAAS,CAAC;AAs5BjB,eAAO,MAAM,oBAAoB,8GAA4B,CAAC;AAyK9D,eAAe,oBAAoB,CAAC"}
|
package/package.json
CHANGED
|
@@ -106,6 +106,23 @@ describe('overlay pointer-events contract (web)', () => {
|
|
|
106
106
|
container.remove();
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
+
// Every Bloom surface dims the app the same way: the blur is part of the
|
|
110
|
+
// shared backdrop, not something each surface opts into (it used to be — the
|
|
111
|
+
// image viewer blurred, dialogs and sheets only dimmed).
|
|
112
|
+
it('blurs by default and drops the blur layer only when asked', () => {
|
|
113
|
+
const withBlur = render(createElement(Backdrop, { onPress: () => {}, testID: 'a' }));
|
|
114
|
+
expect(withBlur.container.getElementsByTagName('blurview').length).toBe(1);
|
|
115
|
+
act(() => withBlur.root.unmount());
|
|
116
|
+
withBlur.container.remove();
|
|
117
|
+
|
|
118
|
+
const noBlur = render(
|
|
119
|
+
createElement(Backdrop, { onPress: () => {}, blurIntensity: 0, testID: 'b' }),
|
|
120
|
+
);
|
|
121
|
+
expect(noBlur.container.getElementsByTagName('blurview').length).toBe(0);
|
|
122
|
+
act(() => noBlur.root.unmount());
|
|
123
|
+
noBlur.container.remove();
|
|
124
|
+
});
|
|
125
|
+
|
|
109
126
|
it('a disabled Backdrop dims without dismissing', () => {
|
|
110
127
|
const onPress = jest.fn();
|
|
111
128
|
const { root, container } = render(
|
|
@@ -25,6 +25,7 @@ import Animated, {
|
|
|
25
25
|
withTiming,
|
|
26
26
|
} from 'react-native-reanimated';
|
|
27
27
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
28
|
+
import { Backdrop } from '../overlay';
|
|
28
29
|
import { useTheme } from '../theme/use-theme';
|
|
29
30
|
|
|
30
31
|
/** Hook that returns current screen dimensions and updates on rotation/resize. */
|
|
@@ -198,6 +199,8 @@ export interface BottomSheetBaseProps extends BottomSheetProps {
|
|
|
198
199
|
Shell: React.ComponentType<BottomSheetShellProps>;
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
const AnimatedBackdrop = Animated.createAnimatedComponent(Backdrop);
|
|
203
|
+
|
|
201
204
|
export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: React.ForwardedRef<BottomSheetRef>) => {
|
|
202
205
|
const {
|
|
203
206
|
Shell,
|
|
@@ -783,15 +786,21 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
|
|
|
783
786
|
return (
|
|
784
787
|
<Shell visible={rendered} onRequestClose={dismiss} keyboardHeight={keyboardHeight}>
|
|
785
788
|
<View style={StyleSheet.absoluteFill}>
|
|
786
|
-
|
|
787
|
-
{
|
|
788
|
-
backdropComponent({ onPress: handleBackdropPress })
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
789
|
+
{backdropComponent ? (
|
|
790
|
+
<Animated.View style={[StyleSheet.absoluteFill, backdropStyle]}>
|
|
791
|
+
{backdropComponent({ onPress: handleBackdropPress })}
|
|
792
|
+
</Animated.View>
|
|
793
|
+
) : (
|
|
794
|
+
// The ONE Bloom backdrop (blur + dim + press-to-dismiss).
|
|
795
|
+
// `backdropStyle` drives the fade; the dim level rides on
|
|
796
|
+
// `backdropOpacity`, so the shared component's own dim is
|
|
797
|
+
// switched off here to keep a single source of dimming.
|
|
798
|
+
<AnimatedBackdrop
|
|
799
|
+
onPress={handleBackdropPress}
|
|
800
|
+
dimOpacity={1}
|
|
801
|
+
style={[styles.backdrop, backdropStyle]}
|
|
802
|
+
/>
|
|
803
|
+
)}
|
|
795
804
|
|
|
796
805
|
<GestureDetector gesture={panGesture}>
|
|
797
806
|
<Animated.View
|
|
@@ -60,6 +60,12 @@ import type {
|
|
|
60
60
|
|
|
61
61
|
const FADE_OUT_DURATION = CENTER_FADE_OUT_DURATION;
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The centered card's scrim reads darker than a sheet's because the card floats
|
|
65
|
+
* in the middle of it; the shared blur underneath is identical.
|
|
66
|
+
*/
|
|
67
|
+
const DIALOG_BACKDROP_DIM_OPACITY = 0.6;
|
|
68
|
+
|
|
63
69
|
/**
|
|
64
70
|
* The four CSS `animation` shorthands driving the centered card and its backdrop.
|
|
65
71
|
* `animation` has no `ViewStyle` key at all — react-native-web forwards it to the
|
|
@@ -325,15 +331,18 @@ function CenterOrSideDialog({
|
|
|
325
331
|
onPress={() => close()}
|
|
326
332
|
disabled={!dismissOnBackdrop}
|
|
327
333
|
accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
334
|
+
dimOpacity={DIALOG_BACKDROP_DIM_OPACITY}
|
|
335
|
+
style={[
|
|
336
|
+
{
|
|
337
|
+
position: WEB_POSITION_FIXED,
|
|
338
|
+
zIndex: dialogZIndex.backdrop,
|
|
339
|
+
alignItems: 'center',
|
|
340
|
+
justifyContent: 'center',
|
|
341
|
+
paddingHorizontal: 20,
|
|
342
|
+
},
|
|
343
|
+
isClosing ? BACKDROP_FADE_OUT : BACKDROP_FADE_IN,
|
|
344
|
+
]}
|
|
335
345
|
>
|
|
336
|
-
<DialogBackdrop isClosing={isClosing} />
|
|
337
346
|
<DialogPanel
|
|
338
347
|
testID={testID}
|
|
339
348
|
label={label}
|
|
@@ -693,6 +702,9 @@ function SheetSurface({
|
|
|
693
702
|
accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
|
|
694
703
|
onPress={handleBackdropPress}
|
|
695
704
|
disabled={!dismissOnBackdrop}
|
|
705
|
+
// The dim rides on the element's own animated opacity here, so the
|
|
706
|
+
// shared component's dim layer stays fully opaque under it.
|
|
707
|
+
dimOpacity={1}
|
|
696
708
|
style={[
|
|
697
709
|
sheetStyles.backdrop,
|
|
698
710
|
backdropTransition,
|
|
@@ -799,22 +811,6 @@ function cancelFrame(token: FrameToken): void {
|
|
|
799
811
|
clearTimeout(token.timer);
|
|
800
812
|
}
|
|
801
813
|
|
|
802
|
-
function DialogBackdrop({ isClosing }: { isClosing: boolean }) {
|
|
803
|
-
const style: ViewStyle[] = [
|
|
804
|
-
{
|
|
805
|
-
position: WEB_POSITION_FIXED,
|
|
806
|
-
top: 0,
|
|
807
|
-
left: 0,
|
|
808
|
-
right: 0,
|
|
809
|
-
bottom: 0,
|
|
810
|
-
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
811
|
-
},
|
|
812
|
-
isClosing ? BACKDROP_FADE_OUT : BACKDROP_FADE_IN,
|
|
813
|
-
];
|
|
814
|
-
|
|
815
|
-
return <View style={style} />;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
814
|
/**
|
|
819
815
|
* Inline imperative dialog used by `alert()`. Mounts and immediately
|
|
820
816
|
* presents; resolves the host's `onResolve` once the dialog has finished
|
package/src/overlay/index.tsx
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The shared plumbing every portaled surface needs: an interactive ROOT and a
|
|
3
3
|
* press-to-dismiss BACKDROP. Dialog, BottomSheet, the image gallery, menus and
|
|
4
4
|
* toasts each used to hand-roll both, and each one got the web contract subtly
|
|
5
|
-
* wrong in its own way
|
|
5
|
+
* wrong in its own way — and looked different while doing it (the image viewer
|
|
6
|
+
* blurred, everything else only dimmed).
|
|
6
7
|
*
|
|
7
8
|
* ## The contract these two components encode
|
|
8
9
|
*
|
|
@@ -26,14 +27,39 @@
|
|
|
26
27
|
* which is what makes it look like a dismissal bug rather than a hit-testing
|
|
27
28
|
* one.
|
|
28
29
|
*
|
|
30
|
+
* A backdrop also only dismisses what is actually ON TOP of it: a full-screen
|
|
31
|
+
* layer rendered ABOVE the backdrop (a pager, a zoom container) receives the
|
|
32
|
+
* press first and swallows it. Either that layer opts out (`pointerEvents
|
|
33
|
+
*="box-none"`) or it owns the dismiss itself — the backdrop being present is
|
|
34
|
+
* not enough.
|
|
35
|
+
*
|
|
36
|
+
* EXPO/EXPO-ROUTER APPS ONLY for `BloomProvider` — see `src/provider`; these
|
|
37
|
+
* two components are universal.
|
|
38
|
+
*
|
|
29
39
|
* Use `<OverlayRoot>` for the surface's outermost node and `<Backdrop>` for its
|
|
30
40
|
* dimming layer; do not re-implement either with raw `View`s.
|
|
31
41
|
*/
|
|
32
42
|
import { memo, type ReactNode } from 'react';
|
|
33
|
-
import {
|
|
43
|
+
import { BlurView } from 'expo-blur';
|
|
44
|
+
import {
|
|
45
|
+
Platform,
|
|
46
|
+
Pressable,
|
|
47
|
+
StyleSheet,
|
|
48
|
+
View,
|
|
49
|
+
type StyleProp,
|
|
50
|
+
type ViewStyle,
|
|
51
|
+
} from 'react-native';
|
|
34
52
|
|
|
35
53
|
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
36
54
|
|
|
55
|
+
/**
|
|
56
|
+
* One blur radius for every Bloom overlay. Surfaces differ in what they show,
|
|
57
|
+
* not in how the app behind them recedes.
|
|
58
|
+
*/
|
|
59
|
+
export const BACKDROP_BLUR_INTENSITY = 40;
|
|
60
|
+
/** Dim laid over the blur — the blur alone does not give enough contrast. */
|
|
61
|
+
export const BACKDROP_DIM_OPACITY = 0.45;
|
|
62
|
+
|
|
37
63
|
export interface OverlayRootProps {
|
|
38
64
|
children?: ReactNode;
|
|
39
65
|
style?: StyleProp<ViewStyle>;
|
|
@@ -61,22 +87,34 @@ export interface BackdropProps {
|
|
|
61
87
|
onPress?: () => void;
|
|
62
88
|
/** `true` keeps the dim but makes it inert — a blocking dialog, a busy state. */
|
|
63
89
|
disabled?: boolean;
|
|
64
|
-
/**
|
|
90
|
+
/** Blur radius behind the dim. `0` renders the dim alone. */
|
|
91
|
+
blurIntensity?: number;
|
|
92
|
+
/** Blur tint. Backdrops dim the app, so `dark` is the default on every theme. */
|
|
93
|
+
blurTint?: 'light' | 'dark' | 'default';
|
|
94
|
+
/** Dim colour over the blur. */
|
|
95
|
+
dimColor?: string;
|
|
96
|
+
/** Dim opacity, 0–1. */
|
|
97
|
+
dimOpacity?: number;
|
|
98
|
+
/** Extra style on the press target — animated opacity, insets, z-index. */
|
|
65
99
|
style?: StyleProp<ViewStyle>;
|
|
66
|
-
/** Rendered
|
|
100
|
+
/** Rendered ON TOP of the dim, inside the press target (Dialog's panel does this). */
|
|
67
101
|
children?: ReactNode;
|
|
68
102
|
accessibilityLabel?: string;
|
|
69
103
|
testID?: string;
|
|
70
104
|
}
|
|
71
105
|
|
|
72
106
|
/**
|
|
73
|
-
* Full-bleed
|
|
74
|
-
*
|
|
75
|
-
*
|
|
107
|
+
* Full-bleed blur + dim that dismisses the surface when pressed. Always takes
|
|
108
|
+
* pointer events (that is its whole job), so anything that must stay pressable
|
|
109
|
+
* goes INSIDE it as `children`, never as a sibling rendered over it.
|
|
76
110
|
*/
|
|
77
111
|
export const Backdrop = memo(function Backdrop({
|
|
78
112
|
onPress,
|
|
79
113
|
disabled = false,
|
|
114
|
+
blurIntensity = BACKDROP_BLUR_INTENSITY,
|
|
115
|
+
blurTint = 'dark',
|
|
116
|
+
dimColor = '#000',
|
|
117
|
+
dimOpacity = BACKDROP_DIM_OPACITY,
|
|
80
118
|
style,
|
|
81
119
|
children,
|
|
82
120
|
accessibilityLabel = 'Dismiss',
|
|
@@ -97,6 +135,21 @@ export const Backdrop = memo(function Backdrop({
|
|
|
97
135
|
testID={testID}
|
|
98
136
|
style={[StyleSheet.absoluteFill, style]}
|
|
99
137
|
>
|
|
138
|
+
{blurIntensity > 0 ? (
|
|
139
|
+
<BlurView
|
|
140
|
+
intensity={blurIntensity}
|
|
141
|
+
tint={blurTint}
|
|
142
|
+
// Android's default blur is a no-op on many devices; this is the
|
|
143
|
+
// implementation that actually renders there.
|
|
144
|
+
experimentalBlurMethod="dimezisBlurView"
|
|
145
|
+
pointerEvents="none"
|
|
146
|
+
style={StyleSheet.absoluteFill}
|
|
147
|
+
/>
|
|
148
|
+
) : null}
|
|
149
|
+
<View
|
|
150
|
+
pointerEvents="none"
|
|
151
|
+
style={[StyleSheet.absoluteFill, { backgroundColor: dimColor, opacity: dimOpacity }]}
|
|
152
|
+
/>
|
|
100
153
|
{children}
|
|
101
154
|
</Pressable>
|
|
102
155
|
);
|
package/src/provider/index.tsx
CHANGED
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* Nesting extra contexts costs nothing at runtime — the win is that scope is no
|
|
19
19
|
* longer a per-app decision.
|
|
20
20
|
*
|
|
21
|
+
* EXPO/EXPO-ROUTER APPS ONLY. The scroll-restoration provider it mounts imports
|
|
22
|
+
* `expo-router` (its web implementation keys offsets on the focused route), so
|
|
23
|
+
* a Vite/SPA consumer that has no expo-router cannot resolve this module. Those
|
|
24
|
+
* apps keep mounting `BloomThemeProvider` (and any other piece they need)
|
|
25
|
+
* directly — that is not a lesser path, just the one without a router.
|
|
26
|
+
*
|
|
21
27
|
* NOT included, on purpose — these are OUTLETS, not state, and their placement
|
|
22
28
|
* in the tree is a real app decision (z-order, safe areas, and mounting a
|
|
23
29
|
* second one duplicates every surface it renders):
|
|
@@ -72,8 +72,9 @@ import type {
|
|
|
72
72
|
ZoomableImageGalleryProps,
|
|
73
73
|
} from './types';
|
|
74
74
|
|
|
75
|
-
const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
|
|
76
75
|
const AnimatedImage = Animated.createAnimatedComponent(Image);
|
|
76
|
+
// The shared overlay backdrop, driven by the viewer's open/drag progress.
|
|
77
|
+
const AnimatedBackdrop = Animated.createAnimatedComponent(Backdrop);
|
|
77
78
|
|
|
78
79
|
// Web-only interaction hints (no-ops on native): the zoom surfaces are
|
|
79
80
|
// tap-to-dismiss (`cursor: pointer`) and must not select text or drag the
|
|
@@ -172,6 +173,8 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
172
173
|
// (`handleDismiss` runs via `runOnJS` and from `Pressable.onPress`, where the
|
|
173
174
|
// state closure can be stale). Kept in lockstep by `setActiveIndexBoth`.
|
|
174
175
|
const activeIndexRef = useRef(0);
|
|
176
|
+
// True from the first dismiss request until the viewer is actually unmounted.
|
|
177
|
+
const dismissingRef = useRef(false);
|
|
175
178
|
|
|
176
179
|
// Single writer for the current index: updates state (drives indicator + open
|
|
177
180
|
// image) and the synchronous mirror together, and only when it changes.
|
|
@@ -238,6 +241,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
238
241
|
// every dismiss path (fly-back and fade-out fallback).
|
|
239
242
|
const finalizeDismiss = useCallback(() => {
|
|
240
243
|
setIsOpen(false);
|
|
244
|
+
dismissingRef.current = false;
|
|
241
245
|
scale.value = 1;
|
|
242
246
|
translateX.value = 0;
|
|
243
247
|
translateY.value = 0;
|
|
@@ -286,6 +290,12 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
286
290
|
}, [finalizeDismiss, opacity, scale]);
|
|
287
291
|
|
|
288
292
|
const handleDismiss = useCallback(() => {
|
|
293
|
+
// A press on the image reaches BOTH its tap gesture and the page beneath it
|
|
294
|
+
// (which owns the dismiss for the empty area around the image), so this can
|
|
295
|
+
// fire twice for one click. Latch it: a second call would start a second
|
|
296
|
+
// fly-back from an already-moving image.
|
|
297
|
+
if (dismissingRef.current) return;
|
|
298
|
+
dismissingRef.current = true;
|
|
289
299
|
// Collapse the pager back to the single open-image so the fly-back animates
|
|
290
300
|
// one image (the current one) rather than the whole scrolled strip.
|
|
291
301
|
setPagerReady(false);
|
|
@@ -334,6 +344,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
334
344
|
const open = useCallback(
|
|
335
345
|
(nextImages: GalleryImage[], index: number, rect?: MeasuredRect) => {
|
|
336
346
|
if (isOpen || nextImages.length === 0) return;
|
|
347
|
+
dismissingRef.current = false;
|
|
337
348
|
const safeIndex = Math.min(Math.max(index, 0), nextImages.length - 1);
|
|
338
349
|
const target = nextImages[safeIndex];
|
|
339
350
|
if (!target) return;
|
|
@@ -753,18 +764,12 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
753
764
|
did nothing on web while Escape still closed. `Backdrop` opts back
|
|
754
765
|
in via the `pointerEvents` PROP, the only form that reaches the DOM
|
|
755
766
|
(see `src/overlay`). */}
|
|
756
|
-
<
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
>
|
|
763
|
-
<Animated.View
|
|
764
|
-
style={[StyleSheet.absoluteFill, { backgroundColor: theme.colors.overlay }, backdropStyle]}
|
|
765
|
-
/>
|
|
766
|
-
</AnimatedBlurView>
|
|
767
|
-
</Backdrop>
|
|
767
|
+
<AnimatedBackdrop
|
|
768
|
+
onPress={handleDismiss}
|
|
769
|
+
accessibilityLabel="Close image viewer"
|
|
770
|
+
dimColor={theme.colors.overlay}
|
|
771
|
+
style={backdropStyle}
|
|
772
|
+
/>
|
|
768
773
|
|
|
769
774
|
<GestureDetector gesture={panGesture}>
|
|
770
775
|
<Animated.View
|
|
@@ -817,8 +822,16 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
817
822
|
// through the gesture, so it isn't wrapped in a Pressable.
|
|
818
823
|
if (idx === activeIndex) {
|
|
819
824
|
return (
|
|
820
|
-
<
|
|
825
|
+
<Pressable
|
|
821
826
|
key={`${img.uri}-${idx}`}
|
|
827
|
+
// The page fills the screen ON TOP of the backdrop (the
|
|
828
|
+
// pager below it needs pointer events to swipe), so a
|
|
829
|
+
// press on the empty area around the image can never
|
|
830
|
+
// reach the backdrop — the page dismisses it itself.
|
|
831
|
+
// Same outcome as tapping the image, which already
|
|
832
|
+
// dismisses through `singleTapDismissGesture`.
|
|
833
|
+
onPress={handleDismiss}
|
|
834
|
+
accessibilityLabel="Close image viewer"
|
|
822
835
|
style={[styles.page, { width: SCREEN_WIDTH, height: SCREEN_HEIGHT }, webPointerStyle]}
|
|
823
836
|
>
|
|
824
837
|
<GestureDetector gesture={activePageGesture}>
|
|
@@ -833,12 +846,14 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
833
846
|
{...(Platform.OS === 'web' ? { draggable: false } : {})}
|
|
834
847
|
/>
|
|
835
848
|
</GestureDetector>
|
|
836
|
-
</
|
|
849
|
+
</Pressable>
|
|
837
850
|
);
|
|
838
851
|
}
|
|
839
852
|
return (
|
|
840
|
-
<
|
|
853
|
+
<Pressable
|
|
841
854
|
key={`${img.uri}-${idx}`}
|
|
855
|
+
onPress={handleDismiss}
|
|
856
|
+
accessibilityLabel="Close image viewer"
|
|
842
857
|
style={[styles.page, { width: SCREEN_WIDTH, height: SCREEN_HEIGHT }]}
|
|
843
858
|
>
|
|
844
859
|
<Image
|
|
@@ -848,7 +863,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
848
863
|
transition={0}
|
|
849
864
|
{...(Platform.OS === 'web' ? { draggable: false } : {})}
|
|
850
865
|
/>
|
|
851
|
-
</
|
|
866
|
+
</Pressable>
|
|
852
867
|
);
|
|
853
868
|
})}
|
|
854
869
|
</ScrollView>
|