@oxyhq/bloom 0.61.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.
Files changed (32) hide show
  1. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +24 -9
  2. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  3. package/lib/commonjs/dialog/Dialog.web.js +14 -8
  4. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  5. package/lib/commonjs/overlay/index.js +33 -19
  6. package/lib/commonjs/overlay/index.js.map +1 -1
  7. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +2 -4
  8. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
  9. package/lib/module/bottom-sheet/BottomSheetBase.js +25 -10
  10. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  11. package/lib/module/dialog/Dialog.web.js +14 -8
  12. package/lib/module/dialog/Dialog.web.js.map +1 -1
  13. package/lib/module/overlay/index.js +33 -21
  14. package/lib/module/overlay/index.js.map +1 -1
  15. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +2 -4
  16. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
  17. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  18. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  19. package/lib/typescript/commonjs/overlay/index.d.ts +21 -3
  20. package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
  21. package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
  22. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  23. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  24. package/lib/typescript/module/overlay/index.d.ts +21 -3
  25. package/lib/typescript/module/overlay/index.d.ts.map +1 -1
  26. package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/__tests__/overlay-pointer-events.test.tsx +17 -16
  29. package/src/bottom-sheet/BottomSheetBase.tsx +24 -9
  30. package/src/dialog/Dialog.web.tsx +16 -18
  31. package/src/overlay/index.tsx +59 -30
  32. package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +6 -4
