@oxyhq/bloom 0.59.0 → 0.59.2

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 (69) hide show
  1. package/README.md +4 -2
  2. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +16 -17
  3. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  4. package/lib/commonjs/dialog/Dialog.js +1 -5
  5. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  6. package/lib/commonjs/dialog/DialogBottomSheet.js +0 -4
  7. package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
  8. package/lib/commonjs/dialog/DialogContent.js +15 -4
  9. package/lib/commonjs/dialog/DialogContent.js.map +1 -1
  10. package/lib/commonjs/dialog/SheetShell.js +1 -2
  11. package/lib/commonjs/dialog/SheetShell.js.map +1 -1
  12. package/lib/commonjs/overlay/blur-target.js +53 -0
  13. package/lib/commonjs/overlay/blur-target.js.map +1 -0
  14. package/lib/commonjs/overlay/index.js +6 -4
  15. package/lib/commonjs/overlay/index.js.map +1 -1
  16. package/lib/commonjs/provider/index.js +4 -6
  17. package/lib/commonjs/provider/index.js.map +1 -1
  18. package/lib/module/bottom-sheet/BottomSheetBase.js +17 -18
  19. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  20. package/lib/module/dialog/Dialog.js +1 -5
  21. package/lib/module/dialog/Dialog.js.map +1 -1
  22. package/lib/module/dialog/DialogBottomSheet.js +1 -5
  23. package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
  24. package/lib/module/dialog/DialogContent.js +16 -5
  25. package/lib/module/dialog/DialogContent.js.map +1 -1
  26. package/lib/module/dialog/SheetShell.js +1 -2
  27. package/lib/module/dialog/SheetShell.js.map +1 -1
  28. package/lib/module/overlay/blur-target.js +48 -0
  29. package/lib/module/overlay/blur-target.js.map +1 -0
  30. package/lib/module/overlay/index.js +6 -4
  31. package/lib/module/overlay/index.js.map +1 -1
  32. package/lib/module/provider/index.js +4 -6
  33. package/lib/module/provider/index.js.map +1 -1
  34. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/dialog/DialogContent.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/dialog/SheetShell.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/dialog/types.d.ts +5 -0
  40. package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/overlay/blur-target.d.ts +21 -0
  42. package/lib/typescript/commonjs/overlay/blur-target.d.ts.map +1 -0
  43. package/lib/typescript/commonjs/overlay/index.d.ts +4 -2
  44. package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/provider/index.d.ts.map +1 -1
  46. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  47. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  48. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
  49. package/lib/typescript/module/dialog/DialogContent.d.ts.map +1 -1
  50. package/lib/typescript/module/dialog/SheetShell.d.ts.map +1 -1
  51. package/lib/typescript/module/dialog/types.d.ts +5 -0
  52. package/lib/typescript/module/dialog/types.d.ts.map +1 -1
  53. package/lib/typescript/module/overlay/blur-target.d.ts +21 -0
  54. package/lib/typescript/module/overlay/blur-target.d.ts.map +1 -0
  55. package/lib/typescript/module/overlay/index.d.ts +4 -2
  56. package/lib/typescript/module/overlay/index.d.ts.map +1 -1
  57. package/lib/typescript/module/provider/index.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/__tests__/BottomSheet.test.tsx +46 -1
  60. package/src/__tests__/Dialog.test.tsx +33 -1
  61. package/src/bottom-sheet/BottomSheetBase.tsx +16 -17
  62. package/src/dialog/Dialog.tsx +1 -4
  63. package/src/dialog/DialogBottomSheet.tsx +0 -4
  64. package/src/dialog/DialogContent.tsx +16 -6
  65. package/src/dialog/SheetShell.tsx +1 -2
  66. package/src/dialog/types.ts +5 -0
  67. package/src/overlay/blur-target.tsx +45 -0
  68. package/src/overlay/index.tsx +9 -4
  69. package/src/provider/index.tsx +14 -11
