@rific/auto-paper 0.12.2 → 0.12.3

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/dist/index.js CHANGED
@@ -750,6 +750,8 @@ var BlurView = ({ blur = true, children, elevation, style, tintColor, tintOpacit
750
750
  var import_jsx_runtime6 = require("react/jsx-runtime");
751
751
  var ANIMATION_DURATION = 220;
752
752
  var DIALOG_MARGIN = 32;
753
+ var AnimatedBlurView = import_react_native5.Animated.createAnimatedComponent(BlurView);
754
+ var AnimatedPressable = import_react_native5.Animated.createAnimatedComponent(import_react_native5.Pressable);
753
755
  function DialogComponent({ animatedStyle, blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = "dialog" }) {
754
756
  const blur = useBlur(blurProp);
755
757
  const reanimated = useReanimatedModule();
@@ -793,9 +795,16 @@ function DialogComponent({ animatedStyle, blur: blurProp, dismissable = true, di
793
795
  return child;
794
796
  });
795
797
  const borderRadius = (isV3 ? 7 : 1) * roundness;
796
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_paper6.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native5.Animated.View, { accessibilityLiveRegion: "polite", accessibilityViewIsModal: true, pointerEvents: visible ? "box-none" : "none", style: [import_react_native5.StyleSheet.absoluteFill, { opacity }], testID, children: [
797
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native5.Pressable, { accessibilityLabel: "Close modal", accessibilityRole: "button", disabled: !dismissable, onPress: dismissable ? onDismiss : void 0, style: [import_react_native5.StyleSheet.absoluteFill, { backgroundColor: colors.backdrop }], testID: `${testID}-backdrop` }),
798
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native5.View, { pointerEvents: "box-none", style: [styles2.wrapper, { marginBottom: bottom, marginTop: top, paddingHorizontal: Math.max(left, right, DIALOG_MARGIN) }], testID: `${testID}-wrapper`, children: reanimated && animatedStyle ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(reanimated.View, { style: animatedStyle, testID: `${testID}-animated-wrapper`, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BlurView, { blur, style: [styles2.card, { borderRadius }, style], testID: `${testID}-surface`, children: content }) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BlurView, { blur, style: [styles2.card, { borderRadius }, style], testID: `${testID}-surface`, children: content }) })
798
+ const web = import_react_native5.Platform.OS === "web";
799
+ const fade = { opacity };
800
+ const styleOpacity = import_react_native5.StyleSheet.flatten(style)?.opacity;
801
+ const cardFade = typeof styleOpacity === "number" ? { opacity: import_react_native5.Animated.multiply(opacity, styleOpacity) } : fade;
802
+ const Backdrop = web ? AnimatedPressable : import_react_native5.Pressable;
803
+ const Card = web ? AnimatedBlurView : BlurView;
804
+ const card = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Card, { blur, style: [styles2.card, { borderRadius }, style, web && cardFade], testID: `${testID}-surface`, children: content });
805
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native_paper6.Portal, { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react_native5.Animated.View, { accessibilityLiveRegion: "polite", accessibilityViewIsModal: true, pointerEvents: visible ? "box-none" : "none", style: [import_react_native5.StyleSheet.absoluteFill, !web && fade], testID, children: [
806
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Backdrop, { accessibilityLabel: "Close modal", accessibilityRole: "button", disabled: !dismissable, onPress: dismissable ? onDismiss : void 0, style: [import_react_native5.StyleSheet.absoluteFill, { backgroundColor: colors.backdrop }, web && fade], testID: `${testID}-backdrop` }),
807
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react_native5.View, { pointerEvents: "box-none", style: [styles2.wrapper, { marginBottom: bottom, marginTop: top, paddingHorizontal: Math.max(left, right, DIALOG_MARGIN) }], testID: `${testID}-wrapper`, children: reanimated && animatedStyle ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(reanimated.View, { style: animatedStyle, testID: `${testID}-animated-wrapper`, children: card }) : card })
799
808
  ] }) });
800
809
  }
801
810
  DialogComponent.Title = import_react_native_paper6.Dialog.Title;