@@ -20,6 +20,7 @@ import Animated, {
20
20
  type SharedValue,
21
21
  useAnimatedScrollHandler,
22
22
  useAnimatedStyle,
23
+ useDerivedValue,
23
24
  useSharedValue,
24
25
  withSpring,
25
26
  withTiming,
@@ -199,8 +200,6 @@ export interface BottomSheetBaseProps extends BottomSheetProps {
199
200
  Shell: React.ComponentType<BottomSheetShellProps>;
200
201
  }
201
202
 
202
- const AnimatedBackdrop = Animated.createAnimatedComponent(Backdrop);
203
-
204
203
  export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: React.ForwardedRef<BottomSheetRef>) => {
205
204
  const {
206
205
  Shell,
@@ -642,7 +641,11 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
642
641
  // dim opacity applies smoothly across the animation. When
643
642
  // `dynamicBackdrop` is enabled, the dim also fades proportionally with
644
643
  // drag distance (iOS Photos style).
645
- const backdropStyle = useAnimatedStyle(() => {
644
+ // A VALUE, not a style: `Backdrop` applies it to its blur and dim layers
645
+ // itself. Handing it over as an animated style would put the opacity on the
646
+ // layers' shared ancestor, which composites the group in isolation and
647
+ // leaves `backdrop-filter` with nothing behind it to blur.
648
+ const backdropProgress = useDerivedValue(() => {
646
649
  const dragFactor = dynamicBackdrop
647
650
  ? interpolate(
648
651
  translateY.value,
@@ -651,11 +654,17 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
651
654
  'clamp',
652
655
  )
653
656
  : 1;
654
- return {
655
- opacity: opacity.value * backdropOpacity * dragFactor,
656
- };
657
+ return opacity.value * backdropOpacity * dragFactor;
657
658
  }, [backdropOpacity, dynamicBackdrop, opacity, translateY, screenHeightSV]);
658
659
 
660
+ // Only the consumer-supplied `backdropComponent` needs the progress as a
661
+ // style — it owns its own visuals, blur included, so the ancestor-opacity
662
+ // hazard is its call to make.
663
+ const customBackdropStyle = useAnimatedStyle(
664
+ () => ({ opacity: backdropProgress.value }),
665
+ [backdropProgress],
666
+ );
667
+
659
668
  const sheetStyle = useAnimatedStyle(() => {
660
669
  const scale = interpolate(translateY.value, [0, screenHeightSV.value], [1, 0.95]);
661
670
  return {
@@ -787,7 +796,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
787
796
  <Shell visible={rendered} onRequestClose={dismiss} keyboardHeight={keyboardHeight}>
788
797
  <View style={StyleSheet.absoluteFill}>
789
798
  {backdropComponent ? (
790
- <Animated.View style={[StyleSheet.absoluteFill, backdropStyle]}>
799
+ <Animated.View style={[StyleSheet.absoluteFill, customBackdropStyle]}>
791
800
  {backdropComponent({ onPress: handleBackdropPress })}
792
801
  </Animated.View>
793
802
  ) : (
@@ -795,10 +804,16 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
795
804
  // `backdropStyle` drives the fade; the dim level rides on
796
805
  // `backdropOpacity`, so the shared component's own dim is
797
806
  // switched off here to keep a single source of dimming.
798
- <AnimatedBackdrop
807
+ <Backdrop
799
808
  onPress={handleBackdropPress}
809
+ // `backdropProgress` already folds in `backdropOpacity`
810
+ // and the drag factor, so the dim rides at full weight.
811
+ // It goes through `progress` rather than an animated
812
+ // style because an opacity on the root would sit above
813
+ // the blur layer and neutralise it.
800
814
  dimOpacity={1}
801
- style={[styles.backdrop, backdropStyle]}
815
+ progress={backdropProgress}
816
+ style={styles.backdrop}
802
817
  />
803
818
  )}
804
819
 
@@ -332,16 +332,17 @@ function CenterOrSideDialog({
332
332
  disabled={!dismissOnBackdrop}
333
333
  accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
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
+ // The fade rides on the LAYERS, never on the press target: an
336
+ // opacity animation on the blur's ancestor composites the group in
337
+ // isolation and leaves `backdrop-filter` nothing to sample.
338
+ layerStyle={isClosing ? BACKDROP_FADE_OUT : BACKDROP_FADE_IN}
339
+ style={{
340
+ position: WEB_POSITION_FIXED,
341
+ zIndex: dialogZIndex.backdrop,
342
+ alignItems: 'center',
343
+ justifyContent: 'center',
344
+ paddingHorizontal: 20,
345
+ }}
345
346
  >
346
347
  <DialogPanel
347
348
  testID={testID}
@@ -702,14 +703,11 @@ function SheetSurface({
702
703
  accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
703
704
  onPress={handleBackdropPress}
704
705
  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}
708
- style={[
709
- sheetStyles.backdrop,
710
- backdropTransition,
711
- { opacity: visible ? SHEET_BACKDROP_OPACITY : 0 },
712
- ]}
706
+ // Same reason as the centred dialog: the transition and the opacity it
707
+ // animates belong to the layers, not to the press target above them.
708
+ dimOpacity={SHEET_BACKDROP_OPACITY}
709
+ layerStyle={backdropTransition}
710
+ style={[sheetStyles.backdrop, { opacity: visible ? 1 : 0 }]}
713
711
  />
714
712
 
715
713
  <View
@@ -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 { forwardRef, memo, type ReactNode } from 'react';
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
- /** Extra style on the press target — animated opacity, insets, z-index. */
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(forwardRef<View, BackdropProps>(function Backdrop(
112
- {
113
- onPress,
114
- disabled = false,
115
- blurIntensity = BACKDROP_BLUR_INTENSITY,
116
- blurTint = 'dark',
117
- dimColor = '#000',
118
- dimOpacity = BACKDROP_DIM_OPACITY,
119
- style,
120
- children,
121
- accessibilityLabel = 'Dismiss',
122
- testID,
123
- },
124
- ref,
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, style]}
175
+ style={[StyleSheet.absoluteFill, rootStyle]}
147
176
  >
148
177
  {blurIntensity > 0 ? (
149
- <BlurView
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, opacity: dimOpacity }]}
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
 
@@ -74,8 +74,6 @@ import type {
74
74
  } from './types';
75
75
 
76
76
  const AnimatedImage = Animated.createAnimatedComponent(Image);
77
- // The shared overlay backdrop, driven by the viewer's open/drag progress.
78
- const AnimatedBackdrop = Animated.createAnimatedComponent(Backdrop);
79
77
 
80
78
  // Web-only interaction hints (no-ops on native): the zoom surfaces are
81
79
  // tap-to-dismiss (`cursor: pointer`) and must not select text or drag the
@@ -791,11 +789,15 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
791
789
  did nothing on web while Escape still closed. `Backdrop` opts back
792
790
  in via the `pointerEvents` PROP, the only form that reaches the DOM
793
791
  (see `src/overlay`). */}
794
- <AnimatedBackdrop
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
795
797
  onPress={handleDismiss}
796
798
  accessibilityLabel="Close image viewer"
797
799
  dimColor={theme.colors.overlay}
798
- style={backdropStyle}
800
+ progress={opacity}
799
801
  />
800
802
 
801
803
  <GestureDetector gesture={panGesture}>