react-native-quick-preview 1.0.4
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/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/index.d.mts +42 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +81 -0
- package/src/QuickLook.tsx +222 -0
- package/src/QuickLookProperties.ts +48 -0
- package/src/index.ts +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Oliver Lindblad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# react-native-quicklook
|
|
2
|
+
|
|
3
|
+
A beautiful, customizable quick preview modal component for React Native.
|
|
4
|
+
Think **Gorhom Bottom Sheet**, but for quick previews.
|
|
5
|
+
|
|
6
|
+
Perfect for:
|
|
7
|
+
- Instagram-style **long-press previews**
|
|
8
|
+
- ποΈ **E-commerce product quick views**
|
|
9
|
+
- π° **Article teasers**
|
|
10
|
+
- βοΈ **Travel destination peeks**
|
|
11
|
+
- β¦any content that needs a **quick peek before full navigation**
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## β¨ Features
|
|
16
|
+
|
|
17
|
+
- π¨ Smooth **fade, scale, and swipe-to-close animations**
|
|
18
|
+
- π― **Universal content** β works with any layout (products, posts, etc.)
|
|
19
|
+
- ποΈ **Customizable** β theme, behavior, styling, backdrop
|
|
20
|
+
- βΏ **Accessibility ready** β screen reader & keyboard navigation
|
|
21
|
+
- π± **Cross-platform** β iOS & Android
|
|
22
|
+
- π§ **TypeScript support**
|
|
23
|
+
- β‘ **Performance optimized** β native drivers
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## π¦ Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install react-native-quicklook
|
|
31
|
+
# or
|
|
32
|
+
yarn add react-native-quicklook
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## π Quick Start
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import React, { useState } from 'react';
|
|
41
|
+
import { View, Text, TouchableOpacity } from 'react-native';
|
|
42
|
+
import { QuickLook } from 'react-native-quicklook';
|
|
43
|
+
|
|
44
|
+
export default function App() {
|
|
45
|
+
const [visible, setVisible] = useState(false);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
49
|
+
<TouchableOpacity onPress={() => setVisible(true)}>
|
|
50
|
+
<Text>Show Quick Look</Text>
|
|
51
|
+
</TouchableOpacity>
|
|
52
|
+
|
|
53
|
+
<QuickLook visible={visible} onClose={() => setVisible(false)}>
|
|
54
|
+
<View style={{ backgroundColor: '#fff', padding: 20 }}>
|
|
55
|
+
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>Quick Preview Content</Text>
|
|
56
|
+
<Text style={{ marginTop: 10 }}>Any custom content goes here.</Text>
|
|
57
|
+
</View>
|
|
58
|
+
</QuickLook>
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## π Usage Examples
|
|
67
|
+
|
|
68
|
+
### ποΈ E-commerce Product Quick View
|
|
69
|
+
```tsx
|
|
70
|
+
<QuickLook visible={visible} onClose={onClose} onPressCard={onViewDetails}>
|
|
71
|
+
<View>
|
|
72
|
+
<Image source={{ uri: product.image }} style={{ width: '100%', height: 200 }} />
|
|
73
|
+
<Text>{product.name}</Text>
|
|
74
|
+
<Text>{product.price}</Text>
|
|
75
|
+
</View>
|
|
76
|
+
</QuickLook>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### π° Article Preview
|
|
80
|
+
```tsx
|
|
81
|
+
<QuickLook visible={visible} onClose={onClose} theme="dark">
|
|
82
|
+
<View style={{ backgroundColor: '#1a1a1a' }}>
|
|
83
|
+
<Image source={{ uri: article.coverImage }} style={{ width: '100%', height: 200 }} />
|
|
84
|
+
<Text style={{ color: '#fff' }}>{article.title}</Text>
|
|
85
|
+
<Text style={{ color: '#ccc' }}>{article.excerpt}</Text>
|
|
86
|
+
</View>
|
|
87
|
+
</QuickLook>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### βοΈ Travel Destination Peek
|
|
91
|
+
```tsx
|
|
92
|
+
<QuickLook visible={visible} onClose={onClose}>
|
|
93
|
+
<Image source={{ uri: destination.image }} style={{ width: '100%', height: 200 }} />
|
|
94
|
+
<Text>{destination.title}</Text>
|
|
95
|
+
<Text>From ${destination.price}</Text>
|
|
96
|
+
</QuickLook>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## ποΈ API Reference
|
|
102
|
+
|
|
103
|
+
### Props
|
|
104
|
+
|
|
105
|
+
| Prop | Type | Default | Description |
|
|
106
|
+
|------|------|---------|-------------|
|
|
107
|
+
| `visible` | `boolean` | **required** | Controls modal visibility |
|
|
108
|
+
| `onClose` | `() => void` | **required** | Called when modal closes |
|
|
109
|
+
| `onPressCard` | `() => void` | `undefined` | Card press handler |
|
|
110
|
+
| `children` | `React.ReactNode` | `undefined` | Content to render |
|
|
111
|
+
| `theme` | `'light' \| 'dark'` | `'light'` | Overlay theme |
|
|
112
|
+
| `backdropOpacity` | `number` | `0.5 / 0.8` | Overlay opacity |
|
|
113
|
+
| `animationDuration` | `number` | `220` | Animation duration |
|
|
114
|
+
| `closeOnBackdropPress` | `boolean` | `true` | Close on backdrop press |
|
|
115
|
+
| `closeOnBackButton` | `boolean` | `true` | Close on Android back button |
|
|
116
|
+
| `enableSwipeToClose` | `boolean` | `true` | Swipe down to close |
|
|
117
|
+
| `swipeThreshold` | `number` | `80` | Swipe distance threshold |
|
|
118
|
+
| `unmountOnExit` | `boolean` | `true` | Unmount when hidden |
|
|
119
|
+
| `avoidKeyboard` | `boolean` | `false` | Avoid keyboard overlap |
|
|
120
|
+
| `renderBackdrop` | `(opacity) => ReactNode` | `undefined` | Custom backdrop |
|
|
121
|
+
| `onBackdropPress` | `() => void` | `undefined` | Backdrop press handler |
|
|
122
|
+
| `testID` | `string` | `'quicklook'` | Test identifier |
|
|
123
|
+
| `accessibilityLabel` | `string` | `'Quick look'` | A11y label |
|
|
124
|
+
| `stylesOverride` | `object` | `{}` | Override default styles |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## π¨ Customization
|
|
129
|
+
|
|
130
|
+
### Custom Backdrop
|
|
131
|
+
```tsx
|
|
132
|
+
<QuickLook
|
|
133
|
+
visible={visible}
|
|
134
|
+
onClose={onClose}
|
|
135
|
+
renderBackdrop={(opacity) => (
|
|
136
|
+
<Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(255,0,0,0.5)', opacity }]} />
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
{/* content */}
|
|
140
|
+
</QuickLook>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Custom Styles
|
|
144
|
+
```tsx
|
|
145
|
+
<QuickLook
|
|
146
|
+
visible={visible}
|
|
147
|
+
onClose={onClose}
|
|
148
|
+
stylesOverride={{
|
|
149
|
+
container: { borderRadius: 24, backgroundColor: '#000' },
|
|
150
|
+
overlay: { backgroundColor: 'rgba(0,0,0,0.9)' }
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
{/* content */}
|
|
154
|
+
</QuickLook>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## π§ͺ Testing
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
163
|
+
|
|
164
|
+
test('QuickLook closes on backdrop press', () => {
|
|
165
|
+
const onClose = jest.fn();
|
|
166
|
+
const { getByTestId } = render(
|
|
167
|
+
<QuickLook visible={true} onClose={onClose} testID="quicklook">
|
|
168
|
+
<Text>Test content</Text>
|
|
169
|
+
</QuickLook>
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
fireEvent.press(getByTestId('ql-backdrop'));
|
|
173
|
+
expect(onClose).toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## π± Platform Support
|
|
180
|
+
- β
iOS 12+
|
|
181
|
+
- β
Android API 21+
|
|
182
|
+
- β
Expo SDK 48+
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## π License
|
|
187
|
+
|
|
188
|
+
MIT Β© Your Name
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { Animated } from 'react-native';
|
|
3
|
+
|
|
4
|
+
type ThemeMode = 'light' | 'dark';
|
|
5
|
+
type CloseReason = 'programmatic' | 'backdrop' | 'backButton' | 'swipe';
|
|
6
|
+
interface QuickLookProps {
|
|
7
|
+
visible: boolean;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
/** Lifecycle hooks (great for haptics/analytics) */
|
|
10
|
+
onOpen?: () => void;
|
|
11
|
+
onClosed?: (reason: CloseReason) => void;
|
|
12
|
+
/** Optional: press whole card (prefer explicit CTAs as well) */
|
|
13
|
+
onPressCard?: () => void;
|
|
14
|
+
theme?: ThemeMode;
|
|
15
|
+
backdropOpacity?: number;
|
|
16
|
+
animationDuration?: number;
|
|
17
|
+
/** Behavior toggles */
|
|
18
|
+
closeOnBackdropPress?: boolean;
|
|
19
|
+
closeOnBackButton?: boolean;
|
|
20
|
+
enableSwipeToClose?: boolean;
|
|
21
|
+
swipeThreshold?: number;
|
|
22
|
+
unmountOnExit?: boolean;
|
|
23
|
+
avoidKeyboard?: boolean;
|
|
24
|
+
/** Headless content */
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
/** Backdrop customization */
|
|
27
|
+
renderBackdrop?: (opacity: Animated.AnimatedInterpolation<number>) => ReactNode;
|
|
28
|
+
onBackdropPress?: () => void;
|
|
29
|
+
/** Testing & a11y */
|
|
30
|
+
testID?: string;
|
|
31
|
+
accessibilityLabel?: string;
|
|
32
|
+
/** Outer wrapper style overrides */
|
|
33
|
+
stylesOverride?: {
|
|
34
|
+
overlay?: any;
|
|
35
|
+
container?: any;
|
|
36
|
+
centerWrap?: any;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare const QuickLook: React.FC<QuickLookProps>;
|
|
41
|
+
|
|
42
|
+
export { type CloseReason, QuickLook, type QuickLookProps, type ThemeMode };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { Animated } from 'react-native';
|
|
3
|
+
|
|
4
|
+
type ThemeMode = 'light' | 'dark';
|
|
5
|
+
type CloseReason = 'programmatic' | 'backdrop' | 'backButton' | 'swipe';
|
|
6
|
+
interface QuickLookProps {
|
|
7
|
+
visible: boolean;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
/** Lifecycle hooks (great for haptics/analytics) */
|
|
10
|
+
onOpen?: () => void;
|
|
11
|
+
onClosed?: (reason: CloseReason) => void;
|
|
12
|
+
/** Optional: press whole card (prefer explicit CTAs as well) */
|
|
13
|
+
onPressCard?: () => void;
|
|
14
|
+
theme?: ThemeMode;
|
|
15
|
+
backdropOpacity?: number;
|
|
16
|
+
animationDuration?: number;
|
|
17
|
+
/** Behavior toggles */
|
|
18
|
+
closeOnBackdropPress?: boolean;
|
|
19
|
+
closeOnBackButton?: boolean;
|
|
20
|
+
enableSwipeToClose?: boolean;
|
|
21
|
+
swipeThreshold?: number;
|
|
22
|
+
unmountOnExit?: boolean;
|
|
23
|
+
avoidKeyboard?: boolean;
|
|
24
|
+
/** Headless content */
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
/** Backdrop customization */
|
|
27
|
+
renderBackdrop?: (opacity: Animated.AnimatedInterpolation<number>) => ReactNode;
|
|
28
|
+
onBackdropPress?: () => void;
|
|
29
|
+
/** Testing & a11y */
|
|
30
|
+
testID?: string;
|
|
31
|
+
accessibilityLabel?: string;
|
|
32
|
+
/** Outer wrapper style overrides */
|
|
33
|
+
stylesOverride?: {
|
|
34
|
+
overlay?: any;
|
|
35
|
+
container?: any;
|
|
36
|
+
centerWrap?: any;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare const QuickLook: React.FC<QuickLookProps>;
|
|
41
|
+
|
|
42
|
+
export { type CloseReason, QuickLook, type QuickLookProps, type ThemeMode };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
'use strict';var react=require('react'),reactNative=require('react-native'),jsxRuntime=require('react/jsx-runtime');var K=Object.defineProperty,U=Object.defineProperties;var X=Object.getOwnPropertyDescriptors;var Q=Object.getOwnPropertySymbols;var Z=Object.prototype.hasOwnProperty,B=Object.prototype.propertyIsEnumerable;var x=(r,e,a)=>e in r?K(r,e,{enumerable:true,configurable:true,writable:true,value:a}):r[e]=a,I=(r,e)=>{for(var a in e||(e={}))Z.call(e,a)&&x(r,a,e[a]);if(Q)for(var a of Q(e))B.call(e,a)&&x(r,a,e[a]);return r},S=(r,e)=>U(r,X(e));var {width:ne,height:_}=reactNative.Dimensions.get("window"),oe="dialog",se=(r,e)=>`rgba(0,0,0,${e!=null?e:r==="dark"?.8:.5})`,ue=({visible:r,onClose:e,onOpen:a,onClosed:d,theme:g="light",backdropOpacity:w,animationDuration:f=220,closeOnBackdropPress:j=true,closeOnBackButton:z=true,enableSwipeToClose:m=true,swipeThreshold:R=80,unmountOnExit:M=true,avoidKeyboard:v=false,children:E,renderBackdrop:D,onBackdropPress:k,testID:T="quicklook",accessibilityLabel:Y="Quick look",stylesOverride:b={}})=>{let[u,L]=react.useState(false),[$,A]=react.useState(0),n=react.useRef(new reactNative.Animated.Value(0)).current,o=react.useRef(new reactNative.Animated.Value(.96)).current,s=react.useRef(new reactNative.Animated.Value(0)).current,G=react.useMemo(()=>se(g,w),[g,w]),l=react.useRef("programmatic");react.useEffect(()=>{r&&!u?(L(true),a==null||a(),requestAnimationFrame(()=>{reactNative.Animated.parallel([reactNative.Animated.timing(n,{toValue:1,duration:f,useNativeDriver:true}),reactNative.Animated.spring(o,{toValue:1,useNativeDriver:true,friction:7,tension:80})]).start();})):!r&&u&&reactNative.Animated.parallel([reactNative.Animated.timing(n,{toValue:0,duration:f,useNativeDriver:true}),reactNative.Animated.timing(o,{toValue:.96,duration:f,useNativeDriver:true})]).start(()=>{s.setValue(0),d==null||d(l.current),l.current="programmatic",M&&L(false);});},[r,u,f,n,o,s,M,a,d]),react.useEffect(()=>{if(!v)return;let y=reactNative.Keyboard.addListener("keyboardDidShow",p=>{var N,P;return A((P=(N=p.endCoordinates)==null?void 0:N.height)!=null?P:0)}),i=reactNative.Keyboard.addListener("keyboardDidHide",()=>A(0));return ()=>{y.remove(),i.remove();}},[v]);let J=react.useRef(reactNative.PanResponder.create({onMoveShouldSetPanResponder:(y,i)=>m&&Math.abs(i.dy)>Math.abs(i.dx)&&Math.abs(i.dy)>5,onPanResponderMove:(y,i)=>{if(m&&i.dy>0){s.setValue(i.dy);let p=Math.min(1,i.dy/(R*2));n.setValue(1-p*.25),o.setValue(1-p*.03);}},onPanResponderRelease:(y,i)=>{m&&(i.dy>R?(l.current="swipe",reactNative.Animated.parallel([reactNative.Animated.timing(s,{toValue:_*.35,duration:180,useNativeDriver:true}),reactNative.Animated.timing(n,{toValue:0,duration:180,useNativeDriver:true})]).start(()=>e==null?void 0:e())):reactNative.Animated.parallel([reactNative.Animated.spring(s,{toValue:0,useNativeDriver:true}),reactNative.Animated.spring(n,{toValue:1,useNativeDriver:true}),reactNative.Animated.spring(o,{toValue:1,useNativeDriver:true})]).start());}})).current;return !u&&!r?null:jsxRuntime.jsx(reactNative.Modal,{transparent:true,visible:u||r,animationType:"none",onRequestClose:()=>{z&&(l.current="backButton",e==null||e());},presentationStyle:"overFullScreen",statusBarTranslucent:reactNative.Platform.OS==="android",children:jsxRuntime.jsxs(reactNative.Animated.View,{style:[V.overlay,{backgroundColor:G,opacity:n},b.overlay],accessibilityViewIsModal:true,children:[D?D(n):jsxRuntime.jsx(reactNative.Pressable,{onPress:()=>{k==null||k(),j&&(l.current="backdrop",e==null||e());},style:reactNative.StyleSheet.absoluteFill,android_disableSound:true,accessibilityRole:"button",accessibilityLabel:"Close quick look",testID:"ql-backdrop"}),jsxRuntime.jsx(reactNative.View,{style:[V.centerWrap,{paddingBottom:Math.max(16,v?$:16)},b.centerWrap],pointerEvents:"box-none",children:jsxRuntime.jsx(reactNative.Animated.View,S(I({},m?J.panHandlers:{}),{style:[V.container,{transform:[{scale:o},{translateY:s}]},b.container],testID:T,accessibilityRole:oe,accessibilityViewIsModal:true,accessibilityLabel:Y,children:E}))})]})})},V=reactNative.StyleSheet.create({overlay:{flex:1,justifyContent:"center",alignItems:"center"},centerWrap:{flex:1,width:"100%",justifyContent:"center",alignItems:"center",paddingHorizontal:16},container:{width:Math.min(ne*.92,560),maxHeight:_*.82,borderRadius:16,overflow:"hidden",elevation:8,shadowColor:"#000",shadowOpacity:.2,shadowRadius:12,shadowOffset:{width:0,height:6},backgroundColor:"#fff"}});exports.QuickLook=ue;//# sourceMappingURL=index.js.map
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/QuickLook.tsx"],"names":["screenWidth","screenHeight","Dimensions","dialogA11yRole","overlayColor","theme","custom","QuickLook","visible","onClose","onOpen","onClosed","backdropOpacity","animationDuration","closeOnBackdropPress","closeOnBackButton","enableSwipeToClose","swipeThreshold","unmountOnExit","avoidKeyboard","children","renderBackdrop","onBackdropPress","testID","accessibilityLabel","stylesOverride","mounted","setMounted","useState","kb","setKb","fade","useRef","Animated","scale","translateY","baseOverlay","useMemo","closeReasonRef","useEffect","show","Keyboard","e","_a","_b","hide","panResponder","PanResponder","_evt","g","jsx","Modal","Platform","jsxs","styles","Pressable","StyleSheet","View","__spreadProps","__spreadValues"],"mappings":"oHACA,IAAA,CAAA,CAAA,MAAA,CAAA,cAAA,CAAA,CAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,IAAA,CAAA,CAAA,MAAA,CAAA,yBAAA,CAAA,IAAA,CAAA,CAAA,MAAA,CAAA,qBAAA,CAAA,IAAA,CAAA,CAAA,MAAA,CAAA,SAAA,CAAA,cAAA,CAAA,CAAA,CAAA,MAAA,CAAA,SAAA,CAAA,oBAAA,CAAA,IAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,IAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,UAAA,CAAA,IAAA,CAAA,YAAA,CAAA,IAAA,CAAA,QAAA,CAAA,IAAA,CAAA,KAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,IAAA,IAAA,CAAA,IAAA,CAAA,GAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,IAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,CAAA,IAAA,IAAA,CAAA,IAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,IAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,OAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAeA,IAAM,CAAE,MAAOA,EAAAA,CAAa,MAAA,CAAQC,CAAa,CAAA,CAAIC,sBAAAA,CAAW,GAAA,CAAI,QAAQ,EAGtEC,EAAAA,CAAiB,QAAA,CAEjBC,GAAe,CAACC,CAAAA,CAAkBC,IACtC,CAAA,WAAA,EAAcA,CAAAA,EAAA,IAAA,CAAAA,CAAAA,CAAWD,IAAU,MAAA,CAAS,EAAA,CAAM,EAAI,CAAA,CAAA,CAAA,CAE3CE,EAAAA,CAAsC,CAAC,CAClD,OAAA,CAAAC,CAAAA,CACA,OAAA,CAAAC,EACA,MAAA,CAAAC,CAAAA,CACA,SAAAC,CAAAA,CACA,KAAA,CAAAN,EAAQ,OAAA,CACR,eAAA,CAAAO,EACA,iBAAA,CAAAC,CAAAA,CAAoB,IACpB,oBAAA,CAAAC,CAAAA,CAAuB,KACvB,iBAAA,CAAAC,CAAAA,CAAoB,KACpB,kBAAA,CAAAC,CAAAA,CAAqB,IAAA,CACrB,cAAA,CAAAC,EAAiB,EAAA,CACjB,aAAA,CAAAC,EAAgB,IAAA,CAChB,aAAA,CAAAC,EAAgB,KAAA,CAChB,QAAA,CAAAC,CAAAA,CACA,cAAA,CAAAC,EACA,eAAA,CAAAC,CAAAA,CACA,OAAAC,CAAAA,CAAS,WAAA,CACT,mBAAAC,CAAAA,CAAqB,YAAA,CACrB,cAAA,CAAAC,CAAAA,CAAiB,EACnB,CAAA,GAAM,CACJ,GAAM,CAACC,EAASC,CAAU,CAAA,CAAIC,eAAS,KAAK,CAAA,CACtC,CAACC,CAAAA,CAAIC,CAAK,EAAIF,cAAAA,CAAS,CAAC,EAExBG,CAAAA,CAAOC,YAAAA,CAAO,IAAIC,oBAAAA,CAAS,MAAM,CAAC,CAAC,EAAE,OAAA,CACrCC,CAAAA,CAAQF,aAAO,IAAIC,oBAAAA,CAAS,KAAA,CAAM,GAAI,CAAC,CAAA,CAAE,OAAA,CACzCE,EAAaH,YAAAA,CAAO,IAAIC,qBAAS,KAAA,CAAM,CAAC,CAAC,CAAA,CAAE,QAE3CG,CAAAA,CAAcC,aAAAA,CAAQ,IAAMjC,EAAAA,CAAaC,CAAAA,CAAOO,CAAe,CAAA,CAAG,CAACP,EAAOO,CAAe,CAAC,EAG1F0B,CAAAA,CAAiBN,YAAAA,CAAoB,cAAc,CAAA,CAGzDO,eAAAA,CAAU,IAAM,CACV/B,CAAAA,EAAW,CAACkB,CAAAA,EACdC,EAAW,IAAI,CAAA,CACfjB,GAAA,IAAA,EAAAA,CAAAA,EAAAA,CAEA,sBAAsB,IAAM,CAC1BuB,oBAAAA,CAAS,QAAA,CAAS,CAChBA,oBAAAA,CAAS,MAAA,CAAOF,EAAM,CAAE,OAAA,CAAS,EAAG,QAAA,CAAUlB,CAAAA,CAAmB,eAAA,CAAiB,IAAK,CAAC,CAAA,CACxFoB,oBAAAA,CAAS,OAAOC,CAAAA,CAAO,CAAE,QAAS,CAAA,CAAG,eAAA,CAAiB,KAAM,QAAA,CAAU,CAAA,CAAG,QAAS,EAAG,CAAC,CACxF,CAAC,CAAA,CAAE,QACL,CAAC,CAAA,EACQ,CAAC1B,GAAWkB,CAAAA,EACrBO,oBAAAA,CAAS,SAAS,CAChBA,oBAAAA,CAAS,OAAOF,CAAAA,CAAM,CAAE,OAAA,CAAS,CAAA,CAAG,SAAUlB,CAAAA,CAAmB,eAAA,CAAiB,IAAK,CAAC,CAAA,CACxFoB,qBAAS,MAAA,CAAOC,CAAAA,CAAO,CAAE,OAAA,CAAS,IAAM,QAAA,CAAUrB,CAAAA,CAAmB,gBAAiB,IAAK,CAAC,CAC9F,CAAC,CAAA,CAAE,MAAM,IAAM,CACbsB,EAAW,QAAA,CAAS,CAAC,EACrBxB,CAAAA,EAAA,IAAA,EAAAA,EAAW2B,CAAAA,CAAe,OAAA,CAAA,CAC1BA,CAAAA,CAAe,OAAA,CAAU,eACrBpB,CAAAA,EAAeS,CAAAA,CAAW,KAAK,EACrC,CAAC,EAEL,CAAA,CAAG,CAACnB,CAAAA,CAASkB,CAAAA,CAASb,EAAmBkB,CAAAA,CAAMG,CAAAA,CAAOC,EAAYjB,CAAAA,CAAeR,CAAAA,CAAQC,CAAQ,CAAC,CAAA,CAGlG4B,eAAAA,CAAU,IAAM,CACd,GAAI,CAACpB,EAAe,OACpB,IAAMqB,EAAOC,oBAAAA,CAAS,WAAA,CAAY,kBAAoBC,CAAAA,EAAG,CArF7D,IAAAC,CAAAA,CAAAC,CAAAA,CAqFgE,OAAAd,CAAAA,CAAAA,CAAMc,CAAAA,CAAAA,CAAAD,EAAAD,CAAAA,CAAE,cAAA,GAAF,IAAA,CAAA,MAAA,CAAAC,CAAAA,CAAkB,SAAlB,IAAA,CAAAC,CAAAA,CAA4B,CAAC,CAAA,CAAC,CAAA,CAC1FC,EAAOJ,oBAAAA,CAAS,WAAA,CAAY,kBAAmB,IAAMX,CAAAA,CAAM,CAAC,CAAC,CAAA,CACnE,OAAO,IAAM,CACXU,EAAK,MAAA,EAAO,CACZK,CAAAA,CAAK,MAAA,GACP,CACF,CAAA,CAAG,CAAC1B,CAAa,CAAC,EAGlB,IAAM2B,CAAAA,CAAed,aACnBe,wBAAAA,CAAa,MAAA,CAAO,CAClB,2BAAA,CAA6B,CAACC,EAAMC,CAAAA,GAClCjC,CAAAA,EAAsB,KAAK,GAAA,CAAIiC,CAAAA,CAAE,EAAE,CAAA,CAAI,KAAK,GAAA,CAAIA,CAAAA,CAAE,EAAE,CAAA,EAAK,IAAA,CAAK,IAAIA,CAAAA,CAAE,EAAE,CAAA,CAAI,CAAA,CAC5E,mBAAoB,CAACD,CAAAA,CAAMC,IAAM,CAC/B,GAAKjC,GACDiC,CAAAA,CAAE,EAAA,CAAK,CAAA,CAAG,CACZd,EAAW,QAAA,CAASc,CAAAA,CAAE,EAAE,CAAA,CACxB,IAAM,EAAI,IAAA,CAAK,GAAA,CAAI,EAAGA,CAAAA,CAAE,EAAA,EAAMhC,EAAiB,CAAA,CAAE,CAAA,CACjDc,EAAK,QAAA,CAAS,CAAA,CAAI,EAAI,GAAI,CAAA,CAC1BG,CAAAA,CAAM,QAAA,CAAS,EAAI,CAAA,CAAI,GAAI,EAC7B,CACF,CAAA,CACA,sBAAuB,CAACc,CAAAA,CAAMC,CAAAA,GAAM,CAC7BjC,IACDiC,CAAAA,CAAE,EAAA,CAAKhC,GACTqB,CAAAA,CAAe,OAAA,CAAU,QACzBL,oBAAAA,CAAS,QAAA,CAAS,CAChBA,oBAAAA,CAAS,OAAOE,CAAAA,CAAY,CAAE,QAASlC,CAAAA,CAAe,GAAA,CAAM,SAAU,GAAA,CAAK,eAAA,CAAiB,IAAK,CAAC,CAAA,CAClGgC,qBAAS,MAAA,CAAOF,CAAAA,CAAM,CAAE,OAAA,CAAS,CAAA,CAAG,SAAU,GAAA,CAAK,eAAA,CAAiB,IAAK,CAAC,CAC5E,CAAC,CAAA,CAAE,MAAM,IAAMtB,CAAAA,EAAA,YAAAA,CAAAA,EAAW,CAAA,EAE1BwB,oBAAAA,CAAS,QAAA,CAAS,CAChBA,oBAAAA,CAAS,MAAA,CAAOE,EAAY,CAAE,OAAA,CAAS,EAAG,eAAA,CAAiB,IAAK,CAAC,CAAA,CACjEF,qBAAS,MAAA,CAAOF,CAAAA,CAAM,CAAE,OAAA,CAAS,CAAA,CAAG,gBAAiB,IAAK,CAAC,EAC3DE,oBAAAA,CAAS,MAAA,CAAOC,EAAO,CAAE,OAAA,CAAS,EAAG,eAAA,CAAiB,IAAK,CAAC,CAC9D,CAAC,CAAA,CAAE,KAAA,IAEP,CACF,CAAC,CACH,CAAA,CAAE,OAAA,CAEF,OAAI,CAACR,CAAAA,EAAW,CAAClB,CAAAA,CAAgB,IAAA,CAG/B0C,eAACC,iBAAAA,CAAA,CACC,YAAW,IAAA,CACX,OAAA,CAASzB,GAAWlB,CAAAA,CACpB,aAAA,CAAc,MAAA,CACd,cAAA,CAAgB,IAAM,CAChBO,CAAAA,GACFuB,EAAe,OAAA,CAAU,YAAA,CACzB7B,GAAA,IAAA,EAAAA,CAAAA,EAAAA,EAEJ,EACA,iBAAA,CAAkB,gBAAA,CAClB,qBAAsB2C,oBAAAA,CAAS,EAAA,GAAO,UAGtC,QAAA,CAAAC,eAAAA,CAACpB,qBAAS,IAAA,CAAT,CACC,KAAA,CAAO,CACLqB,EAAO,OAAA,CACP,CAAE,gBAAiBlB,CAAAA,CAAa,OAAA,CAASL,CAAK,CAAA,CAC9CN,CAAAA,CAAe,OACjB,CAAA,CACA,yBAAwB,IAAA,CAEvB,QAAA,CAAA,CAAAJ,EAECA,CAAAA,CAAeU,CAAyD,EAExEmB,cAAAA,CAACK,qBAAAA,CAAA,CACC,OAAA,CAAS,IAAM,CACbjC,CAAAA,EAAA,MAAAA,CAAAA,EAAAA,CACIR,CAAAA,GACFwB,EAAe,OAAA,CAAU,UAAA,CACzB7B,GAAA,IAAA,EAAAA,CAAAA,EAAAA,EAEJ,EACA,KAAA,CAAO+C,sBAAAA,CAAW,aAClB,oBAAA,CAAoB,IAAA,CACpB,kBAAkB,QAAA,CAClB,kBAAA,CAAmB,kBAAA,CACnB,MAAA,CAAO,cACT,CAAA,CAIFN,cAAAA,CAACO,iBAAA,CACC,KAAA,CAAO,CACLH,CAAAA,CAAO,UAAA,CACP,CAAE,aAAA,CAAe,KAAK,GAAA,CAAI,EAAA,CAAInC,EAAgBU,CAAAA,CAAK,EAAE,CAAE,CAAA,CACvDJ,CAAAA,CAAe,UACjB,CAAA,CACA,cAAc,UAAA,CAEd,QAAA,CAAAyB,eAACjB,oBAAAA,CAAS,IAAA,CAATyB,EAAAC,CAAAA,CAAA,EAAA,CACM3C,EAAqB8B,CAAAA,CAAa,WAAA,CAAc,EAAC,CAAA,CADvD,CAEC,MAAO,CACLQ,CAAAA,CAAO,UACP,CAAE,SAAA,CAAW,CAAC,CAAE,MAAApB,CAAM,CAAA,CAAG,CAAE,UAAA,CAAAC,CAAW,CAAC,CAAE,CAAA,CACzCV,CAAAA,CAAe,SACjB,EACA,MAAA,CAAQF,CAAAA,CACR,kBAAmBpB,EAAAA,CACnB,wBAAA,CAAwB,KACxB,kBAAA,CAAoBqB,CAAAA,CAEnB,QAAA,CAAAJ,CAAAA,CAAAA,CACH,EACF,CAAA,CAAA,CACF,CAAA,CACF,CAEJ,CAAA,CAEMkC,CAAAA,CAASE,uBAAW,MAAA,CAAO,CAC/B,QAAS,CAAE,IAAA,CAAM,EAAG,cAAA,CAAgB,QAAA,CAAU,WAAY,QAAS,CAAA,CACnE,WAAY,CACV,IAAA,CAAM,CAAA,CACN,KAAA,CAAO,OACP,cAAA,CAAgB,QAAA,CAChB,WAAY,QAAA,CACZ,iBAAA,CAAmB,EACrB,CAAA,CACA,SAAA,CAAW,CACT,KAAA,CAAO,IAAA,CAAK,IAAIxD,EAAAA,CAAc,GAAA,CAAM,GAAG,CAAA,CACvC,SAAA,CAAWC,EAAe,GAAA,CAC1B,YAAA,CAAc,EAAA,CACd,QAAA,CAAU,SACV,SAAA,CAAW,CAAA,CACX,YAAa,MAAA,CACb,aAAA,CAAe,GACf,YAAA,CAAc,EAAA,CACd,aAAc,CAAE,KAAA,CAAO,EAAG,MAAA,CAAQ,CAAE,EACpC,eAAA,CAAiB,MACnB,CACF,CAAC","file":"index.js","sourcesContent":["// src/QuickLook.tsx\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n View,\n StyleSheet,\n Modal,\n Dimensions,\n Animated,\n Platform,\n Pressable,\n PanResponder,\n Keyboard,\n} from 'react-native';\nimport type { AccessibilityRole } from 'react-native';\nimport type { CloseReason, QuickLookProps, ThemeMode } from './QuickLookProperties';\n\nconst { width: screenWidth, height: screenHeight } = Dimensions.get('window');\n\n// TS-safe across RN versions where \"dialog\" may not exist in AccessibilityRole\nconst dialogA11yRole = 'dialog' as unknown as AccessibilityRole;\n\nconst overlayColor = (theme: ThemeMode, custom?: number) =>\n `rgba(0,0,0,${custom ?? (theme === 'dark' ? 0.8 : 0.5)})`;\n\nexport const QuickLook: React.FC<QuickLookProps> = ({\n visible,\n onClose,\n onOpen,\n onClosed,\n theme = 'light',\n backdropOpacity,\n animationDuration = 220,\n closeOnBackdropPress = true,\n closeOnBackButton = true,\n enableSwipeToClose = true,\n swipeThreshold = 80,\n unmountOnExit = true,\n avoidKeyboard = false,\n children,\n renderBackdrop,\n onBackdropPress,\n testID = 'quicklook',\n accessibilityLabel = 'Quick look',\n stylesOverride = {},\n}) => {\n const [mounted, setMounted] = useState(false);\n const [kb, setKb] = useState(0); // keyboard height\n\n const fade = useRef(new Animated.Value(0)).current;\n const scale = useRef(new Animated.Value(0.96)).current;\n const translateY = useRef(new Animated.Value(0)).current;\n\n const baseOverlay = useMemo(() => overlayColor(theme, backdropOpacity), [theme, backdropOpacity]);\n\n // Track why we closed so we can report it to onClosed\n const closeReasonRef = useRef<CloseReason>('programmatic');\n\n // Mount/unmount with animation\n useEffect(() => {\n if (visible && !mounted) {\n setMounted(true);\n onOpen?.(); // great place to trigger optional haptics in-app\n\n requestAnimationFrame(() => {\n Animated.parallel([\n Animated.timing(fade, { toValue: 1, duration: animationDuration, useNativeDriver: true }),\n Animated.spring(scale, { toValue: 1, useNativeDriver: true, friction: 7, tension: 80 }),\n ]).start();\n });\n } else if (!visible && mounted) {\n Animated.parallel([\n Animated.timing(fade, { toValue: 0, duration: animationDuration, useNativeDriver: true }),\n Animated.timing(scale, { toValue: 0.96, duration: animationDuration, useNativeDriver: true }),\n ]).start(() => {\n translateY.setValue(0);\n onClosed?.(closeReasonRef.current);\n closeReasonRef.current = 'programmatic';\n if (unmountOnExit) setMounted(false);\n });\n }\n }, [visible, mounted, animationDuration, fade, scale, translateY, unmountOnExit, onOpen, onClosed]);\n\n // Keyboard avoidance (optional)\n useEffect(() => {\n if (!avoidKeyboard) return;\n const show = Keyboard.addListener('keyboardDidShow', (e) => setKb(e.endCoordinates?.height ?? 0));\n const hide = Keyboard.addListener('keyboardDidHide', () => setKb(0));\n return () => {\n show.remove();\n hide.remove();\n };\n }, [avoidKeyboard]);\n\n // Swipe to close\n const panResponder = useRef(\n PanResponder.create({\n onMoveShouldSetPanResponder: (_evt, g) =>\n enableSwipeToClose && Math.abs(g.dy) > Math.abs(g.dx) && Math.abs(g.dy) > 5,\n onPanResponderMove: (_evt, g) => {\n if (!enableSwipeToClose) return;\n if (g.dy > 0) {\n translateY.setValue(g.dy);\n const p = Math.min(1, g.dy / (swipeThreshold * 2));\n fade.setValue(1 - p * 0.25);\n scale.setValue(1 - p * 0.03);\n }\n },\n onPanResponderRelease: (_evt, g) => {\n if (!enableSwipeToClose) return;\n if (g.dy > swipeThreshold) {\n closeReasonRef.current = 'swipe';\n Animated.parallel([\n Animated.timing(translateY, { toValue: screenHeight * 0.35, duration: 180, useNativeDriver: true }),\n Animated.timing(fade, { toValue: 0, duration: 180, useNativeDriver: true }),\n ]).start(() => onClose?.());\n } else {\n Animated.parallel([\n Animated.spring(translateY, { toValue: 0, useNativeDriver: true }),\n Animated.spring(fade, { toValue: 1, useNativeDriver: true }),\n Animated.spring(scale, { toValue: 1, useNativeDriver: true }),\n ]).start();\n }\n },\n })\n ).current;\n\n if (!mounted && !visible) return null;\n\n return (\n <Modal\n transparent\n visible={mounted || visible}\n animationType=\"none\"\n onRequestClose={() => {\n if (closeOnBackButton) {\n closeReasonRef.current = 'backButton';\n onClose?.();\n }\n }}\n presentationStyle=\"overFullScreen\"\n statusBarTranslucent={Platform.OS === 'android'}\n >\n {/* Backdrop */}\n <Animated.View\n style={[\n styles.overlay,\n { backgroundColor: baseOverlay, opacity: fade },\n stylesOverride.overlay,\n ]}\n accessibilityViewIsModal\n >\n {renderBackdrop ? (\n // If you provide a custom backdrop, you control its hit area/closing\n renderBackdrop(fade as unknown as Animated.AnimatedInterpolation<number>)\n ) : (\n <Pressable\n onPress={() => {\n onBackdropPress?.();\n if (closeOnBackdropPress) {\n closeReasonRef.current = 'backdrop';\n onClose?.();\n }\n }}\n style={StyleSheet.absoluteFill}\n android_disableSound\n accessibilityRole=\"button\"\n accessibilityLabel=\"Close quick look\"\n testID=\"ql-backdrop\"\n />\n )}\n\n {/* Centered card */}\n <View\n style={[\n styles.centerWrap,\n { paddingBottom: Math.max(16, avoidKeyboard ? kb : 16) },\n stylesOverride.centerWrap,\n ]}\n pointerEvents=\"box-none\"\n >\n <Animated.View\n {...(enableSwipeToClose ? panResponder.panHandlers : {})}\n style={[\n styles.container,\n { transform: [{ scale }, { translateY }] },\n stylesOverride.container,\n ]}\n testID={testID}\n accessibilityRole={dialogA11yRole}\n accessibilityViewIsModal\n accessibilityLabel={accessibilityLabel}\n >\n {children}\n </Animated.View>\n </View>\n </Animated.View>\n </Modal>\n );\n};\n\nconst styles = StyleSheet.create({\n overlay: { flex: 1, justifyContent: 'center', alignItems: 'center' },\n centerWrap: {\n flex: 1,\n width: '100%',\n justifyContent: 'center',\n alignItems: 'center',\n paddingHorizontal: 16,\n },\n container: {\n width: Math.min(screenWidth * 0.92, 560),\n maxHeight: screenHeight * 0.82,\n borderRadius: 16,\n overflow: 'hidden',\n elevation: 8,\n shadowColor: '#000',\n shadowOpacity: 0.2,\n shadowRadius: 12,\n shadowOffset: { width: 0, height: 6 },\n backgroundColor: '#fff',\n },\n});\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import {useState,useRef,useMemo,useEffect}from'react';import {Dimensions,StyleSheet,Animated,Keyboard,PanResponder,Modal,Pressable,View,Platform}from'react-native';import {jsx,jsxs}from'react/jsx-runtime';var K=Object.defineProperty,U=Object.defineProperties;var X=Object.getOwnPropertyDescriptors;var Q=Object.getOwnPropertySymbols;var Z=Object.prototype.hasOwnProperty,B=Object.prototype.propertyIsEnumerable;var x=(r,e,a)=>e in r?K(r,e,{enumerable:true,configurable:true,writable:true,value:a}):r[e]=a,I=(r,e)=>{for(var a in e||(e={}))Z.call(e,a)&&x(r,a,e[a]);if(Q)for(var a of Q(e))B.call(e,a)&&x(r,a,e[a]);return r},S=(r,e)=>U(r,X(e));var {width:ne,height:_}=Dimensions.get("window"),oe="dialog",se=(r,e)=>`rgba(0,0,0,${e!=null?e:r==="dark"?.8:.5})`,ue=({visible:r,onClose:e,onOpen:a,onClosed:d,theme:g="light",backdropOpacity:w,animationDuration:f=220,closeOnBackdropPress:j=true,closeOnBackButton:z=true,enableSwipeToClose:m=true,swipeThreshold:R=80,unmountOnExit:M=true,avoidKeyboard:v=false,children:E,renderBackdrop:D,onBackdropPress:k,testID:T="quicklook",accessibilityLabel:Y="Quick look",stylesOverride:b={}})=>{let[u,L]=useState(false),[$,A]=useState(0),n=useRef(new Animated.Value(0)).current,o=useRef(new Animated.Value(.96)).current,s=useRef(new Animated.Value(0)).current,G=useMemo(()=>se(g,w),[g,w]),l=useRef("programmatic");useEffect(()=>{r&&!u?(L(true),a==null||a(),requestAnimationFrame(()=>{Animated.parallel([Animated.timing(n,{toValue:1,duration:f,useNativeDriver:true}),Animated.spring(o,{toValue:1,useNativeDriver:true,friction:7,tension:80})]).start();})):!r&&u&&Animated.parallel([Animated.timing(n,{toValue:0,duration:f,useNativeDriver:true}),Animated.timing(o,{toValue:.96,duration:f,useNativeDriver:true})]).start(()=>{s.setValue(0),d==null||d(l.current),l.current="programmatic",M&&L(false);});},[r,u,f,n,o,s,M,a,d]),useEffect(()=>{if(!v)return;let y=Keyboard.addListener("keyboardDidShow",p=>{var N,P;return A((P=(N=p.endCoordinates)==null?void 0:N.height)!=null?P:0)}),i=Keyboard.addListener("keyboardDidHide",()=>A(0));return ()=>{y.remove(),i.remove();}},[v]);let J=useRef(PanResponder.create({onMoveShouldSetPanResponder:(y,i)=>m&&Math.abs(i.dy)>Math.abs(i.dx)&&Math.abs(i.dy)>5,onPanResponderMove:(y,i)=>{if(m&&i.dy>0){s.setValue(i.dy);let p=Math.min(1,i.dy/(R*2));n.setValue(1-p*.25),o.setValue(1-p*.03);}},onPanResponderRelease:(y,i)=>{m&&(i.dy>R?(l.current="swipe",Animated.parallel([Animated.timing(s,{toValue:_*.35,duration:180,useNativeDriver:true}),Animated.timing(n,{toValue:0,duration:180,useNativeDriver:true})]).start(()=>e==null?void 0:e())):Animated.parallel([Animated.spring(s,{toValue:0,useNativeDriver:true}),Animated.spring(n,{toValue:1,useNativeDriver:true}),Animated.spring(o,{toValue:1,useNativeDriver:true})]).start());}})).current;return !u&&!r?null:jsx(Modal,{transparent:true,visible:u||r,animationType:"none",onRequestClose:()=>{z&&(l.current="backButton",e==null||e());},presentationStyle:"overFullScreen",statusBarTranslucent:Platform.OS==="android",children:jsxs(Animated.View,{style:[V.overlay,{backgroundColor:G,opacity:n},b.overlay],accessibilityViewIsModal:true,children:[D?D(n):jsx(Pressable,{onPress:()=>{k==null||k(),j&&(l.current="backdrop",e==null||e());},style:StyleSheet.absoluteFill,android_disableSound:true,accessibilityRole:"button",accessibilityLabel:"Close quick look",testID:"ql-backdrop"}),jsx(View,{style:[V.centerWrap,{paddingBottom:Math.max(16,v?$:16)},b.centerWrap],pointerEvents:"box-none",children:jsx(Animated.View,S(I({},m?J.panHandlers:{}),{style:[V.container,{transform:[{scale:o},{translateY:s}]},b.container],testID:T,accessibilityRole:oe,accessibilityViewIsModal:true,accessibilityLabel:Y,children:E}))})]})})},V=StyleSheet.create({overlay:{flex:1,justifyContent:"center",alignItems:"center"},centerWrap:{flex:1,width:"100%",justifyContent:"center",alignItems:"center",paddingHorizontal:16},container:{width:Math.min(ne*.92,560),maxHeight:_*.82,borderRadius:16,overflow:"hidden",elevation:8,shadowColor:"#000",shadowOpacity:.2,shadowRadius:12,shadowOffset:{width:0,height:6},backgroundColor:"#fff"}});export{ue as QuickLook};//# sourceMappingURL=index.mjs.map
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/QuickLook.tsx"],"names":["screenWidth","screenHeight","Dimensions","dialogA11yRole","overlayColor","theme","custom","QuickLook","visible","onClose","onOpen","onClosed","backdropOpacity","animationDuration","closeOnBackdropPress","closeOnBackButton","enableSwipeToClose","swipeThreshold","unmountOnExit","avoidKeyboard","children","renderBackdrop","onBackdropPress","testID","accessibilityLabel","stylesOverride","mounted","setMounted","useState","kb","setKb","fade","useRef","Animated","scale","translateY","baseOverlay","useMemo","closeReasonRef","useEffect","show","Keyboard","e","_a","_b","hide","panResponder","PanResponder","_evt","g","jsx","Modal","Platform","jsxs","styles","Pressable","StyleSheet","View","__spreadProps","__spreadValues"],"mappings":"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","file":"index.mjs","sourcesContent":["// src/QuickLook.tsx\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n View,\n StyleSheet,\n Modal,\n Dimensions,\n Animated,\n Platform,\n Pressable,\n PanResponder,\n Keyboard,\n} from 'react-native';\nimport type { AccessibilityRole } from 'react-native';\nimport type { CloseReason, QuickLookProps, ThemeMode } from './QuickLookProperties';\n\nconst { width: screenWidth, height: screenHeight } = Dimensions.get('window');\n\n// TS-safe across RN versions where \"dialog\" may not exist in AccessibilityRole\nconst dialogA11yRole = 'dialog' as unknown as AccessibilityRole;\n\nconst overlayColor = (theme: ThemeMode, custom?: number) =>\n `rgba(0,0,0,${custom ?? (theme === 'dark' ? 0.8 : 0.5)})`;\n\nexport const QuickLook: React.FC<QuickLookProps> = ({\n visible,\n onClose,\n onOpen,\n onClosed,\n theme = 'light',\n backdropOpacity,\n animationDuration = 220,\n closeOnBackdropPress = true,\n closeOnBackButton = true,\n enableSwipeToClose = true,\n swipeThreshold = 80,\n unmountOnExit = true,\n avoidKeyboard = false,\n children,\n renderBackdrop,\n onBackdropPress,\n testID = 'quicklook',\n accessibilityLabel = 'Quick look',\n stylesOverride = {},\n}) => {\n const [mounted, setMounted] = useState(false);\n const [kb, setKb] = useState(0); // keyboard height\n\n const fade = useRef(new Animated.Value(0)).current;\n const scale = useRef(new Animated.Value(0.96)).current;\n const translateY = useRef(new Animated.Value(0)).current;\n\n const baseOverlay = useMemo(() => overlayColor(theme, backdropOpacity), [theme, backdropOpacity]);\n\n // Track why we closed so we can report it to onClosed\n const closeReasonRef = useRef<CloseReason>('programmatic');\n\n // Mount/unmount with animation\n useEffect(() => {\n if (visible && !mounted) {\n setMounted(true);\n onOpen?.(); // great place to trigger optional haptics in-app\n\n requestAnimationFrame(() => {\n Animated.parallel([\n Animated.timing(fade, { toValue: 1, duration: animationDuration, useNativeDriver: true }),\n Animated.spring(scale, { toValue: 1, useNativeDriver: true, friction: 7, tension: 80 }),\n ]).start();\n });\n } else if (!visible && mounted) {\n Animated.parallel([\n Animated.timing(fade, { toValue: 0, duration: animationDuration, useNativeDriver: true }),\n Animated.timing(scale, { toValue: 0.96, duration: animationDuration, useNativeDriver: true }),\n ]).start(() => {\n translateY.setValue(0);\n onClosed?.(closeReasonRef.current);\n closeReasonRef.current = 'programmatic';\n if (unmountOnExit) setMounted(false);\n });\n }\n }, [visible, mounted, animationDuration, fade, scale, translateY, unmountOnExit, onOpen, onClosed]);\n\n // Keyboard avoidance (optional)\n useEffect(() => {\n if (!avoidKeyboard) return;\n const show = Keyboard.addListener('keyboardDidShow', (e) => setKb(e.endCoordinates?.height ?? 0));\n const hide = Keyboard.addListener('keyboardDidHide', () => setKb(0));\n return () => {\n show.remove();\n hide.remove();\n };\n }, [avoidKeyboard]);\n\n // Swipe to close\n const panResponder = useRef(\n PanResponder.create({\n onMoveShouldSetPanResponder: (_evt, g) =>\n enableSwipeToClose && Math.abs(g.dy) > Math.abs(g.dx) && Math.abs(g.dy) > 5,\n onPanResponderMove: (_evt, g) => {\n if (!enableSwipeToClose) return;\n if (g.dy > 0) {\n translateY.setValue(g.dy);\n const p = Math.min(1, g.dy / (swipeThreshold * 2));\n fade.setValue(1 - p * 0.25);\n scale.setValue(1 - p * 0.03);\n }\n },\n onPanResponderRelease: (_evt, g) => {\n if (!enableSwipeToClose) return;\n if (g.dy > swipeThreshold) {\n closeReasonRef.current = 'swipe';\n Animated.parallel([\n Animated.timing(translateY, { toValue: screenHeight * 0.35, duration: 180, useNativeDriver: true }),\n Animated.timing(fade, { toValue: 0, duration: 180, useNativeDriver: true }),\n ]).start(() => onClose?.());\n } else {\n Animated.parallel([\n Animated.spring(translateY, { toValue: 0, useNativeDriver: true }),\n Animated.spring(fade, { toValue: 1, useNativeDriver: true }),\n Animated.spring(scale, { toValue: 1, useNativeDriver: true }),\n ]).start();\n }\n },\n })\n ).current;\n\n if (!mounted && !visible) return null;\n\n return (\n <Modal\n transparent\n visible={mounted || visible}\n animationType=\"none\"\n onRequestClose={() => {\n if (closeOnBackButton) {\n closeReasonRef.current = 'backButton';\n onClose?.();\n }\n }}\n presentationStyle=\"overFullScreen\"\n statusBarTranslucent={Platform.OS === 'android'}\n >\n {/* Backdrop */}\n <Animated.View\n style={[\n styles.overlay,\n { backgroundColor: baseOverlay, opacity: fade },\n stylesOverride.overlay,\n ]}\n accessibilityViewIsModal\n >\n {renderBackdrop ? (\n // If you provide a custom backdrop, you control its hit area/closing\n renderBackdrop(fade as unknown as Animated.AnimatedInterpolation<number>)\n ) : (\n <Pressable\n onPress={() => {\n onBackdropPress?.();\n if (closeOnBackdropPress) {\n closeReasonRef.current = 'backdrop';\n onClose?.();\n }\n }}\n style={StyleSheet.absoluteFill}\n android_disableSound\n accessibilityRole=\"button\"\n accessibilityLabel=\"Close quick look\"\n testID=\"ql-backdrop\"\n />\n )}\n\n {/* Centered card */}\n <View\n style={[\n styles.centerWrap,\n { paddingBottom: Math.max(16, avoidKeyboard ? kb : 16) },\n stylesOverride.centerWrap,\n ]}\n pointerEvents=\"box-none\"\n >\n <Animated.View\n {...(enableSwipeToClose ? panResponder.panHandlers : {})}\n style={[\n styles.container,\n { transform: [{ scale }, { translateY }] },\n stylesOverride.container,\n ]}\n testID={testID}\n accessibilityRole={dialogA11yRole}\n accessibilityViewIsModal\n accessibilityLabel={accessibilityLabel}\n >\n {children}\n </Animated.View>\n </View>\n </Animated.View>\n </Modal>\n );\n};\n\nconst styles = StyleSheet.create({\n overlay: { flex: 1, justifyContent: 'center', alignItems: 'center' },\n centerWrap: {\n flex: 1,\n width: '100%',\n justifyContent: 'center',\n alignItems: 'center',\n paddingHorizontal: 16,\n },\n container: {\n width: Math.min(screenWidth * 0.92, 560),\n maxHeight: screenHeight * 0.82,\n borderRadius: 16,\n overflow: 'hidden',\n elevation: 8,\n shadowColor: '#000',\n shadowOpacity: 0.2,\n shadowRadius: 12,\n shadowOffset: { width: 0, height: 6 },\n backgroundColor: '#fff',\n },\n});\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-quick-preview",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "A beautiful, customizable quick preview modal component for React Native",
|
|
5
|
+
"author": "Oliver Lindblad",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"module": "dist/index.mjs",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"react-native": "dist/index.js",
|
|
12
|
+
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"react-native": "./dist/index.js",
|
|
17
|
+
"import": "./dist/index.mjs",
|
|
18
|
+
"require": "./dist/index.js",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"files": ["dist", "src", "README.md", "LICENSE"],
|
|
26
|
+
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"dev": "tsup --watch",
|
|
30
|
+
"type-check": "tsc --noEmit",
|
|
31
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
32
|
+
"clean": "rimraf dist",
|
|
33
|
+
"prepublishOnly": "rimraf dist && tsup"
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
"keywords": [
|
|
37
|
+
"react-native",
|
|
38
|
+
"modal",
|
|
39
|
+
"preview",
|
|
40
|
+
"quicklook",
|
|
41
|
+
"expo",
|
|
42
|
+
"typescript",
|
|
43
|
+
"ui",
|
|
44
|
+
"component"
|
|
45
|
+
],
|
|
46
|
+
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"react": ">=18.2.0 <21",
|
|
49
|
+
"react-native": ">=0.64.0 <1"
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/react": "^19.0.0",
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
55
|
+
"@typescript-eslint/parser": "^7.18.0",
|
|
56
|
+
"eslint": "^8.57.0",
|
|
57
|
+
"rimraf": "^6.0.0",
|
|
58
|
+
"tsup": "^8.0.0",
|
|
59
|
+
"typescript": "^5.0.0",
|
|
60
|
+
|
|
61
|
+
"react": "19.0.0",
|
|
62
|
+
"react-native": "0.79.5"
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
"overrides": {
|
|
66
|
+
"eslint": "^8.57.0",
|
|
67
|
+
"@typescript-eslint/parser": "^7.18.0",
|
|
68
|
+
"@typescript-eslint/eslint-plugin": "^7.18.0"
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
"repository": {
|
|
72
|
+
"type": "git",
|
|
73
|
+
"url": "https://github.com/Hashtagsmile/react-native-quicklook.git"
|
|
74
|
+
},
|
|
75
|
+
"bugs": {
|
|
76
|
+
"url": "https://github.com/Hashtagsmile/react-native-quicklook/issues"
|
|
77
|
+
},
|
|
78
|
+
"homepage": "https://github.com/Hashtagsmile/react-native-quicklook#readme",
|
|
79
|
+
"publishConfig": { "access": "public" },
|
|
80
|
+
"engines": { "node": ">=16" }
|
|
81
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// src/QuickLook.tsx
|
|
2
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
View,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Modal,
|
|
7
|
+
Dimensions,
|
|
8
|
+
Animated,
|
|
9
|
+
Platform,
|
|
10
|
+
Pressable,
|
|
11
|
+
PanResponder,
|
|
12
|
+
Keyboard,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import type { AccessibilityRole } from 'react-native';
|
|
15
|
+
import type { CloseReason, QuickLookProps, ThemeMode } from './QuickLookProperties';
|
|
16
|
+
|
|
17
|
+
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
|
18
|
+
|
|
19
|
+
// TS-safe across RN versions where "dialog" may not exist in AccessibilityRole
|
|
20
|
+
const dialogA11yRole = 'dialog' as unknown as AccessibilityRole;
|
|
21
|
+
|
|
22
|
+
const overlayColor = (theme: ThemeMode, custom?: number) =>
|
|
23
|
+
`rgba(0,0,0,${custom ?? (theme === 'dark' ? 0.8 : 0.5)})`;
|
|
24
|
+
|
|
25
|
+
export const QuickLook: React.FC<QuickLookProps> = ({
|
|
26
|
+
visible,
|
|
27
|
+
onClose,
|
|
28
|
+
onOpen,
|
|
29
|
+
onClosed,
|
|
30
|
+
theme = 'light',
|
|
31
|
+
backdropOpacity,
|
|
32
|
+
animationDuration = 220,
|
|
33
|
+
closeOnBackdropPress = true,
|
|
34
|
+
closeOnBackButton = true,
|
|
35
|
+
enableSwipeToClose = true,
|
|
36
|
+
swipeThreshold = 80,
|
|
37
|
+
unmountOnExit = true,
|
|
38
|
+
avoidKeyboard = false,
|
|
39
|
+
children,
|
|
40
|
+
renderBackdrop,
|
|
41
|
+
onBackdropPress,
|
|
42
|
+
testID = 'quicklook',
|
|
43
|
+
accessibilityLabel = 'Quick look',
|
|
44
|
+
stylesOverride = {},
|
|
45
|
+
}) => {
|
|
46
|
+
const [mounted, setMounted] = useState(false);
|
|
47
|
+
const [kb, setKb] = useState(0); // keyboard height
|
|
48
|
+
|
|
49
|
+
const fade = useRef(new Animated.Value(0)).current;
|
|
50
|
+
const scale = useRef(new Animated.Value(0.96)).current;
|
|
51
|
+
const translateY = useRef(new Animated.Value(0)).current;
|
|
52
|
+
|
|
53
|
+
const baseOverlay = useMemo(() => overlayColor(theme, backdropOpacity), [theme, backdropOpacity]);
|
|
54
|
+
|
|
55
|
+
// Track why we closed so we can report it to onClosed
|
|
56
|
+
const closeReasonRef = useRef<CloseReason>('programmatic');
|
|
57
|
+
|
|
58
|
+
// Mount/unmount with animation
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (visible && !mounted) {
|
|
61
|
+
setMounted(true);
|
|
62
|
+
onOpen?.(); // great place to trigger optional haptics in-app
|
|
63
|
+
|
|
64
|
+
requestAnimationFrame(() => {
|
|
65
|
+
Animated.parallel([
|
|
66
|
+
Animated.timing(fade, { toValue: 1, duration: animationDuration, useNativeDriver: true }),
|
|
67
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: true, friction: 7, tension: 80 }),
|
|
68
|
+
]).start();
|
|
69
|
+
});
|
|
70
|
+
} else if (!visible && mounted) {
|
|
71
|
+
Animated.parallel([
|
|
72
|
+
Animated.timing(fade, { toValue: 0, duration: animationDuration, useNativeDriver: true }),
|
|
73
|
+
Animated.timing(scale, { toValue: 0.96, duration: animationDuration, useNativeDriver: true }),
|
|
74
|
+
]).start(() => {
|
|
75
|
+
translateY.setValue(0);
|
|
76
|
+
onClosed?.(closeReasonRef.current);
|
|
77
|
+
closeReasonRef.current = 'programmatic';
|
|
78
|
+
if (unmountOnExit) setMounted(false);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}, [visible, mounted, animationDuration, fade, scale, translateY, unmountOnExit, onOpen, onClosed]);
|
|
82
|
+
|
|
83
|
+
// Keyboard avoidance (optional)
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!avoidKeyboard) return;
|
|
86
|
+
const show = Keyboard.addListener('keyboardDidShow', (e) => setKb(e.endCoordinates?.height ?? 0));
|
|
87
|
+
const hide = Keyboard.addListener('keyboardDidHide', () => setKb(0));
|
|
88
|
+
return () => {
|
|
89
|
+
show.remove();
|
|
90
|
+
hide.remove();
|
|
91
|
+
};
|
|
92
|
+
}, [avoidKeyboard]);
|
|
93
|
+
|
|
94
|
+
// Swipe to close
|
|
95
|
+
const panResponder = useRef(
|
|
96
|
+
PanResponder.create({
|
|
97
|
+
onMoveShouldSetPanResponder: (_evt, g) =>
|
|
98
|
+
enableSwipeToClose && Math.abs(g.dy) > Math.abs(g.dx) && Math.abs(g.dy) > 5,
|
|
99
|
+
onPanResponderMove: (_evt, g) => {
|
|
100
|
+
if (!enableSwipeToClose) return;
|
|
101
|
+
if (g.dy > 0) {
|
|
102
|
+
translateY.setValue(g.dy);
|
|
103
|
+
const p = Math.min(1, g.dy / (swipeThreshold * 2));
|
|
104
|
+
fade.setValue(1 - p * 0.25);
|
|
105
|
+
scale.setValue(1 - p * 0.03);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
onPanResponderRelease: (_evt, g) => {
|
|
109
|
+
if (!enableSwipeToClose) return;
|
|
110
|
+
if (g.dy > swipeThreshold) {
|
|
111
|
+
closeReasonRef.current = 'swipe';
|
|
112
|
+
Animated.parallel([
|
|
113
|
+
Animated.timing(translateY, { toValue: screenHeight * 0.35, duration: 180, useNativeDriver: true }),
|
|
114
|
+
Animated.timing(fade, { toValue: 0, duration: 180, useNativeDriver: true }),
|
|
115
|
+
]).start(() => onClose?.());
|
|
116
|
+
} else {
|
|
117
|
+
Animated.parallel([
|
|
118
|
+
Animated.spring(translateY, { toValue: 0, useNativeDriver: true }),
|
|
119
|
+
Animated.spring(fade, { toValue: 1, useNativeDriver: true }),
|
|
120
|
+
Animated.spring(scale, { toValue: 1, useNativeDriver: true }),
|
|
121
|
+
]).start();
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
).current;
|
|
126
|
+
|
|
127
|
+
if (!mounted && !visible) return null;
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Modal
|
|
131
|
+
transparent
|
|
132
|
+
visible={mounted || visible}
|
|
133
|
+
animationType="none"
|
|
134
|
+
onRequestClose={() => {
|
|
135
|
+
if (closeOnBackButton) {
|
|
136
|
+
closeReasonRef.current = 'backButton';
|
|
137
|
+
onClose?.();
|
|
138
|
+
}
|
|
139
|
+
}}
|
|
140
|
+
presentationStyle="overFullScreen"
|
|
141
|
+
statusBarTranslucent={Platform.OS === 'android'}
|
|
142
|
+
>
|
|
143
|
+
{/* Backdrop */}
|
|
144
|
+
<Animated.View
|
|
145
|
+
style={[
|
|
146
|
+
styles.overlay,
|
|
147
|
+
{ backgroundColor: baseOverlay, opacity: fade },
|
|
148
|
+
stylesOverride.overlay,
|
|
149
|
+
]}
|
|
150
|
+
accessibilityViewIsModal
|
|
151
|
+
>
|
|
152
|
+
{renderBackdrop ? (
|
|
153
|
+
// If you provide a custom backdrop, you control its hit area/closing
|
|
154
|
+
renderBackdrop(fade as unknown as Animated.AnimatedInterpolation<number>)
|
|
155
|
+
) : (
|
|
156
|
+
<Pressable
|
|
157
|
+
onPress={() => {
|
|
158
|
+
onBackdropPress?.();
|
|
159
|
+
if (closeOnBackdropPress) {
|
|
160
|
+
closeReasonRef.current = 'backdrop';
|
|
161
|
+
onClose?.();
|
|
162
|
+
}
|
|
163
|
+
}}
|
|
164
|
+
style={StyleSheet.absoluteFill}
|
|
165
|
+
android_disableSound
|
|
166
|
+
accessibilityRole="button"
|
|
167
|
+
accessibilityLabel="Close quick look"
|
|
168
|
+
testID="ql-backdrop"
|
|
169
|
+
/>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{/* Centered card */}
|
|
173
|
+
<View
|
|
174
|
+
style={[
|
|
175
|
+
styles.centerWrap,
|
|
176
|
+
{ paddingBottom: Math.max(16, avoidKeyboard ? kb : 16) },
|
|
177
|
+
stylesOverride.centerWrap,
|
|
178
|
+
]}
|
|
179
|
+
pointerEvents="box-none"
|
|
180
|
+
>
|
|
181
|
+
<Animated.View
|
|
182
|
+
{...(enableSwipeToClose ? panResponder.panHandlers : {})}
|
|
183
|
+
style={[
|
|
184
|
+
styles.container,
|
|
185
|
+
{ transform: [{ scale }, { translateY }] },
|
|
186
|
+
stylesOverride.container,
|
|
187
|
+
]}
|
|
188
|
+
testID={testID}
|
|
189
|
+
accessibilityRole={dialogA11yRole}
|
|
190
|
+
accessibilityViewIsModal
|
|
191
|
+
accessibilityLabel={accessibilityLabel}
|
|
192
|
+
>
|
|
193
|
+
{children}
|
|
194
|
+
</Animated.View>
|
|
195
|
+
</View>
|
|
196
|
+
</Animated.View>
|
|
197
|
+
</Modal>
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const styles = StyleSheet.create({
|
|
202
|
+
overlay: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
|
203
|
+
centerWrap: {
|
|
204
|
+
flex: 1,
|
|
205
|
+
width: '100%',
|
|
206
|
+
justifyContent: 'center',
|
|
207
|
+
alignItems: 'center',
|
|
208
|
+
paddingHorizontal: 16,
|
|
209
|
+
},
|
|
210
|
+
container: {
|
|
211
|
+
width: Math.min(screenWidth * 0.92, 560),
|
|
212
|
+
maxHeight: screenHeight * 0.82,
|
|
213
|
+
borderRadius: 16,
|
|
214
|
+
overflow: 'hidden',
|
|
215
|
+
elevation: 8,
|
|
216
|
+
shadowColor: '#000',
|
|
217
|
+
shadowOpacity: 0.2,
|
|
218
|
+
shadowRadius: 12,
|
|
219
|
+
shadowOffset: { width: 0, height: 6 },
|
|
220
|
+
backgroundColor: '#fff',
|
|
221
|
+
},
|
|
222
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Animated } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export type ThemeMode = 'light' | 'dark';
|
|
5
|
+
export type CloseReason = 'programmatic' | 'backdrop' | 'backButton' | 'swipe';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export interface QuickLookProps {
|
|
9
|
+
visible: boolean;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
|
|
12
|
+
/** Lifecycle hooks (great for haptics/analytics) */
|
|
13
|
+
onOpen?: () => void; // fires when animating in
|
|
14
|
+
onClosed?: (reason: CloseReason) => void; // fires after fully closed
|
|
15
|
+
|
|
16
|
+
/** Optional: press whole card (prefer explicit CTAs as well) */
|
|
17
|
+
onPressCard?: () => void;
|
|
18
|
+
|
|
19
|
+
theme?: ThemeMode;
|
|
20
|
+
backdropOpacity?: number; // default 0.5 light / 0.8 dark
|
|
21
|
+
animationDuration?: number; // default 220
|
|
22
|
+
|
|
23
|
+
/** Behavior toggles */
|
|
24
|
+
closeOnBackdropPress?: boolean; // default true
|
|
25
|
+
closeOnBackButton?: boolean; // default true
|
|
26
|
+
enableSwipeToClose?: boolean; // default true
|
|
27
|
+
swipeThreshold?: number; // default 80
|
|
28
|
+
unmountOnExit?: boolean; // default true
|
|
29
|
+
avoidKeyboard?: boolean; // default false
|
|
30
|
+
|
|
31
|
+
/** Headless content */
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
|
|
34
|
+
/** Backdrop customization */
|
|
35
|
+
renderBackdrop?: (opacity: Animated.AnimatedInterpolation<number>) => ReactNode;
|
|
36
|
+
onBackdropPress?: () => void;
|
|
37
|
+
|
|
38
|
+
/** Testing & a11y */
|
|
39
|
+
testID?: string;
|
|
40
|
+
accessibilityLabel?: string;
|
|
41
|
+
|
|
42
|
+
/** Outer wrapper style overrides */
|
|
43
|
+
stylesOverride?: {
|
|
44
|
+
overlay?: any;
|
|
45
|
+
container?: any;
|
|
46
|
+
centerWrap?: any;
|
|
47
|
+
};
|
|
48
|
+
}
|
package/src/index.ts
ADDED