@oxyhq/bloom 0.60.0 → 0.62.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 +24 -9
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/dialog/Dialog.web.js +14 -8
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.js +18 -1
- package/lib/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.js.map +1 -1
- package/lib/commonjs/image-aspect-ratio-cache/index.js +6 -0
- package/lib/commonjs/image-aspect-ratio-cache/index.js.map +1 -1
- package/lib/commonjs/overlay/index.js +33 -19
- package/lib/commonjs/overlay/index.js.map +1 -1
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +17 -10
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +25 -10
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/dialog/Dialog.web.js +14 -8
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/image-aspect-ratio-cache/imageAspectRatioCache.js +16 -0
- package/lib/module/image-aspect-ratio-cache/imageAspectRatioCache.js.map +1 -1
- package/lib/module/image-aspect-ratio-cache/index.js +1 -1
- package/lib/module/image-aspect-ratio-cache/index.js.map +1 -1
- package/lib/module/overlay/index.js +33 -21
- package/lib/module/overlay/index.js.map +1 -1
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +18 -11
- 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/image-aspect-ratio-cache/imageAspectRatioCache.d.ts +12 -0
- package/lib/typescript/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.d.ts.map +1 -1
- package/lib/typescript/commonjs/image-aspect-ratio-cache/index.d.ts +2 -1
- package/lib/typescript/commonjs/image-aspect-ratio-cache/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/overlay/index.d.ts +21 -3
- package/lib/typescript/commonjs/overlay/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/image-aspect-ratio-cache/imageAspectRatioCache.d.ts +12 -0
- package/lib/typescript/module/image-aspect-ratio-cache/imageAspectRatioCache.d.ts.map +1 -1
- package/lib/typescript/module/image-aspect-ratio-cache/index.d.ts +2 -1
- package/lib/typescript/module/image-aspect-ratio-cache/index.d.ts.map +1 -1
- package/lib/typescript/module/overlay/index.d.ts +21 -3
- package/lib/typescript/module/overlay/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__/imageIntrinsicSize.test.ts +47 -0
- package/src/__tests__/overlay-pointer-events.test.tsx +17 -16
- package/src/bottom-sheet/BottomSheetBase.tsx +24 -9
- package/src/dialog/Dialog.web.tsx +16 -18
- package/src/image-aspect-ratio-cache/imageAspectRatioCache.ts +18 -0
- package/src/image-aspect-ratio-cache/index.ts +2 -0
- package/src/overlay/index.tsx +59 -30
- package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +25 -10
package/src/overlay/index.tsx
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* Use `<OverlayRoot>` for the surface's outermost node and `<Backdrop>` for its
|
|
40
40
|
* dimming layer; do not re-implement either with raw `View`s.
|
|
41
41
|
*/
|
|
42
|
-
import {
|
|
42
|
+
import { memo, type ReactNode } from 'react';
|
|
43
43
|
import { BlurView } from 'expo-blur';
|
|
44
44
|
import {
|
|
45
45
|
Platform,
|
|
@@ -49,9 +49,12 @@ import {
|
|
|
49
49
|
type StyleProp,
|
|
50
50
|
type ViewStyle,
|
|
51
51
|
} from 'react-native';
|
|
52
|
+
import Animated, { useAnimatedStyle, type SharedValue } from 'react-native-reanimated';
|
|
52
53
|
|
|
53
54
|
import { WEB_POSITION_FIXED } from '../styles/web-view-style';
|
|
54
55
|
|
|
56
|
+
const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
|
|
57
|
+
|
|
55
58
|
/**
|
|
56
59
|
* One blur radius for every Bloom overlay. Surfaces differ in what they show,
|
|
57
60
|
* not in how the app behind them recedes.
|
|
@@ -85,6 +88,12 @@ OverlayRoot.displayName = 'OverlayRoot';
|
|
|
85
88
|
export interface BackdropProps {
|
|
86
89
|
/** Dismiss handler. Omit (or pass `disabled`) for a backdrop that only dims. */
|
|
87
90
|
onPress?: () => void;
|
|
91
|
+
/**
|
|
92
|
+
* Fade driver, 0 (hidden) → 1 (fully shown). The backdrop applies it to its
|
|
93
|
+
* OWN layers — see the note on `style` for why the caller must not animate
|
|
94
|
+
* opacity from the outside.
|
|
95
|
+
*/
|
|
96
|
+
progress?: SharedValue<number>;
|
|
88
97
|
/** `true` keeps the dim but makes it inert — a blocking dialog, a busy state. */
|
|
89
98
|
disabled?: boolean;
|
|
90
99
|
/** Blur radius behind the dim. `0` renders the dim alone. */
|
|
@@ -95,8 +104,19 @@ export interface BackdropProps {
|
|
|
95
104
|
dimColor?: string;
|
|
96
105
|
/** Dim opacity, 0–1. */
|
|
97
106
|
dimOpacity?: number;
|
|
98
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Geometry for the press target: insets, z-index, layout. NOT opacity —
|
|
109
|
+
* `backdrop-filter` samples nothing under an ancestor with `opacity < 1`
|
|
110
|
+
* (the group composites in isolation), so a fade applied here silently kills
|
|
111
|
+
* the blur. An `opacity` found in this style is redirected onto the layers;
|
|
112
|
+
* animate the fade through `progress` instead.
|
|
113
|
+
*/
|
|
99
114
|
style?: StyleProp<ViewStyle>;
|
|
115
|
+
/**
|
|
116
|
+
* Extra style for the blur + dim LAYERS, for fades a shared value can't
|
|
117
|
+
* express — the web dialog's CSS keyframes, for instance.
|
|
118
|
+
*/
|
|
119
|
+
layerStyle?: StyleProp<ViewStyle>;
|
|
100
120
|
/** Rendered ON TOP of the dim, inside the press target (Dialog's panel does this). */
|
|
101
121
|
children?: ReactNode;
|
|
102
122
|
accessibilityLabel?: string;
|
|
@@ -108,31 +128,40 @@ export interface BackdropProps {
|
|
|
108
128
|
* pointer events (that is its whole job), so anything that must stay pressable
|
|
109
129
|
* goes INSIDE it as `children`, never as a sibling rendered over it.
|
|
110
130
|
*/
|
|
111
|
-
export const Backdrop = memo(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
) {
|
|
131
|
+
export const Backdrop = memo(function Backdrop({
|
|
132
|
+
onPress,
|
|
133
|
+
progress,
|
|
134
|
+
disabled = false,
|
|
135
|
+
blurIntensity = BACKDROP_BLUR_INTENSITY,
|
|
136
|
+
blurTint = 'dark',
|
|
137
|
+
dimColor = '#000',
|
|
138
|
+
dimOpacity = BACKDROP_DIM_OPACITY,
|
|
139
|
+
style,
|
|
140
|
+
layerStyle,
|
|
141
|
+
children,
|
|
142
|
+
accessibilityLabel = 'Dismiss',
|
|
143
|
+
testID,
|
|
144
|
+
}: BackdropProps) {
|
|
126
145
|
const inert = disabled || !onPress;
|
|
146
|
+
|
|
147
|
+
// An `opacity` handed in through `style` would sit on the ancestor of the
|
|
148
|
+
// blur and kill it; hoist it onto the layers instead of honouring it there.
|
|
149
|
+
const flat: ViewStyle = StyleSheet.flatten(style) ?? {};
|
|
150
|
+
const { opacity: styleOpacity, ...rootStyle } = flat;
|
|
151
|
+
const staticOpacity = typeof styleOpacity === 'number' ? styleOpacity : 1;
|
|
152
|
+
|
|
153
|
+
// The fade lives on each LAYER, never on their shared ancestor.
|
|
154
|
+
const blurFade = useAnimatedStyle(
|
|
155
|
+
() => ({ opacity: (progress ? progress.value : 1) * staticOpacity }),
|
|
156
|
+
[progress, staticOpacity],
|
|
157
|
+
);
|
|
158
|
+
const dimFade = useAnimatedStyle(
|
|
159
|
+
() => ({ opacity: (progress ? progress.value : 1) * staticOpacity * dimOpacity }),
|
|
160
|
+
[progress, staticOpacity, dimOpacity],
|
|
161
|
+
);
|
|
162
|
+
|
|
127
163
|
return (
|
|
128
164
|
<Pressable
|
|
129
|
-
// MUST forward the ref: surfaces animate the backdrop through
|
|
130
|
-
// `Animated.createAnimatedComponent(Backdrop)`, and reanimated applies
|
|
131
|
-
// those updates by reaching the underlying host view. With the ref
|
|
132
|
-
// swallowed the animated style never lands, so a backdrop whose opacity
|
|
133
|
-
// is animated from 0 stays at 0 — fully transparent, with the surface
|
|
134
|
-
// floating over an undimmed app.
|
|
135
|
-
ref={ref}
|
|
136
165
|
pointerEvents="auto"
|
|
137
166
|
onPress={inert ? undefined : onPress}
|
|
138
167
|
disabled={inert}
|
|
@@ -143,27 +172,27 @@ export const Backdrop = memo(forwardRef<View, BackdropProps>(function Backdrop(
|
|
|
143
172
|
accessibilityRole={inert ? undefined : 'button'}
|
|
144
173
|
accessibilityLabel={inert ? undefined : accessibilityLabel}
|
|
145
174
|
testID={testID}
|
|
146
|
-
style={[StyleSheet.absoluteFill,
|
|
175
|
+
style={[StyleSheet.absoluteFill, rootStyle]}
|
|
147
176
|
>
|
|
148
177
|
{blurIntensity > 0 ? (
|
|
149
|
-
<
|
|
178
|
+
<AnimatedBlurView
|
|
150
179
|
intensity={blurIntensity}
|
|
151
180
|
tint={blurTint}
|
|
152
181
|
// Android's default blur is a no-op on many devices; this is the
|
|
153
182
|
// implementation that actually renders there.
|
|
154
183
|
experimentalBlurMethod="dimezisBlurView"
|
|
155
184
|
pointerEvents="none"
|
|
156
|
-
style={StyleSheet.absoluteFill}
|
|
185
|
+
style={[StyleSheet.absoluteFill, layerStyle, blurFade]}
|
|
157
186
|
/>
|
|
158
187
|
) : null}
|
|
159
|
-
<View
|
|
188
|
+
<Animated.View
|
|
160
189
|
pointerEvents="none"
|
|
161
|
-
style={[StyleSheet.absoluteFill, { backgroundColor: dimColor,
|
|
190
|
+
style={[StyleSheet.absoluteFill, { backgroundColor: dimColor }, layerStyle, dimFade]}
|
|
162
191
|
/>
|
|
163
192
|
{children}
|
|
164
193
|
</Pressable>
|
|
165
194
|
);
|
|
166
|
-
})
|
|
195
|
+
});
|
|
167
196
|
|
|
168
197
|
Backdrop.displayName = 'Backdrop';
|
|
169
198
|
|
|
@@ -40,6 +40,7 @@ import { Z_INDEX } from '../styles/z-index';
|
|
|
40
40
|
import { WEB_POSITION_FIXED, type WebCssStyle } from '../styles/web-view-style';
|
|
41
41
|
import {
|
|
42
42
|
getAspectRatio,
|
|
43
|
+
getIntrinsicSize,
|
|
43
44
|
fetchAspectRatio,
|
|
44
45
|
DEFAULT_ASPECT_RATIO,
|
|
45
46
|
} from '../image-aspect-ratio-cache';
|
|
@@ -73,8 +74,6 @@ import type {
|
|
|
73
74
|
} from './types';
|
|
74
75
|
|
|
75
76
|
const AnimatedImage = Animated.createAnimatedComponent(Image);
|
|
76
|
-
// The shared overlay backdrop, driven by the viewer's open/drag progress.
|
|
77
|
-
const AnimatedBackdrop = Animated.createAnimatedComponent(Backdrop);
|
|
78
77
|
|
|
79
78
|
// Web-only interaction hints (no-ops on native): the zoom surfaces are
|
|
80
79
|
// tap-to-dismiss (`cursor: pointer`) and must not select text or drag the
|
|
@@ -216,9 +215,12 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
216
215
|
[SCREEN_WIDTH, SCREEN_HEIGHT]
|
|
217
216
|
);
|
|
218
217
|
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
218
|
+
// Largest width/height for `ratio` that fits inside `fitBox` without cropping
|
|
219
|
+
// (contain), never exceeding the image's own resolution: filling the viewport
|
|
220
|
+
// with a 512px avatar or a small thumbnail just renders it soft. `uri` is what
|
|
221
|
+
// lets us look the intrinsic size up; without it (not measured yet) the fit
|
|
222
|
+
// box wins and the clamp applies on the next frame.
|
|
223
|
+
const fitForRatio = useCallback((ratio: number, uri?: string): FittedSize => {
|
|
222
224
|
const safeRatio = ratio > 0 && Number.isFinite(ratio) ? ratio : DEFAULT_ASPECT_RATIO;
|
|
223
225
|
let width = fitBox.width;
|
|
224
226
|
let height = width / safeRatio;
|
|
@@ -226,6 +228,11 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
226
228
|
height = fitBox.height;
|
|
227
229
|
width = height * safeRatio;
|
|
228
230
|
}
|
|
231
|
+
const intrinsic = uri ? getIntrinsicSize(uri) : undefined;
|
|
232
|
+
if (intrinsic && intrinsic.width > 0 && width > intrinsic.width) {
|
|
233
|
+
width = intrinsic.width;
|
|
234
|
+
height = width / safeRatio;
|
|
235
|
+
}
|
|
229
236
|
return { width, height };
|
|
230
237
|
}, [fitBox]);
|
|
231
238
|
|
|
@@ -234,7 +241,11 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
234
241
|
// after swiping it tracks the viewed image so the collapse-on-dismiss renders
|
|
235
242
|
// and flies back the image actually on screen.
|
|
236
243
|
const activeRatio = pageRatios[activeIndex] ?? openRatio;
|
|
237
|
-
const
|
|
244
|
+
const activeUri = images[activeIndex]?.uri;
|
|
245
|
+
const activeFit = useMemo(
|
|
246
|
+
() => fitForRatio(activeRatio, activeUri),
|
|
247
|
+
[fitForRatio, activeRatio, activeUri],
|
|
248
|
+
);
|
|
238
249
|
|
|
239
250
|
// Alt text (accessibility description) of the image currently on screen, shown
|
|
240
251
|
// as a caption at the bottom of the viewer (Bluesky-style lightbox footer).
|
|
@@ -391,7 +402,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
391
402
|
// animation grows it from the thumbnail's footprint (scale < 1) up to 1,
|
|
392
403
|
// mirroring the avatar's small→big scale. The initial scale is the ratio
|
|
393
404
|
// of the thumbnail width to the fitted width.
|
|
394
|
-
const fitted = fitForRatio(ratio);
|
|
405
|
+
const fitted = fitForRatio(ratio, target.uri);
|
|
395
406
|
const centerX = SCREEN_WIDTH / 2;
|
|
396
407
|
const centerY = SCREEN_HEIGHT / 2;
|
|
397
408
|
if (rect && rect.width > 0) {
|
|
@@ -778,11 +789,15 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
778
789
|
did nothing on web while Escape still closed. `Backdrop` opts back
|
|
779
790
|
in via the `pointerEvents` PROP, the only form that reaches the DOM
|
|
780
791
|
(see `src/overlay`). */}
|
|
781
|
-
|
|
792
|
+
{/* `progress` rather than an animated style: an opacity on the
|
|
793
|
+
backdrop's root would sit ABOVE the blur layer and neutralise it —
|
|
794
|
+
`backdrop-filter` samples nothing under an ancestor that composites
|
|
795
|
+
in isolation. */}
|
|
796
|
+
<Backdrop
|
|
782
797
|
onPress={handleDismiss}
|
|
783
798
|
accessibilityLabel="Close image viewer"
|
|
784
799
|
dimColor={theme.colors.overlay}
|
|
785
|
-
|
|
800
|
+
progress={opacity}
|
|
786
801
|
/>
|
|
787
802
|
|
|
788
803
|
<GestureDetector gesture={panGesture}>
|
|
@@ -829,7 +844,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
|
|
|
829
844
|
>
|
|
830
845
|
{images.map((img, idx) => {
|
|
831
846
|
const ratio = pageRatios[idx] ?? getAspectRatio(img.uri) ?? DEFAULT_ASPECT_RATIO;
|
|
832
|
-
const fit = fitForRatio(ratio);
|
|
847
|
+
const fit = fitForRatio(ratio, img.uri);
|
|
833
848
|
// Only the active page is zoomable + gesture-wrapped (double-tap
|
|
834
849
|
// / single-tap dismiss / pinch / pan). Off-screen pages stay a
|
|
835
850
|
// plain image; the active image handles its own tap-to-dismiss
|