@@ -25,7 +25,8 @@ 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
+ import { Backdrop, BACKDROP_DIM_OPACITY } from '../overlay';
29
+ import { useBloomBlurTarget } from '../overlay/blur-target';
29
30
  import { useTheme } from '../theme/use-theme';
30
31
 
31
32
  /** Hook that returns current screen dimensions and updates on rotation/resize. */
@@ -215,7 +216,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
215
216
  onDismissAttempt,
216
217
  detached = false,
217
218
  showHandle = true,
218
- backdropOpacity = 0.5,
219
+ backdropOpacity = BACKDROP_DIM_OPACITY,
219
220
  scrollable = true,
220
221
  manualActivation = false,
221
222
  dynamicBackdrop = false,
@@ -226,6 +227,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
226
227
  } = props;
227
228
 
228
229
  const insets = useSafeAreaInsets();
230
+ const blurTarget = useBloomBlurTarget();
229
231
  const theme = useTheme();
230
232
  const { colors } = theme;
231
233
  const { height: screenHeight } = useScreenDimensions();
@@ -637,11 +639,11 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
637
639
  // Native (with the plugin) auto-tracks and ignores the extra deps, so this
638
640
  // is safe on both platforms. Do NOT strip the shared values from these deps.
639
641
  //
640
- // `opacity.value` drives the fade in/out (0 -> 1). `backdropOpacity` is the
641
- // final dim level once fully visible. We multiply so the consumer-provided
642
- // dim opacity applies smoothly across the animation. When
643
- // `dynamicBackdrop` is enabled, the dim also fades proportionally with
644
- // drag distance (iOS Photos style).
642
+ // `opacity.value` drives the whole blur+dim layer in/out (0 -> 1).
643
+ // `backdropOpacity` belongs ONLY to Backdrop's black dim layer below; if it
644
+ // is multiplied into this group opacity instead, the opaque black child
645
+ // hides the BlurView and Android looks like a plain dark scrim.
646
+ // `dynamicBackdrop` fades the composed blur+dim proportionally with drag.
645
647
  const backdropStyle = useAnimatedStyle(() => {
646
648
  const dragFactor = dynamicBackdrop
647
649
  ? interpolate(
@@ -652,9 +654,9 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
652
654
  )
653
655
  : 1;
654
656
  return {
655
- opacity: opacity.value * backdropOpacity * dragFactor,
657
+ opacity: opacity.value * dragFactor,
656
658
  };
657
- }, [backdropOpacity, dynamicBackdrop, opacity, translateY, screenHeightSV]);
659
+ }, [dynamicBackdrop, opacity, translateY, screenHeightSV]);
658
660
 
659
661
  const sheetStyle = useAnimatedStyle(() => {
660
662
  const scale = interpolate(translateY.value, [0, screenHeightSV.value], [1, 0.95]);
@@ -792,12 +794,12 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
792
794
  </Animated.View>
793
795
  ) : (
794
796
  // 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.
797
+ // `backdropStyle` drives the entrance/drag fade; the black
798
+ // dim stays translucent so the BlurView remains visible.
798
799
  <AnimatedBackdrop
799
800
  onPress={handleBackdropPress}
800
- dimOpacity={1}
801
+ dimOpacity={backdropOpacity}
802
+ blurTarget={blurTarget ?? undefined}
801
803
  style={[styles.backdrop, backdropStyle]}
802
804
  />
803
805
  )}
@@ -827,10 +829,7 @@ BottomSheetBase.displayName = 'BottomSheetBase';
827
829
 
