@os-design-mobile/image-upload 1.0.67 → 1.0.69

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/package.json CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "@os-design-mobile/image-upload",
3
- "version": "1.0.67",
3
+ "version": "1.0.69",
4
4
  "license": "UNLICENSED",
5
5
  "repository": "git@gitlab.com:os-team/libs/os-design-mobile.git",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "react-native": "src/index.tsx",
8
9
  "files": [
9
- "dist"
10
+ "dist",
11
+ "src",
12
+ "!**/*.test.ts",
13
+ "!**/*.test.tsx",
14
+ "!**/__tests__",
15
+ "!**/*.stories.tsx",
16
+ "!**/*.stories.mdx",
17
+ "!**/*.example.tsx"
10
18
  ],
11
19
  "scripts": {
12
20
  "clean": "rimraf dist",
@@ -19,14 +27,14 @@
19
27
  "access": "public"
20
28
  },
21
29
  "dependencies": {
22
- "@os-design-mobile/action-menu": "^1.0.66",
23
- "@os-design-mobile/button": "^1.0.49",
24
- "@os-design-mobile/icons": "^1.0.47",
25
- "@os-design-mobile/image": "^1.0.33",
26
- "@os-design-mobile/text": "^1.0.45",
27
- "@os-design-mobile/theming": "^1.0.33",
28
- "@os-design/omit-emotion-props": "^1.0.11",
29
- "@os-design/use-forwarded-state": "^1.0.13"
30
+ "@os-design-mobile/action-menu": "^1.0.68",
31
+ "@os-design-mobile/button": "^1.0.51",
32
+ "@os-design-mobile/icons": "^1.0.49",
33
+ "@os-design-mobile/image": "^1.0.35",
34
+ "@os-design-mobile/text": "^1.0.47",
35
+ "@os-design-mobile/theming": "^1.0.35",
36
+ "@os-design/omit-emotion-props": "^1.0.13",
37
+ "@os-design/use-forwarded-state": "^1.0.15"
30
38
  },
31
39
  "peerDependencies": {
32
40
  "@emotion/native": ">=11",
@@ -39,5 +47,5 @@
39
47
  "react-native-safe-area-context": ">=3",
40
48
  "react-native-svg": ">=12"
41
49
  },
42
- "gitHead": "4cac95fe2f5cd6dc74885a83b43a730008e20721"
50
+ "gitHead": "ea2e9015ca3ac20795b3fc1303df9016e0c868db"
43
51
  }
