@oneplatformdev/react-native-uikit 1.2.0 → 1.2.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/README.md +13 -2
- package/package.json +3 -1
- package/registry/manifest.json +4 -3
- package/src/components/Checkbox/types/types.ts +1 -0
- package/src/components/Checkbox/ui/Checkbox.tsx +25 -8
- package/src/components/Loader/index.ts +2 -2
- package/src/components/PhotoPreview/presenters/usePhotoPreview.ts +49 -0
- package/src/components/PhotoPreview/types/types.ts +15 -4
- package/src/components/PhotoPreview/ui/PhotoPreview.tsx +136 -15
- package/src/components/PhotoPreview/ui/styles.ts +51 -0
package/README.md
CHANGED
|
@@ -313,11 +313,22 @@ Both are controlled: pass `value`/`onValueChange` to `Switch` or `checked`/`onVa
|
|
|
313
313
|
|
|
314
314
|
`Checkbox` defaults to a 22-unit slightly rounded square with a built-in checkmark. Override `size`, state colors, `borderWidth`, and `iconSize`/`iconColor`. Use `shape="circle"` for a radius derived from its size, or `shape="square"` with `borderRadius` (including `0`) for custom corners. Supply an `icon` node to replace the built-in indicator; the consumer owns that node's size and color.
|
|
315
315
|
|
|
316
|
+
Use `interactive={false}` when Checkbox is only a visual indicator inside another pressable row. It renders a non-interactive View, does not handle touches, and leaves checked-state accessibility to the parent row. The default remains interactive.
|
|
317
|
+
|
|
316
318
|
Owned source is available through `npx @oneplatformdev/react-native-uikit add switch` and `npx @oneplatformdev/react-native-uikit add checkbox`. Both are standalone components; Switch uses the existing `react-native-reanimated` peer.
|
|
317
319
|
|
|
318
320
|
## PhotoPreview and EmptyListView
|
|
319
321
|
|
|
320
|
-
`PhotoPreview`
|
|
322
|
+
`PhotoPreview` uses `@d11/react-native-fast-image` for both the thumbnail and optional fullscreen viewer. Install that native peer in the app. It accepts a FastImage source (`{ uri }` or a local `require(...)`) and passes it unchanged to both images, preserving headers, priority, and any explicit cache policy. Without `source.cache`, FastImage uses its own default `immutable` policy (URI-keyed caching); choose `source={{ uri, cache: FastImage.cacheControl.web }}` when HTTP-header-aware caching is needed. The default thumbnail is a 120-unit square with the theme's medium radius. `width`/`height` are design values; `aspectRatio` derives the missing dimension (width wins if both dimensions and a ratio are supplied). `style` applies after container geometry; `imageStyle` applies after the fill-image style. Source, resize-mode, and image-style types follow FastImage rather than React Native `Image`.
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
<PhotoPreview source={{ uri }} />
|
|
326
|
+
<PhotoPreview source={{ uri }} fullscreen closeAccessibilityLabel={closePhotoLabel} />
|
|
327
|
+
<PhotoPreview source={{ uri }} fullscreen closeIcon={<MyCloseIcon />} loadingIndicator={<MyLoader />} />
|
|
328
|
+
<PhotoPreview source={{ uri }} onRemovePress={onRemove} removeIcon={<MyRemoveIcon />} />
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`fullscreen` is opt-in. Pressing opens one built-in Modal with a cached FastImage (`contain` by default), a centered package Loader until load or error, and a close button positioned at the unscaled top safe-area inset plus scaled design spacing. Apps using fullscreen should configure their usual `SafeAreaProvider`. Override the fullscreen background, resize mode, image style, close icon/labels, or loading indicator as needed. With `fullscreen`, `onPress` observes the same thumbnail press after opening; `onFullscreenOpen`/`onFullscreenClose` observe viewer transitions. Without `fullscreen`, `onPress` keeps its prior behavior. The independent remove action never opens fullscreen; `overlay` remains thumbnail-only. A built-in × is used if `removeIcon` or `closeIcon` is omitted. Supply localized accessibility labels for actions.
|
|
321
332
|
|
|
322
333
|
`EmptyListView` centers optional `visual`, `title`, and `description` in available list space. Strings and numbers use themed Typography (`title` and `body` by default); custom React nodes render as supplied. `titleStyle` and `descriptionStyle` apply to built-in text, while custom nodes own their styling. A supplied `action` replaces the built-in Button; otherwise the Button appears only when both `actionLabel` and `onActionPress` are given. The optional `accessibilityLabel` labels the visual without grouping the readable text or action.
|
|
323
334
|
|
|
@@ -335,7 +346,7 @@ Owned source is available with `npx @oneplatformdev/react-native-uikit add photo
|
|
|
335
346
|
|
|
336
347
|
`BottomModal` provides the reference-style slide/drag dismissal and keyboard-height-aware positioning. Supply `children`, `onClose`, and optionally `title`, `customHeader`, and a labeled `closeAction` icon. `Keyboard.dismiss()` and the close animation start together; `closeRef` can trigger the same close path externally. The kit does not render app-specific toasts.
|
|
337
348
|
|
|
338
|
-
Both modes require consumers to install the declared native peers used by these components: `react-native-keyboard-controller`, `react-native-linear-gradient`, `react-native-reanimated`, and `react-native-safe-area-context`. Applications using keyboard-aware or safe-area behavior should configure the providers and native setup required by those libraries. The kit does not bundle those peers or install them through `add`.
|
|
349
|
+
Both modes require consumers to install the declared native peers used by these components: `@d11/react-native-fast-image`, `react-native-keyboard-controller`, `react-native-linear-gradient`, `react-native-reanimated`, and `react-native-safe-area-context`. Applications using keyboard-aware or safe-area behavior should configure the providers and native setup required by those libraries. The kit does not bundle those peers or install them through `add`.
|
|
339
350
|
|
|
340
351
|
## ConnectionContainer
|
|
341
352
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oneplatformdev/react-native-uikit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"types": "src/index.ts",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"validate:registry": "node scripts/validate-registry.cjs"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
+
"@d11/react-native-fast-image": "^8.13.0",
|
|
21
22
|
"react": "*",
|
|
22
23
|
"react-native": "*",
|
|
23
24
|
"react-native-keyboard-controller": "^1.22.5",
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"react-native-safe-area-context": "^5.5.2"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
30
|
+
"@d11/react-native-fast-image": "^8.13.0",
|
|
29
31
|
"@types/react": "^19.2.0",
|
|
30
32
|
"react-native-keyboard-controller": "^1.22.5",
|
|
31
33
|
"react-native-linear-gradient": "^2.8.3",
|
package/registry/manifest.json
CHANGED
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
},
|
|
59
59
|
"photo-preview": {
|
|
60
60
|
"folder": "PhotoPreview",
|
|
61
|
-
"files": ["ui/PhotoPreview.tsx", "ui/styles.ts", "types/types.ts", "index.ts"],
|
|
62
|
-
"componentDependencies": [],
|
|
61
|
+
"files": ["ui/PhotoPreview.tsx", "presenters/usePhotoPreview.ts", "ui/styles.ts", "types/types.ts", "index.ts"],
|
|
62
|
+
"componentDependencies": ["loader"],
|
|
63
63
|
"foundations": ["useUITheme", "useScaling", "UIColors", "UIRadius", "UISpacing", "ScalingFunctions"],
|
|
64
|
-
"dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
|
|
64
|
+
"dependencies": ["react", "react-native", "@d11/react-native-fast-image", "react-native-safe-area-context", "@oneplatformdev/react-native-uikit"],
|
|
65
65
|
"rewrites": {
|
|
66
66
|
"../../../theme": "@oneplatformdev/react-native-uikit",
|
|
67
67
|
"../../../utils": "@oneplatformdev/react-native-uikit"
|
|
@@ -148,6 +148,7 @@
|
|
|
148
148
|
"folder": "Loader",
|
|
149
149
|
"files": ["ui/Loader.tsx", "ui/styles.ts", "types/types.ts", "index.ts"],
|
|
150
150
|
"componentDependencies": [],
|
|
151
|
+
"publicExports": { "Loader": "ui/Loader.tsx", "LoaderProps": "types/types.ts" },
|
|
151
152
|
"foundations": ["useUITheme", "UIColors"],
|
|
152
153
|
"dependencies": ["react", "react-native", "@oneplatformdev/react-native-uikit"],
|
|
153
154
|
"rewrites": {
|
|
@@ -8,6 +8,7 @@ import type { CheckboxProps } from '../types/types';
|
|
|
8
8
|
export const Checkbox = ({
|
|
9
9
|
checked,
|
|
10
10
|
onValueChange,
|
|
11
|
+
interactive = true,
|
|
11
12
|
disabled = false,
|
|
12
13
|
size = 22,
|
|
13
14
|
shape = 'square',
|
|
@@ -47,8 +48,29 @@ export const Checkbox = ({
|
|
|
47
48
|
disabledBackgroundColor, disabledBorderColor, iconSize, iconColor,
|
|
48
49
|
]);
|
|
49
50
|
const onPress = useCallback(() => {
|
|
50
|
-
if (!disabled) onValueChange?.(!checked);
|
|
51
|
-
}, [checked, disabled, onValueChange]);
|
|
51
|
+
if (interactive && !disabled) onValueChange?.(!checked);
|
|
52
|
+
}, [checked, disabled, interactive, onValueChange]);
|
|
53
|
+
|
|
54
|
+
const content = checked ? (
|
|
55
|
+
<View pointerEvents="none">
|
|
56
|
+
{icon === undefined ? <View style={styles.indicator} />
|
|
57
|
+
: typeof icon === 'string' || typeof icon === 'number' ? <Text>{icon}</Text> : icon}
|
|
58
|
+
</View>
|
|
59
|
+
) : null;
|
|
60
|
+
|
|
61
|
+
if (!interactive) {
|
|
62
|
+
return (
|
|
63
|
+
<View
|
|
64
|
+
pointerEvents="none"
|
|
65
|
+
accessible={false}
|
|
66
|
+
accessibilityElementsHidden
|
|
67
|
+
importantForAccessibility="no-hide-descendants"
|
|
68
|
+
style={[styles.container, style]}
|
|
69
|
+
>
|
|
70
|
+
{content}
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
52
74
|
|
|
53
75
|
return (
|
|
54
76
|
<Pressable
|
|
@@ -61,12 +83,7 @@ export const Checkbox = ({
|
|
|
61
83
|
onPress={onPress}
|
|
62
84
|
style={[styles.container, style]}
|
|
63
85
|
>
|
|
64
|
-
{
|
|
65
|
-
<View pointerEvents="none">
|
|
66
|
-
{icon === undefined ? <View style={styles.indicator} />
|
|
67
|
-
: typeof icon === 'string' || typeof icon === 'number' ? <Text>{icon}</Text> : icon}
|
|
68
|
-
</View>
|
|
69
|
-
) : null}
|
|
86
|
+
{content}
|
|
70
87
|
</Pressable>
|
|
71
88
|
);
|
|
72
89
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
1
|
+
export * from './ui/Loader';
|
|
2
|
+
export * from './types/types';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import type { PhotoPreviewProps } from '../types/types';
|
|
3
|
+
|
|
4
|
+
export const usePhotoPreview = ({
|
|
5
|
+
fullscreen,
|
|
6
|
+
disabled,
|
|
7
|
+
onPress,
|
|
8
|
+
onFullscreenOpen,
|
|
9
|
+
onFullscreenClose,
|
|
10
|
+
}: Pick<PhotoPreviewProps, 'fullscreen' | 'disabled' | 'onPress' | 'onFullscreenOpen' | 'onFullscreenClose'>) => {
|
|
11
|
+
const [visible, setVisible] = useState(false);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!fullscreen) setVisible(false);
|
|
15
|
+
}, [fullscreen]);
|
|
16
|
+
|
|
17
|
+
const onThumbnailPress = useCallback(() => {
|
|
18
|
+
if (disabled) return;
|
|
19
|
+
if (fullscreen) {
|
|
20
|
+
setVisible(true);
|
|
21
|
+
onFullscreenOpen?.();
|
|
22
|
+
}
|
|
23
|
+
onPress?.();
|
|
24
|
+
}, [disabled, fullscreen, onFullscreenOpen, onPress]);
|
|
25
|
+
|
|
26
|
+
const onClose = useCallback(() => {
|
|
27
|
+
setVisible(false);
|
|
28
|
+
onFullscreenClose?.();
|
|
29
|
+
}, [onFullscreenClose]);
|
|
30
|
+
|
|
31
|
+
return { visible: fullscreen === true && visible, onThumbnailPress, onClose };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type LoadState = 'loading' | 'loaded' | 'error';
|
|
35
|
+
|
|
36
|
+
export const useFullscreenImage = () => {
|
|
37
|
+
const [loadState, setLoadState] = useState<LoadState>('loading');
|
|
38
|
+
const onLoadStart = useCallback(() => setLoadState('loading'), []);
|
|
39
|
+
const onLoad = useCallback(() => setLoadState('loaded'), []);
|
|
40
|
+
const onError = useCallback(() => setLoadState('error'), []);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
isLoading: loadState === 'loading',
|
|
44
|
+
isImageVisible: loadState === 'loaded',
|
|
45
|
+
onLoadStart,
|
|
46
|
+
onLoad,
|
|
47
|
+
onError,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import type { FastImageProps } from '@d11/react-native-fast-image';
|
|
3
4
|
|
|
4
5
|
export interface PhotoPreviewProps {
|
|
5
|
-
source:
|
|
6
|
+
source: NonNullable<FastImageProps['source']>;
|
|
6
7
|
width?: number;
|
|
7
8
|
height?: number;
|
|
8
9
|
aspectRatio?: number;
|
|
9
10
|
borderRadius?: number;
|
|
10
|
-
resizeMode?:
|
|
11
|
+
resizeMode?: FastImageProps['resizeMode'];
|
|
12
|
+
fullscreen?: boolean;
|
|
13
|
+
fullscreenResizeMode?: FastImageProps['resizeMode'];
|
|
14
|
+
fullscreenBackgroundColor?: string;
|
|
15
|
+
fullscreenImageStyle?: FastImageProps['style'];
|
|
16
|
+
closeIcon?: ReactNode;
|
|
17
|
+
closeAccessibilityLabel?: string;
|
|
18
|
+
closeAccessibilityHint?: string;
|
|
19
|
+
loadingIndicator?: ReactNode;
|
|
20
|
+
onFullscreenOpen?: () => void;
|
|
21
|
+
onFullscreenClose?: () => void;
|
|
11
22
|
overlay?: ReactNode;
|
|
12
23
|
removeIcon?: ReactNode;
|
|
13
24
|
onRemovePress?: () => void;
|
|
@@ -18,5 +29,5 @@ export interface PhotoPreviewProps {
|
|
|
18
29
|
accessibilityHint?: string;
|
|
19
30
|
disabled?: boolean;
|
|
20
31
|
style?: StyleProp<ViewStyle>;
|
|
21
|
-
imageStyle?:
|
|
32
|
+
imageStyle?: FastImageProps['style'];
|
|
22
33
|
}
|
|
@@ -1,17 +1,118 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
+
import FastImage from '@d11/react-native-fast-image';
|
|
5
|
+
import { Loader } from '../../Loader';
|
|
3
6
|
import { useUITheme } from '../../../theme';
|
|
4
7
|
import { useScaling } from '../../../utils';
|
|
5
|
-
import {
|
|
8
|
+
import { useFullscreenImage, usePhotoPreview } from '../presenters/usePhotoPreview';
|
|
9
|
+
import { fastImageStyles, getFullscreenStyles, getStyles } from './styles';
|
|
6
10
|
import type { PhotoPreviewProps } from '../types/types';
|
|
7
11
|
|
|
12
|
+
type FullscreenImageProps = Pick<PhotoPreviewProps, 'source' | 'fullscreenResizeMode' | 'fullscreenImageStyle' | 'loadingIndicator'>
|
|
13
|
+
& { styles: ReturnType<typeof getFullscreenStyles>['styles'] };
|
|
14
|
+
|
|
15
|
+
const FullscreenImage = ({ source, fullscreenResizeMode, fullscreenImageStyle, loadingIndicator, styles }: FullscreenImageProps) => {
|
|
16
|
+
const { isLoading, isImageVisible, onLoadStart, onLoad, onError } = useFullscreenImage();
|
|
17
|
+
const hasSource = typeof source === 'number' || Boolean(source.uri?.trim());
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<View style={styles.imageArea}>
|
|
21
|
+
{hasSource ? (
|
|
22
|
+
<FastImage
|
|
23
|
+
source={source}
|
|
24
|
+
resizeMode={fullscreenResizeMode ?? FastImage.resizeMode.contain}
|
|
25
|
+
accessible={false}
|
|
26
|
+
style={[fastImageStyles.fill, fullscreenImageStyle, !isImageVisible && fastImageStyles.hidden]}
|
|
27
|
+
onLoadStart={onLoadStart}
|
|
28
|
+
onLoad={onLoad}
|
|
29
|
+
onError={onError}
|
|
30
|
+
/>
|
|
31
|
+
) : null}
|
|
32
|
+
{hasSource && isLoading ? (
|
|
33
|
+
<View pointerEvents="none" style={styles.loadingOverlay}>
|
|
34
|
+
{loadingIndicator === undefined ? <Loader inline transparent accessibilityLabel="Loading photo" /> : loadingIndicator}
|
|
35
|
+
</View>
|
|
36
|
+
) : null}
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type FullscreenViewerProps = Pick<PhotoPreviewProps,
|
|
42
|
+
| 'source' | 'fullscreenResizeMode' | 'fullscreenBackgroundColor' | 'fullscreenImageStyle'
|
|
43
|
+
| 'closeIcon' | 'closeAccessibilityLabel' | 'closeAccessibilityHint' | 'loadingIndicator'>
|
|
44
|
+
& { visible: boolean; onClose: () => void };
|
|
45
|
+
|
|
46
|
+
const FullscreenViewer = ({
|
|
47
|
+
source,
|
|
48
|
+
visible,
|
|
49
|
+
onClose,
|
|
50
|
+
fullscreenResizeMode,
|
|
51
|
+
fullscreenBackgroundColor,
|
|
52
|
+
fullscreenImageStyle,
|
|
53
|
+
closeIcon,
|
|
54
|
+
closeAccessibilityLabel,
|
|
55
|
+
closeAccessibilityHint,
|
|
56
|
+
loadingIndicator,
|
|
57
|
+
}: FullscreenViewerProps) => {
|
|
58
|
+
const { colors, spacing } = useUITheme();
|
|
59
|
+
const scaling = useScaling();
|
|
60
|
+
const { top } = useSafeAreaInsets();
|
|
61
|
+
const { styles, closeHitSlop } = useMemo(
|
|
62
|
+
() => getFullscreenStyles(colors, spacing, scaling, top, fullscreenBackgroundColor),
|
|
63
|
+
[colors, spacing, scaling, top, fullscreenBackgroundColor],
|
|
64
|
+
);
|
|
65
|
+
const sourceKey = typeof source === 'number' ? `asset:${source}` : JSON.stringify(source);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Modal visible={visible} animationType="fade" statusBarTranslucent onRequestClose={onClose}>
|
|
69
|
+
<SafeAreaView edges={['left', 'right', 'bottom']} style={styles.fullscreen} accessibilityViewIsModal>
|
|
70
|
+
<View style={styles.toolbar}>
|
|
71
|
+
<Pressable
|
|
72
|
+
accessibilityRole="button"
|
|
73
|
+
accessibilityLabel={closeAccessibilityLabel ?? 'Close photo'}
|
|
74
|
+
accessibilityHint={closeAccessibilityHint}
|
|
75
|
+
hitSlop={closeHitSlop}
|
|
76
|
+
onPress={onClose}
|
|
77
|
+
style={styles.close}
|
|
78
|
+
>
|
|
79
|
+
{closeIcon === undefined ? <Text style={styles.closeText}>×</Text>
|
|
80
|
+
: typeof closeIcon === 'string' || typeof closeIcon === 'number'
|
|
81
|
+
? <Text style={styles.closeText}>{closeIcon}</Text> : closeIcon}
|
|
82
|
+
</Pressable>
|
|
83
|
+
</View>
|
|
84
|
+
{visible ? (
|
|
85
|
+
<FullscreenImage
|
|
86
|
+
key={sourceKey}
|
|
87
|
+
source={source}
|
|
88
|
+
fullscreenResizeMode={fullscreenResizeMode}
|
|
89
|
+
fullscreenImageStyle={fullscreenImageStyle}
|
|
90
|
+
loadingIndicator={loadingIndicator}
|
|
91
|
+
styles={styles}
|
|
92
|
+
/>
|
|
93
|
+
) : null}
|
|
94
|
+
</SafeAreaView>
|
|
95
|
+
</Modal>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
8
99
|
export const PhotoPreview = ({
|
|
9
100
|
source,
|
|
10
101
|
width,
|
|
11
102
|
height,
|
|
12
103
|
aspectRatio,
|
|
13
104
|
borderRadius,
|
|
14
|
-
resizeMode =
|
|
105
|
+
resizeMode = FastImage.resizeMode.cover,
|
|
106
|
+
fullscreen = false,
|
|
107
|
+
fullscreenResizeMode,
|
|
108
|
+
fullscreenBackgroundColor,
|
|
109
|
+
fullscreenImageStyle,
|
|
110
|
+
closeIcon,
|
|
111
|
+
closeAccessibilityLabel,
|
|
112
|
+
closeAccessibilityHint,
|
|
113
|
+
loadingIndicator,
|
|
114
|
+
onFullscreenOpen,
|
|
115
|
+
onFullscreenClose,
|
|
15
116
|
overlay,
|
|
16
117
|
removeIcon,
|
|
17
118
|
onRemovePress,
|
|
@@ -30,28 +131,34 @@ export const PhotoPreview = ({
|
|
|
30
131
|
() => getStyles(colors, radius, spacing, scaling, width, height, aspectRatio, borderRadius),
|
|
31
132
|
[colors, radius, spacing, scaling, width, height, aspectRatio, borderRadius],
|
|
32
133
|
);
|
|
134
|
+
const { visible, onThumbnailPress, onClose } = usePhotoPreview({
|
|
135
|
+
fullscreen, disabled, onPress, onFullscreenOpen, onFullscreenClose,
|
|
136
|
+
});
|
|
137
|
+
const isPressable = fullscreen || onPress !== undefined;
|
|
33
138
|
const image = (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
139
|
+
<View style={styles.image}>
|
|
140
|
+
<FastImage
|
|
141
|
+
source={source}
|
|
142
|
+
resizeMode={resizeMode}
|
|
143
|
+
accessible={!isPressable}
|
|
144
|
+
accessibilityRole={isPressable ? undefined : 'image'}
|
|
145
|
+
accessibilityLabel={isPressable ? undefined : accessibilityLabel}
|
|
146
|
+
accessibilityHint={isPressable ? undefined : accessibilityHint}
|
|
147
|
+
style={[fastImageStyles.fill, imageStyle]}
|
|
148
|
+
/>
|
|
149
|
+
</View>
|
|
43
150
|
);
|
|
44
151
|
|
|
45
152
|
return (
|
|
46
153
|
<View style={[styles.container, style]}>
|
|
47
|
-
{
|
|
154
|
+
{isPressable ? (
|
|
48
155
|
<Pressable
|
|
49
156
|
accessibilityRole="button"
|
|
50
|
-
accessibilityLabel={accessibilityLabel}
|
|
157
|
+
accessibilityLabel={accessibilityLabel ?? 'Open photo'}
|
|
51
158
|
accessibilityHint={accessibilityHint}
|
|
52
159
|
accessibilityState={{ disabled }}
|
|
53
160
|
disabled={disabled}
|
|
54
|
-
onPress={
|
|
161
|
+
onPress={onThumbnailPress}
|
|
55
162
|
style={StyleSheet.absoluteFill}
|
|
56
163
|
>
|
|
57
164
|
{image}
|
|
@@ -78,6 +185,20 @@ export const PhotoPreview = ({
|
|
|
78
185
|
? <Text style={styles.removeText}>{removeIcon}</Text> : removeIcon}
|
|
79
186
|
</Pressable>
|
|
80
187
|
) : null}
|
|
188
|
+
{fullscreen ? (
|
|
189
|
+
<FullscreenViewer
|
|
190
|
+
source={source}
|
|
191
|
+
visible={visible}
|
|
192
|
+
onClose={onClose}
|
|
193
|
+
fullscreenResizeMode={fullscreenResizeMode}
|
|
194
|
+
fullscreenBackgroundColor={fullscreenBackgroundColor}
|
|
195
|
+
fullscreenImageStyle={fullscreenImageStyle}
|
|
196
|
+
closeIcon={closeIcon}
|
|
197
|
+
closeAccessibilityLabel={closeAccessibilityLabel}
|
|
198
|
+
closeAccessibilityHint={closeAccessibilityHint}
|
|
199
|
+
loadingIndicator={loadingIndicator}
|
|
200
|
+
/>
|
|
201
|
+
) : null}
|
|
81
202
|
</View>
|
|
82
203
|
);
|
|
83
204
|
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { StyleSheet } from 'react-native';
|
|
2
|
+
import type { ViewStyle } from 'react-native';
|
|
3
|
+
import type { ImageStyle as FastImageImageStyle } from '@d11/react-native-fast-image';
|
|
2
4
|
import type { UIColors, UIRadius, UISpacing } from '../../../theme';
|
|
3
5
|
import type { ScalingFunctions } from '../../../utils';
|
|
4
6
|
import type { PhotoPreviewProps } from '../types/types';
|
|
@@ -6,6 +8,13 @@ import type { PhotoPreviewProps } from '../types/types';
|
|
|
6
8
|
const positiveOrUndefined = (value: number | undefined) =>
|
|
7
9
|
value !== undefined && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
8
10
|
|
|
11
|
+
type CompatibleFastImageStyle = FastImageImageStyle & Pick<ViewStyle, 'width' | 'height' | 'opacity'>;
|
|
12
|
+
|
|
13
|
+
export const fastImageStyles: Record<'fill' | 'hidden', CompatibleFastImageStyle> = {
|
|
14
|
+
fill: { width: '100%', height: '100%' },
|
|
15
|
+
hidden: { opacity: 0 },
|
|
16
|
+
};
|
|
17
|
+
|
|
9
18
|
export const getStyles = (
|
|
10
19
|
colors: UIColors,
|
|
11
20
|
radius: UIRadius,
|
|
@@ -66,3 +75,45 @@ export const getStyles = (
|
|
|
66
75
|
|
|
67
76
|
return { styles, removeHitSlop };
|
|
68
77
|
};
|
|
78
|
+
|
|
79
|
+
export const getFullscreenStyles = (
|
|
80
|
+
colors: UIColors,
|
|
81
|
+
spacing: UISpacing,
|
|
82
|
+
scaling: ScalingFunctions,
|
|
83
|
+
topInset: number,
|
|
84
|
+
backgroundColor?: string,
|
|
85
|
+
) => {
|
|
86
|
+
const styles = StyleSheet.create({
|
|
87
|
+
fullscreen: {
|
|
88
|
+
flex: 1,
|
|
89
|
+
backgroundColor: backgroundColor ?? colors.text,
|
|
90
|
+
},
|
|
91
|
+
toolbar: {
|
|
92
|
+
alignItems: 'flex-end',
|
|
93
|
+
paddingHorizontal: scaling.scaleHorizontal(spacing.lg),
|
|
94
|
+
paddingTop: topInset + scaling.scaleVertical(spacing.sm),
|
|
95
|
+
},
|
|
96
|
+
close: {
|
|
97
|
+
width: scaling.scaleHorizontal(40),
|
|
98
|
+
height: scaling.scaleHorizontal(40),
|
|
99
|
+
alignItems: 'center',
|
|
100
|
+
justifyContent: 'center',
|
|
101
|
+
},
|
|
102
|
+
closeText: {
|
|
103
|
+
color: colors.onPrimary,
|
|
104
|
+
fontSize: scaling.scaleFontSize(24),
|
|
105
|
+
lineHeight: scaling.scaleLineHeight(28),
|
|
106
|
+
},
|
|
107
|
+
imageArea: {
|
|
108
|
+
flex: 1,
|
|
109
|
+
width: '100%',
|
|
110
|
+
},
|
|
111
|
+
loadingOverlay: {
|
|
112
|
+
...StyleSheet.absoluteFill,
|
|
113
|
+
alignItems: 'center',
|
|
114
|
+
justifyContent: 'center',
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return { styles, closeHitSlop: scaling.scaleHorizontal(spacing.xs) };
|
|
119
|
+
};
|