828
830
  const styles = StyleSheet.create({
829
831
  backdrop: {
830
- // Color is solid black; the dim level is driven by `backdropOpacity` via
831
- // the animated `opacity` style on the wrapping <Animated.View>.
832
832
  flex: 1,
833
- backgroundColor: '#000',
834
833
  },
835
834
  backdropTouchable: {
836
835
  flex: 1,
@@ -846,7 +845,7 @@ const styles = StyleSheet.create({
846
845
  sheetDetached: {
847
846
  left: 16,
848
847
  right: 16,
849
- borderRadius: 24,
848
+ borderRadius: 28,
850
849
  },
851
850
  sheetNormal: {
852
851
  left: 0,
@@ -251,7 +251,7 @@ function CenteredOrSideDialog({
251
251
  // All four corners rounded — bloom's BottomSheet defaults to top-only
252
252
  // radius in flush mode, but we use `detached` so the whole card is
253
253
  // floating and rounded uniformly.
254
- borderRadius: 20,
254
+ borderRadius: 28,
255
255
  },
256
256
  // Drives the card's `height` only while a morph is in flight; at rest it
257
257
  // resolves to `height: 'auto'` — the card's normal content sizing.
@@ -344,9 +344,6 @@ function CenteredOrSideDialog({
344
344
  />
345
345
  ) : undefined
346
346
  }
347
- // Stronger dim when a Dialog is stacked over another sheet so the
348
- // underlying sheet's handle/content doesn't bleed through.
349
- backdropOpacity={0.7}
350
347
  // Measures the floating card — the size a morph starts from.
351
348
  onLayout={morphState.onPanelLayout}
352
349
  style={sheetStyle}
@@ -30,7 +30,6 @@ import {
30
30
  DEFAULT_DIALOG_CONTENT_PADDING,
31
31
  DEFAULT_MAX_HEIGHT_RATIO,
32
32
  PANEL_RADIUS,
33
- SHEET_BACKDROP_OPACITY,
34
33
  } from './placement';
35
34
  import type { DialogControlProps, DialogProps } from './types';
36
35
 
@@ -284,9 +283,6 @@ export function DialogBottomSheet({
284
283
  />
285
284
  ) : undefined
286
285
  }
287
- // Stronger dim than a lone sheet so an underlying sheet's handle/content
288
- // doesn't bleed through when a Dialog is stacked over one.
289
- backdropOpacity={SHEET_BACKDROP_OPACITY + 0.3}
290
286
  // Measures the sheet card — the size a morph starts from.
291
287
  onLayout={morphState.onPanelLayout}
292
288
  style={sheetStyle}
@@ -1,5 +1,6 @@
1
1
  import React, { useCallback } from 'react';
2
2
  import {
3
+ ActivityIndicator,
3
4
  Text,
4
5
  TouchableOpacity,
5
6
  View,
@@ -91,11 +92,13 @@ function ActionButton({ action }: { action: DialogAction }) {
91
92
  const theme = useTheme();
92
93
  const color: DialogActionColor = action.color ?? 'default';
93
94
  const shouldCloseOnPress = action.shouldCloseOnPress ?? true;
95
+ const inactive = action.disabled === true || action.loading === true;
94
96
 
95
97
  const { background, foreground } = getActionPalette(color, theme.colors);
96
98
 
97
99
  const handlePress = useCallback(
98
100
  (e: GestureResponderEvent) => {
101
+ if (inactive) return;
99
102
  const onPress = action.onPress;
100
103
  if (color === 'cancel') {
101
104
  // Cancel always dismisses; consumer's onPress (rare) runs after.
@@ -108,7 +111,7 @@ function ActionButton({ action }: { action: DialogAction }) {
108
111
  onPress?.(e);
109
112
  }
110
113
  },
111
- [action.onPress, close, color, shouldCloseOnPress],
114
+ [action.onPress, close, color, inactive, shouldCloseOnPress],
112
115
  );
113
116
 
114
117
  return (
@@ -118,18 +121,25 @@ function ActionButton({ action }: { action: DialogAction }) {
118
121
  alignItems: 'center',
119
122
  justifyContent: 'center',
120
123
  backgroundColor: background,
121
- opacity: action.disabled ? 0.5 : 1,
124
+ opacity: inactive ? 0.5 : 1,
122
125
  paddingVertical: 12,
123
126
  paddingHorizontal: 24,
124
127
  }}
125
128
  onPress={handlePress}
126
- disabled={action.disabled}
129
+ disabled={inactive}
130
+ accessibilityState={{ disabled: inactive, busy: action.loading === true }}
131
+ aria-disabled={inactive}
132
+ aria-busy={action.loading || undefined}
127
133
  activeOpacity={0.7}
128
134
  testID={action.testID}
129
135
  >
130
- <Text style={{ fontSize: 16, fontWeight: '500', color: foreground }}>
131
- {action.label}
132
- </Text>
136
+ {action.loading ? (
137
+ <ActivityIndicator color={foreground} size="small" />
138
+ ) : (
139
+ <Text style={{ fontSize: 16, fontWeight: '500', color: foreground }}>
140
+ {action.label}
141
+ </Text>
142
+ )}
133
143
  </TouchableOpacity>
134
144
  );
135
145
  }
@@ -87,7 +87,7 @@ export function SheetShell({
87
87
  () => ({
88
88
  maxWidth: 500,
89
89
  backgroundColor: theme.colors.background,
90
- borderRadius: 20,
90
+ borderRadius: 28,
91
91
  }),
92
92
  [theme.colors.background],
93
93
  );
@@ -98,7 +98,6 @@ export function SheetShell({
98
98
  onDismiss={handleDismiss}
99
99
  enablePanDownToClose
100
100
  detached
101
- backdropOpacity={0.7}
102
101
  style={sheetStyle}
103
102
  >
104
103
  <Context.Provider value={context}>
@@ -185,6 +185,11 @@ export type DialogAction = {
185
185
  onPress?: (e: GestureResponderEvent) => void;
186
186
  color?: DialogActionColor;
187
187
  disabled?: boolean;
188
+ /**
189
+ * Replaces the label with a progress indicator and disables interaction while
190
+ * an async action is in flight.
191
+ */
192
+ loading?: boolean;
188
193
  /**
189
194
  * Defaults to `true`. When `true`, the dialog closes (and runs the close
190
195
  * animation) before `onPress` is invoked, which gives the surrounding
@@ -0,0 +1,45 @@
1
+ import { createContext, useContext, useRef, type ReactNode, type RefObject } from 'react';
2
+ import { BlurTargetView } from 'expo-blur';
3
+ import { Platform, StyleSheet, type View } from 'react-native';
4
+
5
+ type BlurTargetRef = RefObject<View | null>;
6
+
7
+ const BlurTargetContext = createContext<BlurTargetRef | null>(null);
8
+
9
+ /**
10
+ * Gives Android overlay blurs a real view to sample.
11
+ *
12
+ * Expo Blur 56+ intentionally disables the Dimezis implementation unless a
13
+ * `BlurTargetView` is supplied. Bloom's sheets render their backdrop inside a
14
+ * native `Modal`, so the target must live around the app content in the
15
+ * underlying window and travel to the modal through React context.
16
+ *
17
+ * Only modal backdrops consume this target. Ordinary in-tree BlurViews never
18
+ * target their own ancestor, avoiding the recursive capture/crash that causes.
19
+ */
20
+ export function BloomBlurTargetProvider({ children }: { children: ReactNode }) {
21
+ const targetRef = useRef<View>(null);
22
+
23
+ if (Platform.OS !== 'android') {
24
+ return <>{children}</>;
25
+ }
26
+
27
+ return (
28
+ <BlurTargetContext.Provider value={targetRef}>
29
+ <BlurTargetView ref={targetRef} style={styles.target}>
30
+ {children}
31
+ </BlurTargetView>
32
+ </BlurTargetContext.Provider>
33
+ );
34
+ }
35
+
36
+ /** Internal target used only by Bloom's native modal backdrops. */
37
+ export function useBloomBlurTarget(): BlurTargetRef | null {
38
+ return useContext(BlurTargetContext);
39
+ }
40
+
41
+ const styles = StyleSheet.create({
42
+ target: {
43
+ flex: 1,
44
+ },
45
+ });
@@ -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 { forwardRef, memo, type ReactNode, type RefObject } from 'react';
43
43
  import { BlurView } from 'expo-blur';
44
44
  import {
45
45
  Platform,
@@ -47,6 +47,7 @@ import {
47
47
  StyleSheet,
48
48
  View,
49
49
  type StyleProp,
50
+ type View as NativeView,
50
51
  type ViewStyle,
51
52
  } from 'react-native';
52
53
 
@@ -95,6 +96,8 @@ export interface BackdropProps {
95
96
  dimColor?: string;
96
97
  /** Dim opacity, 0–1. */
97
98
  dimOpacity?: number;
99
+ /** Android view sampled by the native blur. Modal surfaces receive this from BloomProvider. */
100
+ blurTarget?: RefObject<NativeView | null>;
98
101
  /** Extra style on the press target — animated opacity, insets, z-index. */
99
102
  style?: StyleProp<ViewStyle>;
100
103
  /** Rendered ON TOP of the dim, inside the press target (Dialog's panel does this). */
@@ -116,6 +119,7 @@ export const Backdrop = memo(forwardRef<View, BackdropProps>(function Backdrop(
116
119
  blurTint = 'dark',
117
120
  dimColor = '#000',
118
121
  dimOpacity = BACKDROP_DIM_OPACITY,
122
+ blurTarget,
119
123
  style,
120
124
  children,
121
125
  accessibilityLabel = 'Dismiss',
@@ -149,9 +153,10 @@ export const Backdrop = memo(forwardRef<View, BackdropProps>(function Backdrop(
149
153
  <BlurView
150
154
  intensity={blurIntensity}
151
155
  tint={blurTint}
152
- // Android's default blur is a no-op on many devices; this is the
153
- // implementation that actually renders there.
154
- experimentalBlurMethod="dimezisBlurView"
156
+ blurTarget={blurTarget}
157
+ // Expo disables Android blur without an explicit target. Sheets get
158
+ // one from BloomProvider; targetless surfaces degrade without warning.
159
+ blurMethod={Platform.OS === 'android' && blurTarget ? 'dimezisBlurViewSdk31Plus' : undefined}
155
160
  pointerEvents="none"
156
161
  style={StyleSheet.absoluteFill}
157
162
  />
@@ -34,6 +34,7 @@ import type { ReactNode } from 'react';
34
34
 
35
35
  import { ImageResolverProvider, type ImageResolver } from '../image-resolver';
36
36
  import { BloomHapticsProvider } from '../hooks/useHaptics';
37
+ import { BloomBlurTargetProvider } from '../overlay/blur-target';
37
38
  import { TabBarMinimizeProvider } from '../tab-bar/minimize-context';
38
39
  import { BloomThemeProvider, type BloomThemeProviderProps } from '../theme';
39
40
  import { ScrollRestorationProvider } from './scroll-provider';
@@ -57,17 +58,19 @@ export function BloomProvider({
57
58
  ...themeProps
58
59
  }: BloomProviderProps) {
59
60
  return (
60
- // `value` is passed unconditionally (null when unset) so toggling a resolver
61
- // never changes the tree shape and remounts everything below it.
62
- <ImageResolverProvider value={imageResolver ?? null}>
63
- <BloomThemeProvider {...themeProps}>
64
- <ScrollRestorationProvider>
65
- <BloomHapticsProvider enabled={haptics}>
66
- <TabBarMinimizeProvider>{children}</TabBarMinimizeProvider>
67
- </BloomHapticsProvider>
68
- </ScrollRestorationProvider>
69
- </BloomThemeProvider>
70
- </ImageResolverProvider>
61
+ <BloomBlurTargetProvider>
62
+ {/* `value` is passed unconditionally (null when unset) so toggling a resolver
63
+ never changes the tree shape and remounts everything below it. */}
64
+ <ImageResolverProvider value={imageResolver ?? null}>
65
+ <BloomThemeProvider {...themeProps}>
66
+ <ScrollRestorationProvider>
67
+ <BloomHapticsProvider enabled={haptics}>
68
+ <TabBarMinimizeProvider>{children}</TabBarMinimizeProvider>
69
+ </BloomHapticsProvider>
70
+ </ScrollRestorationProvider>
71
+ </BloomThemeProvider>
72
+ </ImageResolverProvider>
73
+ </BloomBlurTargetProvider>
71
74
  );
72
75
  }
73
76