@@ -1036,25 +1045,45 @@ function IconButton({ variant, iconColor, containerColor, ...props }) {
1036
1045
  }
1037
1046
 
1038
1047
  // src/components/Menu.tsx
1048
+ var import_react11 = require("react");
1039
1049
  var import_react_native9 = require("react-native");
1040
1050
  var import_react_native_paper15 = require("react-native-paper");
1041
1051
  var import_jsx_runtime16 = require("react/jsx-runtime");
1052
+ var ANIMATION_DURATION2 = 250;
1053
+ var EASING = import_react_native9.Easing.bezier(0.4, 0, 0.2, 1);
1054
+ var DEFAULT_ELEVATION = 2;
1055
+ var SHADOW_HEIGHT = [0, 1, 2, 4, 6, 8];
1056
+ var SHADOW_RADIUS = [0, 3, 6, 8, 10, 12];
1057
+ var AnimatedBlurView2 = import_react_native9.Animated.createAnimatedComponent(BlurView);
1058
+ var FadingBlurView = ({ blur, children, elevation, theme: themeOverrides, visible }) => {
1059
+ const { animation, roundness } = (0, import_react_native_paper15.useTheme)(themeOverrides);
1060
+ const [opacity] = (0, import_react11.useState)(() => new import_react_native9.Animated.Value(0));
1061
+ const [closing, setClosing] = (0, import_react11.useState)(false);
1062
+ if (!visible && !closing) setClosing(true);
1063
+ (0, import_react11.useEffect)(() => {
1064
+ import_react_native9.Animated.timing(opacity, { duration: animation.scale * ANIMATION_DURATION2, easing: EASING, toValue: closing ? 0 : 1, useNativeDriver: false }).start();
1065
+ }, [animation.scale, closing, opacity]);
1066
+ const shadow = elevation > 0 ? { shadowColor: "#000000", shadowOffset: { height: SHADOW_HEIGHT[elevation], width: 0 }, shadowOpacity: 0.3, shadowRadius: SHADOW_RADIUS[elevation] } : null;
1067
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(AnimatedBlurView2, { blur, style: [{ borderRadius: roundness, opacity }, shadow], children });
1068
+ };
1042
1069
  var MenuComponent = ({ blur: blurProp, children, ...props }) => {
1043
1070
  const blur = useBlur(blurProp);
1044
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper15.Menu, { ...props, contentStyle: [styles5.content, props.contentStyle], children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(BlurView, { blur, children }) });
1071
+ const web = import_react_native9.Platform.OS === "web";
1072
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native_paper15.Menu, { ...props, contentStyle: [styles5.content, web && styles5.webSurface, props.contentStyle], children: web ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(FadingBlurView, { blur, elevation: props.mode === "flat" ? 0 : props.elevation ?? DEFAULT_ELEVATION, theme: props.theme, visible: props.visible, children }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(BlurView, { blur, children }) });
1045
1073
  };
1046
1074
  var Menu = Object.assign(MenuComponent, {
1047
1075
  Item: import_react_native_paper15.Menu.Item
1048
1076
  });
1049
1077
  var styles5 = import_react_native9.StyleSheet.create({
1050
- content: { backgroundColor: "transparent", paddingVertical: 0 }
1078
+ content: { backgroundColor: "transparent", paddingVertical: 0 },
1079
+ webSurface: { opacity: 1, shadowOpacity: 0 }
1051
1080
  });
1052
1081
 
1053
1082
  // src/components/TextInput.tsx
1054
- var import_react11 = require("react");
1083
+ var import_react12 = require("react");
1055
1084
  var import_react_native_paper16 = require("react-native-paper");
1056
1085
  var import_jsx_runtime17 = require("react/jsx-runtime");