@@ -0,0 +1,7 @@
1
+ import '@emotion/react';
2
+ import { Theme as BaseTheme } from '@os-design-mobile/theming';
3
+
4
+ declare module '@emotion/react' {
5
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
6
+ export interface Theme extends BaseTheme {}
7
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,294 @@
1
+ import React, { forwardRef, useCallback, useMemo, useState } from 'react';
2
+ import { View, ViewProps } from 'react-native';
3
+ import { RectButton, RectButtonProps } from 'react-native-gesture-handler';
4
+ import ActionMenu, { MenuItem } from '@os-design-mobile/action-menu';
5
+ import { Picture, Camera, Delete } from '@os-design-mobile/icons';
6
+ import {
7
+ launchImageLibrary,
8
+ launchCamera,
9
+ Asset,
10
+ ImagePickerResponse,
11
+ } from 'react-native-image-picker';
12
+ import Text from '@os-design-mobile/text';
13
+ import Image from '@os-design-mobile/image';
14
+ import useForwardedState from '@os-design/use-forwarded-state';
15
+ import styled from '@emotion/native';
16
+ import omitEmotionProps from '@os-design/omit-emotion-props';
17
+ import { clr, ThemeOverrider, useTheme } from '@os-design-mobile/theming';
18
+ import Button from '@os-design-mobile/button';
19
+ import {
20
+ CameraOptions,
21
+ ImageLibraryOptions,
22
+ } from 'react-native-image-picker/src/types';
23
+ import defaultLocale, { ImageUploadLocale } from './utils/defaultLocale';
24
+
25
+ export interface ImageUploadProps extends RectButtonProps {
26
+ /**
27
+ * The url of the image.
28
+ * @default undefined
29
+ */
30
+ url?: string;
31
+ /**
32
+ * The locale of the component.
33
+ * @default undefined
34
+ */
35
+ locale?: ImageUploadLocale;
36
+ /**
37
+ * The image library options.
38
+ * @default undefined
39
+ */
40
+ imageLibraryOptions?: Pick<
41
+ ImageLibraryOptions,
42
+ 'maxWidth' | 'maxHeight' | 'quality'
43
+ >;
44
+ /**
45
+ * The camera options.
46
+ * @default undefined
47
+ */
48
+ cameraOptions?: Pick<
49
+ CameraOptions,
50
+ 'maxWidth' | 'maxHeight' | 'quality' | 'saveToPhotos' | 'cameraType'
51
+ >;
52
+ /**
53
+ * The selected local file, or null if the image is marked as deleted.
54
+ * @default undefined
55
+ */
56
+ value?: Asset | null;
57
+ /**
58
+ * The default value.
59
+ * @default undefined
60
+ */
61
+ defaultValue?: Asset | null;
62
+ /**
63
+ * The change event handler.
64
+ * @default undefined
65
+ */
66
+ onChange?: (value: Asset | null) => void;
67
+ }
68
+
69
+ const overlayHasImageStyles = (p) =>
70
+ p.hasImage
71
+ ? {
72
+ backgroundColor: clr([0, 0, 0, p.theme.imageUploadOverlayOpacity]),
73
+ borderRadius: p.theme.borderRadius * p.theme.fontSize,
74
+ }
75
+ : {};
76
+
77
+ interface OverlayProps {
78
+ hasImage: boolean;
79
+ }
80
+ const Overlay = styled(
81
+ View,
82
+ omitEmotionProps('hasImage')
83
+ )<OverlayProps>((p) => ({
84
+ position: 'absolute',
85
+ top: 0,
86
+ right: 0,
87
+ bottom: 0,
88
+ left: 0,
89
+ justifyContent: 'center',
90
+ alignItems: 'center',
91
+ padding: p.theme.imageUploadOverlayPadding * p.theme.fontSize,
92
+ ...overlayHasImageStyles(p),
93
+ }));
94
+
95
+ const StyledRectButton = styled(RectButton)((p) => ({
96
+ width: '100%',
97
+ maxWidth: p.theme.imageUploadMaxWidth * p.theme.fontSize,
98
+ minHeight: p.theme.imageUploadMinHeight * p.theme.fontSize,
99
+ borderRadius: p.theme.borderRadius * p.theme.fontSize,
100
+ }));
101
+
102
+ const notHasImageStyles = (p): Partial<ViewProps> =>
103
+ !p.hasImage
104
+ ? ({
105
+ backgroundColor: clr(p.theme.imageUploadNoImageColorBg),
106
+ borderWidth: 2,
107
+ borderColor: clr(p.theme.imageUploadNoImageColorBorder),
108
+ borderStyle: 'dashed',
109
+ } as Partial<ViewProps>)
110
+ : {};
111
+
112
+ interface ContainerProps {
113
+ hasImage: boolean;
114
+ }
115
+
116
+ const Container = styled(
117
+ View,
118
+ omitEmotionProps('hasImage')
119
+ )<ContainerProps>((p) => ({
120
+ minHeight: p.theme.imageUploadMinHeight * p.theme.fontSize,
121
+ borderRadius: p.theme.borderRadius * p.theme.fontSize,
122
+ ...notHasImageStyles(p),
123
+ }));
124
+
125
+ const Content = styled.View({
126
+ flex: 1,
127
+ justifyContent: 'center',
128
+ alignItems: 'center',
129
+ });
130
+
131
+ const Title = styled(Text)((p) => ({
132
+ marginTop: 0.5 * p.theme.fontSize,
133
+ textAlign: 'center',
134
+ }));
135
+
136
+ const StyledImage = styled(Image)({
137
+ resizeMode: 'cover',
138
+ aspectRatio: 1,
139
+ });
140
+
141
+ const DeleteButtonContainer = styled.View((p) => ({
142
+ position: 'absolute',
143
+ top: 0.5 * p.theme.fontSize,
144
+ right: 0.5 * p.theme.fontSize,
145
+ }));
146
+
147
+ /**
148
+ * The component to upload an image.
149
+ */
150
+ const ImageUpload = forwardRef<RectButton, ImageUploadProps>(
151
+ (
152
+ {
153
+ url,
154
+ locale = defaultLocale,
155
+ imageLibraryOptions = {},
156
+ cameraOptions = {},
157
+ value,
158
+ defaultValue,
159
+ onChange,
160
+ onPress = () => {},
161
+ ...rest
162
+ },
163
+ ref
164
+ ) => {
165
+ const [forwardedValue, setForwardedValue] = useForwardedState({
166
+ value,
167
+ defaultValue,
168
+ onChange,
169
+ });
170
+
171
+ const [modalVisible, setModalVisible] = useState(false);
172
+ const { theme } = useTheme();
173
+
174
+ const chooseFromPhotoLibraryHandler = useCallback(async () => {
175
+ try {
176
+ const { assets } = await new Promise<ImagePickerResponse>((resolve) => {
177
+ launchImageLibrary(
178
+ {
179
+ mediaType: 'photo',
180
+ ...imageLibraryOptions,
181
+ },
182
+ resolve
183
+ );
184
+ });
185
+ setModalVisible(false);
186
+ if (assets) setForwardedValue(assets[0]);
187
+ } catch (e) {} // eslint-disable-line no-empty
188
+ }, [imageLibraryOptions, setForwardedValue]);
189
+
190
+ const takePhotoHandler = useCallback(async () => {
191
+ try {
192
+ const { assets } = await new Promise<ImagePickerResponse>((resolve) => {
193
+ launchCamera(
194
+ {
195
+ mediaType: 'photo',
196
+ ...cameraOptions,
197
+ },
198
+ resolve
199
+ );
200
+ });
201
+ setModalVisible(false);
202
+ if (assets) setForwardedValue(assets[0]);
203
+ } catch (e) {} // eslint-disable-line no-empty
204
+ }, [cameraOptions, setForwardedValue]);
205
+
206
+ const source = useMemo(() => {
207
+ if (forwardedValue === null) return null;
208
+ if (forwardedValue) return forwardedValue.uri; // If the user select an image
209
+ if (url) return url; // If the image already exists
210
+ return null;
211
+ }, [forwardedValue, url]);
212
+
213
+ const hasImage = useMemo(() => !!source, [source]);
214
+
215
+ return (
216
+ <>
217
+ <ThemeOverrider overrides={hasImage ? { colorText: [0, 0, 100] } : {}}>
218
+ <StyledRectButton
219
+ onPress={(e) => {
220
+ setModalVisible(true);
221
+ onPress(e);
222
+ }}
223
+ underlayColor={
224
+ hasImage
225
+ ? clr([0, 0, 100])
226
+ : clr(theme.imageUploadNoImageBgHighlight)
227
+ }
228
+ rippleColor={
229
+ hasImage
230
+ ? clr([0, 0, 100])
231
+ : clr(theme.imageUploadNoImageBgRipple)
232
+ }
233
+ activeOpacity={hasImage ? 0.8 : 1}
234
+ {...rest}
235
+ ref={ref}
236
+ >
237
+ <Container
238
+ hasImage={hasImage}
239
+ accessible
240
+ accessibilityRole='imagebutton'
241
+ >
242
+ {source && <StyledImage source={{ uri: source }} />}
243
+
244
+ <Overlay hasImage={hasImage}>
245
+ <Content>
246
+ <ThemeOverrider
247
+ overrides={(t) => ({ fontSize: 2.5 * t.fontSize })}
248
+ >
249
+ <Picture />
250
+ </ThemeOverrider>
251
+ <Title>{locale?.title}</Title>
252
+ </Content>
253
+
254
+ {hasImage && (
255
+ <ThemeOverrider
256
+ overrides={{
257
+ buttonGhostColorText: [0, 0, 100],
258
+ buttonGhostColorBgHighlight: [0, 0, 100, 0.1],
259
+ }}
260
+ >
261
+ <DeleteButtonContainer>
262
+ <Button
263
+ type='ghost'
264
+ onPress={() => setForwardedValue(null)}
265
+ >
266
+ <Delete />
267
+ </Button>
268
+ </DeleteButtonContainer>
269
+ </ThemeOverrider>
270
+ )}
271
+ </Overlay>
272
+ </Container>
273
+ </StyledRectButton>
274
+ </ThemeOverrider>
275
+
276
+ <ActionMenu
277
+ visible={modalVisible}
278
+ onClose={() => setModalVisible(false)}
279
+ >
280
+ <MenuItem left={<Picture />} onPress={chooseFromPhotoLibraryHandler}>
281
+ {locale?.chooseFromLibrary}
282
+ </MenuItem>
283
+ <MenuItem left={<Camera />} onPress={takePhotoHandler}>
284
+ {locale?.takePhoto}
285
+ </MenuItem>
286
+ </ActionMenu>
287
+ </>
288
+ );
289
+ }
290
+ );
291
+
292
+ ImageUpload.displayName = 'ImageUpload';
293
+
294
+ export default ImageUpload;
@@ -0,0 +1,13 @@
1
+ export interface ImageUploadLocale {
2
+ title: string;
3
+ chooseFromLibrary: string;
4
+ takePhoto: string;
5
+ }
6
+
7
+ const defaultLocale: ImageUploadLocale = {
8
+ title: 'Click here to choose an image',
9
+ chooseFromLibrary: 'Choose from Library',
10
+ takePhoto: 'Take a Photo',
11
+ };
12
+
13
+ export default defaultLocale;