1057
- var TextInput = (0, import_react11.forwardRef)(function TextInput2(props, ref) {
1086
+ var TextInput = (0, import_react12.forwardRef)(function TextInput2(props, ref) {
1058
1087
  const defaults = usePaperDefaults();
1059
1088
  return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native_paper16.TextInput, { ref, ...defaults.TextInput, ...props });
1060
1089
  });
@@ -1085,10 +1114,10 @@ var import_react_native_paper17 = require("react-native-paper");
1085
1114
  var useAutoPaperTheme = () => (0, import_react_native_paper17.useTheme)();
1086
1115
 
1087
1116
  // src/useThemeBridgeProps.ts
1088
- var import_react12 = require("react");
1117
+ var import_react13 = require("react");
1089
1118
  function useThemeBridgeProps(props) {
1090
1119
  const { initialValue, onChange, onReady } = props;
1091
- return (0, import_react12.useMemo)(() => ({ initialValue, onChange, onReady }), [initialValue, onChange, onReady]);
1120
+ return (0, import_react13.useMemo)(() => ({ initialValue, onChange, onReady }), [initialValue, onChange, onReady]);
1092
1121
  }
1093
1122
 
1094
1123
  // src/utils/getThirdColor.ts
package/dist/index.mjs CHANGED
@@ -416,7 +416,7 @@ import { Icon, TouchableRipple, useTheme as useTheme4 } from "react-native-paper
416
416
 
417
417
  // src/components/Dialog.tsx
418
418
  import React, { useEffect as useEffect3, useState as useState3 } from "react";
419
- import { Animated, BackHandler, Easing, Pressable, StyleSheet as StyleSheet3, View as View3 } from "react-native";
419
+ import { Animated, BackHandler, Easing, Platform as Platform2, Pressable, StyleSheet as StyleSheet3, View as View3 } from "react-native";
420
420
  import { Dialog as PaperDialog, Portal as Portal2, useTheme as useTheme3 } from "react-native-paper";
421
421
  import { useSafeAreaInsets } from "react-native-safe-area-context";
422
422
 
@@ -667,6 +667,8 @@ var BlurView = ({ blur = true, children, elevation, style, tintColor, tintOpacit
667
667
  import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
668
668
  var ANIMATION_DURATION = 220;
669
669
  var DIALOG_MARGIN = 32;
670
+ var AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
671
+ var AnimatedPressable = Animated.createAnimatedComponent(Pressable);
670
672
  function DialogComponent({ animatedStyle, blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = "dialog" }) {
671
673
  const blur = useBlur(blurProp);
672
674
  const reanimated = useReanimatedModule();
@@ -710,9 +712,16 @@ function DialogComponent({ animatedStyle, blur: blurProp, dismissable = true, di
710
712
  return child;
711
713
  });
712
714
  const borderRadius = (isV3 ? 7 : 1) * roundness;
713
- return /* @__PURE__ */ jsx6(Portal2, { children: /* @__PURE__ */ jsxs4(Animated.View, { accessibilityLiveRegion: "polite", accessibilityViewIsModal: true, pointerEvents: visible ? "box-none" : "none", style: [StyleSheet3.absoluteFill, { opacity }], testID, children: [
714
- /* @__PURE__ */ jsx6(Pressable, { accessibilityLabel: "Close modal", accessibilityRole: "button", disabled: !dismissable, onPress: dismissable ? onDismiss : void 0, style: [StyleSheet3.absoluteFill, { backgroundColor: colors.backdrop }], testID: `${testID}-backdrop` }),
715
- /* @__PURE__ */ jsx6(View3, { pointerEvents: "box-none", style: [styles2.wrapper, { marginBottom: bottom, marginTop: top, paddingHorizontal: Math.max(left, right, DIALOG_MARGIN) }], testID: `${testID}-wrapper`, children: reanimated && animatedStyle ? /* @__PURE__ */ jsx6(reanimated.View, { style: animatedStyle, testID: `${testID}-animated-wrapper`, children: /* @__PURE__ */ jsx6(BlurView, { blur, style: [styles2.card, { borderRadius }, style], testID: `${testID}-surface`, children: content }) }) : /* @__PURE__ */ jsx6(BlurView, { blur, style: [styles2.card, { borderRadius }, style], testID: `${testID}-surface`, children: content }) })
715
+ const web = Platform2.OS === "web";
716
+ const fade = { opacity };
717
+ const styleOpacity = StyleSheet3.flatten(style)?.opacity;
718
+ const cardFade = typeof styleOpacity === "number" ? { opacity: Animated.multiply(opacity, styleOpacity) } : fade;
719
+ const Backdrop = web ? AnimatedPressable : Pressable;
720
+ const Card = web ? AnimatedBlurView : BlurView;
721
+ const card = /* @__PURE__ */ jsx6(Card, { blur, style: [styles2.card, { borderRadius }, style, web && cardFade], testID: `${testID}-surface`, children: content });
722
+ return /* @__PURE__ */ jsx6(Portal2, { children: /* @__PURE__ */ jsxs4(Animated.View, { accessibilityLiveRegion: "polite", accessibilityViewIsModal: true, pointerEvents: visible ? "box-none" : "none", style: [StyleSheet3.absoluteFill, !web && fade], testID, children: [
723
+ /* @__PURE__ */ jsx6(Backdrop, { accessibilityLabel: "Close modal", accessibilityRole: "button", disabled: !dismissable, onPress: dismissable ? onDismiss : void 0, style: [StyleSheet3.absoluteFill, { backgroundColor: colors.backdrop }, web && fade], testID: `${testID}-backdrop` }),
724
+ /* @__PURE__ */ jsx6(View3, { pointerEvents: "box-none", style: [styles2.wrapper, { marginBottom: bottom, marginTop: top, paddingHorizontal: Math.max(left, right, DIALOG_MARGIN) }], testID: `${testID}-wrapper`, children: reanimated && animatedStyle ? /* @__PURE__ */ jsx6(reanimated.View, { style: animatedStyle, testID: `${testID}-animated-wrapper`, children: card }) : card })
716
725
  ] }) });
717
726
  }
718
727
  DialogComponent.Title = PaperDialog.Title;
@@ -881,7 +890,7 @@ var AutoPalettePicker = (props) => {
881
890
 
882
891
  // src/components/BottomNavigation.tsx
883
892
  import { useEffect as useEffect4 } from "react";
884
- import { Platform as Platform2 } from "react-native";
893
+ import { Platform as Platform3 } from "react-native";
885
894
  import { BottomNavigation as PaperBottomNavigation, useTheme as useTheme6 } from "react-native-paper";
886
895
 
887
896
  // src/navigation-bar.ts
@@ -896,7 +905,7 @@ function useNavigationBarSync() {
896
905
  const theme = useTheme6();
897
906
  const { navigationBar, onNavBarChange } = useNavBarContext();
898
907
  useEffect4(() => {
899
- if (Platform2.OS !== "android") return;
908
+ if (Platform3.OS !== "android") return;
900
909
  if (onNavBarChange) onNavBarChange(theme.colors.surface, theme.dark);
901
910
  else if (navigationBar) setNavigationBarStyle(navigationBar, theme.dark);
902
911
  }, [theme, onNavBarChange, navigationBar]);
@@ -953,18 +962,38 @@ function IconButton({ variant, iconColor, containerColor, ...props }) {
953
962
  }
954
963
 
955
964
  // src/components/Menu.tsx
956
- import { StyleSheet as StyleSheet6 } from "react-native";
957
- import { Menu as PaperMenu } from "react-native-paper";
965
+ import { useEffect as useEffect5, useState as useState6 } from "react";
966
+ import { Animated as Animated2, Easing as Easing2, Platform as Platform4, StyleSheet as StyleSheet6 } from "react-native";
967
+ import { Menu as PaperMenu, useTheme as useTheme9 } from "react-native-paper";
958
968
  import { jsx as jsx16 } from "react/jsx-runtime";
969
+ var ANIMATION_DURATION2 = 250;
970
+ var EASING = Easing2.bezier(0.4, 0, 0.2, 1);
971
+ var DEFAULT_ELEVATION = 2;
972
+ var SHADOW_HEIGHT = [0, 1, 2, 4, 6, 8];
973
+ var SHADOW_RADIUS = [0, 3, 6, 8, 10, 12];
974
+ var AnimatedBlurView2 = Animated2.createAnimatedComponent(BlurView);
975
+ var FadingBlurView = ({ blur, children, elevation, theme: themeOverrides, visible }) => {
976
+ const { animation, roundness } = useTheme9(themeOverrides);
977
+ const [opacity] = useState6(() => new Animated2.Value(0));
978
+ const [closing, setClosing] = useState6(false);
979
+ if (!visible && !closing) setClosing(true);
980
+ useEffect5(() => {
981
+ Animated2.timing(opacity, { duration: animation.scale * ANIMATION_DURATION2, easing: EASING, toValue: closing ? 0 : 1, useNativeDriver: false }).start();
982
+ }, [animation.scale, closing, opacity]);
983
+ const shadow = elevation > 0 ? { shadowColor: "#000000", shadowOffset: { height: SHADOW_HEIGHT[elevation], width: 0 }, shadowOpacity: 0.3, shadowRadius: SHADOW_RADIUS[elevation] } : null;
984
+ return /* @__PURE__ */ jsx16(AnimatedBlurView2, { blur, style: [{ borderRadius: roundness, opacity }, shadow], children });
985
+ };
959
986
  var MenuComponent = ({ blur: blurProp, children, ...props }) => {
960
987
  const blur = useBlur(blurProp);
961
- return /* @__PURE__ */ jsx16(PaperMenu, { ...props, contentStyle: [styles5.content, props.contentStyle], children: /* @__PURE__ */ jsx16(BlurView, { blur, children }) });
988
+ const web = Platform4.OS === "web";
989
+ return /* @__PURE__ */ jsx16(PaperMenu, { ...props, contentStyle: [styles5.content, web && styles5.webSurface, props.contentStyle], children: web ? /* @__PURE__ */ jsx16(FadingBlurView, { blur, elevation: props.mode === "flat" ? 0 : props.elevation ?? DEFAULT_ELEVATION, theme: props.theme, visible: props.visible, children }) : /* @__PURE__ */ jsx16(BlurView, { blur, children }) });
962
990
  };
963
991
  var Menu = Object.assign(MenuComponent, {
964
992
  Item: PaperMenu.Item
965
993
  });
966
994
  var styles5 = StyleSheet6.create({
967
- content: { backgroundColor: "transparent", paddingVertical: 0 }
995
+ content: { backgroundColor: "transparent", paddingVertical: 0 },
996
+ webSurface: { opacity: 1, shadowOpacity: 0 }
968
997
  });
969
998
 
970
999
  // src/components/TextInput.tsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rific/auto-paper",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "Adaptive react-native-paper theming: triadic palette from a seed color + system/light/dark appearance",
5
5
  "keywords": [
6
6
  "react-native",
@@ -1,5 +1,5 @@
1
1
  import React, { type ComponentType, type ReactNode, useEffect, useState } from 'react'
2
- import { Animated, BackHandler, Easing, Pressable, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
2
+ import { Animated, BackHandler, Easing, Platform, Pressable, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'
3
3
  import { Dialog as PaperDialog, type DialogActionsProps, type DialogContentProps, type DialogProps as PaperDialogProps, type DialogScrollAreaProps, type DialogTitleProps, Portal, useTheme } from 'react-native-paper'
4
4
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
5
5
 
@@ -39,6 +39,9 @@ const ANIMATION_DURATION = 220
39
39
  // Minimum breathing room between the card and the screen edge, clamped up from safe-area insets.
40
40
  const DIALOG_MARGIN = 32
41
41
 
42
+ const AnimatedBlurView = Animated.createAnimatedComponent(BlurView)
43
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
44
+
42
45
  function DialogComponent({ animatedStyle, blur: blurProp, dismissable = true, dismissableBackButton = dismissable, visible, onDismiss, children, style, testID = 'dialog' }: DialogProps) {
43
46
  const blur = useBlur(blurProp)
44
47
  const reanimated = useReanimatedModule()
@@ -93,21 +96,34 @@ function DialogComponent({ animatedStyle, blur: blurProp, dismissable = true, di
93
96
 
94
97
  const borderRadius = (isV3 ? 7 : 1) * roundness
95
98
 
99
+ // On web, an ancestor below opacity 1 cuts the card's CSS backdrop-filter off from the page behind
100
+ // it, and Chromium doesn't restore the blur once that ancestor fades back up to 1, so fading the
101
+ // whole layer would leave the open card see-through. There the fade runs on the backdrop and on the
102
+ // card (the backdrop-filter element) itself instead, off the same value. A numeric opacity in the
103
+ // card's own style still applies there, multiplied into the fade.
104
+ const web = Platform.OS === 'web'
105
+ const fade = { opacity }
106
+ const styleOpacity = StyleSheet.flatten(style)?.opacity
107
+ const cardFade = typeof styleOpacity === 'number' ? { opacity: Animated.multiply(opacity, styleOpacity) } : fade
108
+ const Backdrop = web ? AnimatedPressable : Pressable
109
+ const Card = web ? AnimatedBlurView : BlurView
110
+ const card = (
111
+ <Card blur={blur} style={[styles.card, { borderRadius }, style, web && cardFade]} testID={`${testID}-surface`}>
112
+ {content}
113
+ </Card>
114
+ )
115
+
96
116
  return (
97
117
  <Portal>
98
- <Animated.View accessibilityLiveRegion='polite' accessibilityViewIsModal pointerEvents={visible ? 'box-none' : 'none'} style={[StyleSheet.absoluteFill, { opacity }]} testID={testID}>
99
- <Pressable accessibilityLabel='Close modal' accessibilityRole='button' disabled={!dismissable} onPress={dismissable ? onDismiss : undefined} style={[StyleSheet.absoluteFill, { backgroundColor: colors.backdrop }]} testID={`${testID}-backdrop`} />
118
+ <Animated.View accessibilityLiveRegion='polite' accessibilityViewIsModal pointerEvents={visible ? 'box-none' : 'none'} style={[StyleSheet.absoluteFill, !web && fade]} testID={testID}>
119
+ <Backdrop accessibilityLabel='Close modal' accessibilityRole='button' disabled={!dismissable} onPress={dismissable ? onDismiss : undefined} style={[StyleSheet.absoluteFill, { backgroundColor: colors.backdrop }, web && fade]} testID={`${testID}-backdrop`} />
100
120
  <View pointerEvents='box-none' style={[styles.wrapper, { marginBottom: bottom, marginTop: top, paddingHorizontal: Math.max(left, right, DIALOG_MARGIN) }]} testID={`${testID}-wrapper`}>
101
121
  {reanimated && animatedStyle ? (
102
122
  <reanimated.View style={animatedStyle} testID={`${testID}-animated-wrapper`}>
103
- <BlurView blur={blur} style={[styles.card, { borderRadius }, style]} testID={`${testID}-surface`}>
104
- {content}
105
- </BlurView>
123
+ {card}
106
124
  </reanimated.View>
107
125
  ) : (
108
- <BlurView blur={blur} style={[styles.card, { borderRadius }, style]} testID={`${testID}-surface`}>
109
- {content}
110
- </BlurView>
126
+ card
111
127
  )}
112
128
  </View>
113
129
  </Animated.View>
@@ -1,6 +1,6 @@
1
- import React from 'react'
2
- import { StyleSheet } from 'react-native'
3
- import { Menu as PaperMenu, type MenuItemProps, type MenuProps as PaperMenuProps } from 'react-native-paper'
1
+ import React, { type ReactNode, useEffect, useState } from 'react'
2
+ import { Animated, Easing, Platform, StyleSheet } from 'react-native'
3
+ import { type MD2Theme, type MD3Theme, Menu as PaperMenu, type MenuItemProps, type MenuProps as PaperMenuProps, useTheme } from 'react-native-paper'
4
4
 
5
5
  import { useBlur } from '../BlurContext'
6
6
  import { BlurView } from './BlurView'
@@ -9,11 +9,61 @@ export type MenuProps = PaperMenuProps & {
9
9
  blur?: boolean
10
10
  }
11
11
 
12
+ // Mirror react-native-paper's Menu: its fade (ANIMATION_DURATION * theme.animation.scale, eased with
13
+ // EASING), its default elevation, and the MD3 shadow its Surface draws at each elevation level.
14
+ const ANIMATION_DURATION = 250
15
+ const EASING = Easing.bezier(0.4, 0, 0.2, 1)
16
+ const DEFAULT_ELEVATION = 2
17
+ const SHADOW_HEIGHT = [0, 1, 2, 4, 6, 8]
18
+ const SHADOW_RADIUS = [0, 3, 6, 8, 10, 12]
19
+
20
+ const AnimatedBlurView = Animated.createAnimatedComponent(BlurView)
21
+
22
+ type FadingBlurViewProps = {
23
+ blur: boolean
24
+ children: ReactNode
25
+ elevation: number
26
+ theme: PaperMenuProps['theme']
27
+ visible: boolean
28
+ }
29
+
30
+ // Paper fades the menu's Surface in and out. On web, an ancestor below opacity 1 cuts BlurView's CSS
31
+ // backdrop-filter off from the page behind it, and Chromium doesn't restore the blur once the Surface
32
+ // reaches 1, so the open menu stays see-through until something else repaints it. So on web the Surface
33
+ // is pinned at opacity 1 with its shadow hidden, and BlurView itself (the backdrop-filter element) runs
34
+ // Paper's fades and draws that shadow, so the two still fade together.
35
+ const FadingBlurView = ({ blur, children, elevation, theme: themeOverrides, visible }: FadingBlurViewProps) => {
36
+ const { animation, roundness } = useTheme<MD2Theme | MD3Theme>(themeOverrides)
37
+ const [opacity] = useState(() => new Animated.Value(0))
38
+ // Once Paper starts hiding the menu it unmounts this content when that fade-out ends, even if visible
39
+ // turns back on first, so a close is final here too rather than fading back in only to vanish.
40
+ const [closing, setClosing] = useState(false)
41
+ if (!visible && !closing) setClosing(true)
42
+
43
+ useEffect(() => {
44
+ Animated.timing(opacity, { duration: animation.scale * ANIMATION_DURATION, easing: EASING, toValue: closing ? 0 : 1, useNativeDriver: false }).start()
45
+ }, [animation.scale, closing, opacity])
46
+
47
+ const shadow = elevation > 0 ? { shadowColor: '#000000', shadowOffset: { height: SHADOW_HEIGHT[elevation], width: 0 }, shadowOpacity: 0.3, shadowRadius: SHADOW_RADIUS[elevation] } : null
48
+ return (
49
+ <AnimatedBlurView blur={blur} style={[{ borderRadius: roundness, opacity }, shadow]}>
50
+ {children}
51
+ </AnimatedBlurView>
52
+ )
53
+ }
54
+
12
55
  const MenuComponent = ({ blur: blurProp, children, ...props }: MenuProps) => {
13
56
  const blur = useBlur(blurProp)
57
+ const web = Platform.OS === 'web'
14
58
  return (
15
- <PaperMenu {...props} contentStyle={[styles.content, props.contentStyle]}>
16
- <BlurView blur={blur}>{children}</BlurView>
59
+ <PaperMenu {...props} contentStyle={[styles.content, web && styles.webSurface, props.contentStyle]}>
60
+ {web ? (
61
+ <FadingBlurView blur={blur} elevation={props.mode === 'flat' ? 0 : (props.elevation ?? DEFAULT_ELEVATION)} theme={props.theme} visible={props.visible}>
62
+ {children}
63
+ </FadingBlurView>
64
+ ) : (
65
+ <BlurView blur={blur}>{children}</BlurView>
66
+ )}
17
67
  </PaperMenu>
18
68
  )
19
69
  }
@@ -23,5 +73,6 @@ export const Menu = Object.assign(MenuComponent, {
23
73
  })
24
74
 
25
75
  const styles = StyleSheet.create({
26
- content: { backgroundColor: 'transparent', paddingVertical: 0 }
76
+ content: { backgroundColor: 'transparent', paddingVertical: 0 },
77
+ webSurface: { opacity: 1, shadowOpacity: 0 }
27
78
  })