@jobber/components-native 0.91.0 → 0.91.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +2 -2
- package/dist/src/ActionItem/ActionItemGroup.js +1 -1
- package/dist/src/AutoLink/components/Link/Link.js +1 -1
- package/dist/src/ErrorMessageWrapper/context/ErrorMessageProvider.js +1 -1
- package/dist/src/Form/components/FormMessageBanner/FormMessageBanner.js +1 -1
- package/dist/src/FormatFile/components/MediaView/MediaView.js +22 -5
- package/dist/src/Select/components/SelectPressable/SelectPressable.js +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/src/ActionItem/ActionItem.d.ts +2 -1
- package/dist/types/src/ActionItem/ActionItemGroup.d.ts +2 -1
- package/dist/types/src/ActionItem/components/ActionItemContainer.d.ts +2 -1
- package/dist/types/src/ActionLabel/ActionLabel.d.ts +1 -1
- package/dist/types/src/ActivityIndicator/ActivityIndicator.d.ts +2 -1
- package/dist/types/src/AutoLink/AutoLink.d.ts +2 -1
- package/dist/types/src/AutoLink/components/ComposeTextWithLinks/ComposeTextWithLinks.d.ts +2 -1
- package/dist/types/src/AutoLink/components/Link/Link.d.ts +2 -1
- package/dist/types/src/Banner/Banner.d.ts +2 -1
- package/dist/types/src/Banner/components/BannerIcon/BannerIcon.d.ts +2 -1
- package/dist/types/src/BottomSheet/components/BottomSheetOption/BottomSheetOption.d.ts +2 -1
- package/dist/types/src/Button/Button.d.ts +2 -1
- package/dist/types/src/Button/components/InternalButtonLoading/InternalButtonLoading.d.ts +1 -1
- package/dist/types/src/ButtonGroup/ButtonGroup.d.ts +2 -1
- package/dist/types/src/ButtonGroup/ButtonGroupAction.d.ts +4 -3
- package/dist/types/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.d.ts +2 -1
- package/dist/types/src/Card/Card.d.ts +2 -1
- package/dist/types/src/Card/components/InternalCardHeader.d.ts +2 -1
- package/dist/types/src/Checkbox/Checkbox.d.ts +2 -1
- package/dist/types/src/Checkbox/CheckboxGroup.d.ts +2 -1
- package/dist/types/src/Chip/Chip.d.ts +2 -1
- package/dist/types/src/Content/Content.d.ts +2 -1
- package/dist/types/src/Disclosure/Disclosure.d.ts +1 -1
- package/dist/types/src/Divider/Divider.d.ts +2 -1
- package/dist/types/src/EmptyState/EmptyState.d.ts +2 -1
- package/dist/types/src/ErrorMessageWrapper/ErrorMessageWrapper.d.ts +2 -1
- package/dist/types/src/ErrorMessageWrapper/context/ErrorMessageProvider.d.ts +2 -1
- package/dist/types/src/Flex/Flex.d.ts +2 -1
- package/dist/types/src/Form/Form.d.ts +2 -1
- package/dist/types/src/Form/components/FormActionBar/FormActionBar.d.ts +3 -2
- package/dist/types/src/Form/components/FormBody/FormBody.d.ts +3 -2
- package/dist/types/src/Form/components/FormCache/FormCache.d.ts +2 -1
- package/dist/types/src/Form/components/FormErrorBanner/FormErrorBanner.d.ts +2 -1
- package/dist/types/src/Form/components/FormMask/FormMask.d.ts +2 -1
- package/dist/types/src/Form/components/FormMessage/FormMessage.d.ts +2 -1
- package/dist/types/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.d.ts +2 -1
- package/dist/types/src/Form/components/FormMessageBanner/FormMessageBanner.d.ts +2 -1
- package/dist/types/src/Form/components/FormSaveButton/FormSaveButton.d.ts +2 -1
- package/dist/types/src/Form/types.d.ts +2 -2
- package/dist/types/src/FormField/FormField.d.ts +1 -1
- package/dist/types/src/FormatFile/FormatFile.d.ts +2 -1
- package/dist/types/src/FormatFile/components/ErrorIcon/ErrorIcon.d.ts +2 -1
- package/dist/types/src/FormatFile/components/FileView/FileView.d.ts +2 -1
- package/dist/types/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.d.ts +2 -1
- package/dist/types/src/FormatFile/components/MediaView/MediaView.d.ts +2 -1
- package/dist/types/src/FormatFile/components/ProgressBar/ProgressBar.d.ts +2 -1
- package/dist/types/src/Heading/Heading.d.ts +1 -1
- package/dist/types/src/Icon/Icon.d.ts +2 -1
- package/dist/types/src/IconButton/IconButton.d.ts +1 -1
- package/dist/types/src/InputCurrency/InputCurrency.d.ts +2 -1
- package/dist/types/src/InputDate/InputDate.d.ts +2 -1
- package/dist/types/src/InputFieldWrapper/InputFieldWrapper.d.ts +1 -1
- package/dist/types/src/InputFieldWrapper/components/ClearAction/ClearAction.d.ts +2 -1
- package/dist/types/src/InputFieldWrapper/components/Prefix/Prefix.d.ts +3 -2
- package/dist/types/src/InputFieldWrapper/components/Suffix/Suffix.d.ts +3 -2
- package/dist/types/src/InputPressable/InputPressable.d.ts +1 -1
- package/dist/types/src/InputSearch/components/FilterButton.d.ts +2 -1
- package/dist/types/src/InputText/context/InputAccessoriesProvider.d.ts +2 -1
- package/dist/types/src/InputTime/InputTime.d.ts +2 -1
- package/dist/types/src/Menu/Menu.d.ts +2 -1
- package/dist/types/src/Menu/components/MenuOption/MenuOption.d.ts +2 -1
- package/dist/types/src/Menu/components/Overlay/Overlay.d.ts +2 -1
- package/dist/types/src/Menu/types.d.ts +2 -1
- package/dist/types/src/ProgressBar/ProgressBar.d.ts +2 -1
- package/dist/types/src/ProgressBar/ProgressBarInner.d.ts +2 -1
- package/dist/types/src/Select/Select.d.ts +3 -2
- package/dist/types/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.d.ts +2 -1
- package/dist/types/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.ios.d.ts +2 -1
- package/dist/types/src/Select/components/SelectInternalPicker/SelectInternalPicker.d.ts +2 -1
- package/dist/types/src/Select/components/SelectPressable/SelectPressable.d.ts +2 -1
- package/dist/types/src/StatusLabel/StatusLabel.d.ts +2 -1
- package/dist/types/src/Switch/Switch.d.ts +2 -1
- package/dist/types/src/Switch/components/BaseSwitch/BaseSwitch.d.ts +2 -1
- package/dist/types/src/Text/Text.d.ts +1 -1
- package/dist/types/src/TextList/TextList.d.ts +2 -1
- package/dist/types/src/ThumbnailList/ThumbnailList.d.ts +2 -1
- package/dist/types/src/Toast/Toast.d.ts +2 -1
- package/dist/types/src/Typography/Typography.d.ts +1 -1
- package/dist/types/src/Typography/TypographyGestureDetector.d.ts +2 -1
- package/dist/types/src/utils/test/MockSafeAreaProvider.d.ts +1 -1
- package/package.json +2 -2
- package/src/ActionItem/ActionItem.tsx +1 -1
- package/src/ActionItem/ActionItemGroup.tsx +1 -3
- package/src/ActionItem/components/ActionItemContainer.tsx +1 -1
- package/src/ActionLabel/ActionLabel.tsx +1 -1
- package/src/ActivityIndicator/ActivityIndicator.tsx +1 -3
- package/src/AutoLink/AutoLink.tsx +1 -1
- package/src/AutoLink/components/ComposeTextWithLinks/ComposeTextWithLinks.tsx +1 -1
- package/src/AutoLink/components/Link/Link.tsx +1 -5
- package/src/Banner/Banner.tsx +2 -2
- package/src/Banner/components/BannerIcon/BannerIcon.tsx +1 -1
- package/src/BottomSheet/components/BottomSheetOption/BottomSheetOption.tsx +1 -1
- package/src/Button/Button.tsx +1 -1
- package/src/Button/components/InternalButtonLoading/InternalButtonLoading.tsx +1 -1
- package/src/ButtonGroup/ButtonGroup.tsx +1 -1
- package/src/ButtonGroup/ButtonGroupAction.tsx +4 -4
- package/src/ButtonGroup/components/SecondaryActionSheet/SecondaryActionSheet.tsx +1 -1
- package/src/Card/Card.tsx +1 -1
- package/src/Card/components/InternalCardHeader.tsx +1 -1
- package/src/Checkbox/Checkbox.tsx +2 -2
- package/src/Checkbox/CheckboxGroup.test.tsx +1 -1
- package/src/Checkbox/CheckboxGroup.tsx +2 -2
- package/src/Chip/Chip.tsx +1 -1
- package/src/Content/Content.tsx +1 -1
- package/src/ContentOverlay/ContentOverlay.tsx +1 -1
- package/src/Disclosure/Disclosure.tsx +1 -1
- package/src/Divider/Divider.tsx +1 -1
- package/src/EmptyState/EmptyState.tsx +1 -1
- package/src/ErrorMessageWrapper/ErrorMessageWrapper.tsx +1 -1
- package/src/ErrorMessageWrapper/context/ErrorMessageProvider.tsx +1 -3
- package/src/Flex/Flex.tsx +2 -2
- package/src/Form/Form.test.tsx +2 -2
- package/src/Form/Form.tsx +1 -1
- package/src/Form/components/FormActionBar/FormActionBar.tsx +3 -3
- package/src/Form/components/FormBody/FormBody.tsx +3 -3
- package/src/Form/components/FormCache/FormCache.tsx +1 -1
- package/src/Form/components/FormErrorBanner/FormErrorBanner.tsx +1 -1
- package/src/Form/components/FormMask/FormMask.tsx +1 -1
- package/src/Form/components/FormMessage/FormMessage.tsx +1 -1
- package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.tsx +1 -1
- package/src/Form/components/FormMessageBanner/FormMessageBanner.tsx +1 -3
- package/src/Form/components/FormSaveButton/FormSaveButton.tsx +1 -1
- package/src/Form/types.ts +2 -2
- package/src/FormField/FormField.tsx +1 -1
- package/src/FormatFile/FormatFile.tsx +3 -3
- package/src/FormatFile/components/ErrorIcon/ErrorIcon.tsx +1 -1
- package/src/FormatFile/components/FileView/FileView.tsx +1 -1
- package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.tsx +1 -1
- package/src/FormatFile/components/MediaView/MediaView.test.tsx +283 -0
- package/src/FormatFile/components/MediaView/MediaView.tsx +28 -7
- package/src/FormatFile/components/ProgressBar/ProgressBar.tsx +1 -1
- package/src/Heading/Heading.tsx +1 -1
- package/src/Icon/Icon.tsx +1 -1
- package/src/IconButton/IconButton.tsx +1 -1
- package/src/InputCurrency/InputCurrency.tsx +1 -1
- package/src/InputDate/InputDate.tsx +2 -2
- package/src/InputFieldWrapper/InputFieldWrapper.tsx +1 -1
- package/src/InputFieldWrapper/components/ClearAction/ClearAction.tsx +1 -1
- package/src/InputFieldWrapper/components/Prefix/Prefix.tsx +2 -2
- package/src/InputFieldWrapper/components/Suffix/Suffix.tsx +2 -2
- package/src/InputPassword/InputPassword.tsx +1 -1
- package/src/InputPressable/InputPressable.tsx +1 -1
- package/src/InputSearch/components/FilterButton.tsx +1 -1
- package/src/InputText/context/InputAccessoriesProvider.tsx +1 -1
- package/src/InputTime/InputTime.tsx +2 -2
- package/src/Menu/Menu.tsx +1 -1
- package/src/Menu/components/MenuOption/MenuOption.tsx +1 -1
- package/src/Menu/components/Overlay/Overlay.tsx +1 -1
- package/src/Menu/types.ts +2 -1
- package/src/ProgressBar/ProgressBar.tsx +1 -1
- package/src/ProgressBar/ProgressBarInner.tsx +1 -1
- package/src/Select/Select.tsx +2 -2
- package/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.ios.tsx +1 -1
- package/src/Select/components/SelectDefaultPicker/SelectDefaultPicker.tsx +1 -1
- package/src/Select/components/SelectInternalPicker/SelectInternalPicker.tsx +1 -1
- package/src/Select/components/SelectPressable/SelectPressable.tsx +1 -4
- package/src/StatusLabel/StatusLabel.tsx +1 -1
- package/src/Switch/Switch.tsx +1 -1
- package/src/Switch/components/BaseSwitch/BaseSwitch.tsx +1 -1
- package/src/Text/Text.tsx +1 -1
- package/src/TextList/TextList.tsx +1 -1
- package/src/ThumbnailList/ThumbnailList.tsx +1 -1
- package/src/Toast/Toast.tsx +2 -2
- package/src/Typography/Typography.tsx +1 -1
- package/src/Typography/TypographyGestureDetector.tsx +1 -3
- package/src/utils/test/MockSafeAreaProvider.tsx +1 -1
|
@@ -34,7 +34,7 @@ export function ErrorMessageWrapper({
|
|
|
34
34
|
message,
|
|
35
35
|
wrapFor = "default",
|
|
36
36
|
children,
|
|
37
|
-
}: ErrorMessageWrapperProps)
|
|
37
|
+
}: ErrorMessageWrapperProps) {
|
|
38
38
|
const errorMessageContext = useErrorMessageContext();
|
|
39
39
|
const register = errorMessageContext?.register;
|
|
40
40
|
const unregister = errorMessageContext?.unregister;
|
|
@@ -13,9 +13,7 @@ interface ErrorMessageProviderProps {
|
|
|
13
13
|
readonly children: ReactNode;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function ErrorMessageProvider({
|
|
17
|
-
children,
|
|
18
|
-
}: ErrorMessageProviderProps): JSX.Element {
|
|
16
|
+
export function ErrorMessageProvider({ children }: ErrorMessageProviderProps) {
|
|
19
17
|
const [elements, setElements] = useState<
|
|
20
18
|
ErrorMessageContextProps["elements"]
|
|
21
19
|
>({});
|
package/src/Flex/Flex.tsx
CHANGED
|
@@ -11,7 +11,7 @@ export function Flex({
|
|
|
11
11
|
align = "center",
|
|
12
12
|
gap = "base",
|
|
13
13
|
children,
|
|
14
|
-
}: PropsWithChildren<FlexProps>)
|
|
14
|
+
}: PropsWithChildren<FlexProps>) {
|
|
15
15
|
if (template.length === 1) {
|
|
16
16
|
console.warn("Please use <Content /> component for a stacked layout");
|
|
17
17
|
}
|
|
@@ -54,7 +54,7 @@ function Row({
|
|
|
54
54
|
align = "center",
|
|
55
55
|
gap = "base",
|
|
56
56
|
children,
|
|
57
|
-
}: PropsWithChildren<FlexProps>)
|
|
57
|
+
}: PropsWithChildren<FlexProps>) {
|
|
58
58
|
return (
|
|
59
59
|
<View testID="ATL-Flex-Row" style={[styles.row, { alignItems: align }]}>
|
|
60
60
|
{Children.map(children, (child, index) => (
|
package/src/Form/Form.test.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { type ReactElement } from "react";
|
|
2
2
|
import { act, fireEvent, render, waitFor } from "@testing-library/react-native";
|
|
3
3
|
import { Alert, Keyboard } from "react-native";
|
|
4
4
|
import { Host } from "react-native-portalize";
|
|
@@ -94,7 +94,7 @@ interface FormTestProps {
|
|
|
94
94
|
onSubmit: () => void,
|
|
95
95
|
label: string | undefined,
|
|
96
96
|
isSubmitting: boolean,
|
|
97
|
-
) =>
|
|
97
|
+
) => ReactElement;
|
|
98
98
|
readonly initialLoading?: boolean;
|
|
99
99
|
readonly initialValues?: FormFields;
|
|
100
100
|
readonly bannerMessages?: FormBannerMessage[];
|
package/src/Form/Form.tsx
CHANGED
|
@@ -34,7 +34,7 @@ import { ErrorMessageProvider } from "../ErrorMessageWrapper";
|
|
|
34
34
|
export function Form<T extends FieldValues, S>({
|
|
35
35
|
initialLoading,
|
|
36
36
|
...rest
|
|
37
|
-
}: FormProps<T, S>)
|
|
37
|
+
}: FormProps<T, S>) {
|
|
38
38
|
const child = initialLoading ? <FormMask /> : <InternalForm {...rest} />;
|
|
39
39
|
|
|
40
40
|
return (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { type ReactElement } from "react";
|
|
2
2
|
import type { LayoutChangeEvent } from "react-native";
|
|
3
3
|
import { StyleSheet } from "react-native";
|
|
4
4
|
import Reanimated from "react-native-reanimated";
|
|
@@ -18,7 +18,7 @@ export interface FormActionBarProps {
|
|
|
18
18
|
onSubmit: () => void,
|
|
19
19
|
label: string | undefined,
|
|
20
20
|
isSubmitting: boolean,
|
|
21
|
-
) =>
|
|
21
|
+
) => ReactElement;
|
|
22
22
|
readonly secondaryActions?: SecondaryActionProp[];
|
|
23
23
|
readonly setSecondaryActionLoading?: (bool: boolean) => void;
|
|
24
24
|
}
|
|
@@ -32,7 +32,7 @@ export function FormActionBar({
|
|
|
32
32
|
setSaveButtonHeight,
|
|
33
33
|
secondaryActions,
|
|
34
34
|
setSecondaryActionLoading,
|
|
35
|
-
}: FormActionBarProps)
|
|
35
|
+
}: FormActionBarProps) {
|
|
36
36
|
const styles = useStyles();
|
|
37
37
|
|
|
38
38
|
const buttonStyle = StyleSheet.flatten([
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useMemo } from "react";
|
|
1
|
+
import React, { type ReactElement, useMemo } from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
import { useStyles } from "./FormBody.style";
|
|
4
4
|
import { useScreenInformation } from "../../hooks/useScreenInformation";
|
|
@@ -7,7 +7,7 @@ import { FormActionBar } from "../FormActionBar";
|
|
|
7
7
|
import { tokens } from "../../../utils/design";
|
|
8
8
|
|
|
9
9
|
interface FormBodyProps extends FormActionBarProps {
|
|
10
|
-
readonly children:
|
|
10
|
+
readonly children: ReactElement;
|
|
11
11
|
readonly shouldRenderActionBar?: boolean;
|
|
12
12
|
readonly saveButtonOffset?: number;
|
|
13
13
|
}
|
|
@@ -24,7 +24,7 @@ export function FormBody({
|
|
|
24
24
|
setSecondaryActionLoading,
|
|
25
25
|
setSaveButtonHeight,
|
|
26
26
|
saveButtonOffset,
|
|
27
|
-
}: FormBodyProps)
|
|
27
|
+
}: FormBodyProps) {
|
|
28
28
|
const paddingBottom = useBottomPadding();
|
|
29
29
|
const fullViewPadding = useMemo(() => ({ paddingBottom }), [paddingBottom]);
|
|
30
30
|
const styles = useStyles();
|
|
@@ -14,7 +14,7 @@ export function FormCache<T extends FieldValues>({
|
|
|
14
14
|
localCacheExclude,
|
|
15
15
|
localCacheKey,
|
|
16
16
|
setLocalCache,
|
|
17
|
-
}: FormCacheProps<T>)
|
|
17
|
+
}: FormCacheProps<T>) {
|
|
18
18
|
const { control, formState } = useFormContext<T>();
|
|
19
19
|
const { isDirty } = formState;
|
|
20
20
|
|
|
@@ -4,7 +4,7 @@ import { useStyles } from "./FormMask.style";
|
|
|
4
4
|
import { ActivityIndicator } from "../../../ActivityIndicator";
|
|
5
5
|
import { useAtlantisI18n } from "../../../hooks/useAtlantisI18n";
|
|
6
6
|
|
|
7
|
-
export function FormMask()
|
|
7
|
+
export function FormMask() {
|
|
8
8
|
const { t } = useAtlantisI18n();
|
|
9
9
|
const styles = useStyles();
|
|
10
10
|
|
|
@@ -17,7 +17,7 @@ let close: (() => void) | undefined;
|
|
|
17
17
|
* message to the user. Use `FormMessage.close()` to close the most
|
|
18
18
|
* recent message.
|
|
19
19
|
*/
|
|
20
|
-
export const FormMessage = ()
|
|
20
|
+
export const FormMessage = () => {
|
|
21
21
|
const [data, setData] = useState<FormMessageData[]>([]);
|
|
22
22
|
|
|
23
23
|
open = useCallback(
|
package/src/Form/components/FormMessage/components/InternalFormMessage/InternalFormMessage.tsx
CHANGED
|
@@ -15,7 +15,7 @@ interface FormMessageProps {
|
|
|
15
15
|
export function InternalFormMessage({
|
|
16
16
|
data,
|
|
17
17
|
onRequestClose,
|
|
18
|
-
}: FormMessageProps)
|
|
18
|
+
}: FormMessageProps) {
|
|
19
19
|
const { t } = useAtlantisI18n();
|
|
20
20
|
const styles = useStyles();
|
|
21
21
|
const emptyStateData: EmptyStateProps = useMemo(() => {
|
|
@@ -8,9 +8,7 @@ interface FormMessageBannerProps {
|
|
|
8
8
|
readonly bannerMessages?: FormBannerMessage[];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function FormMessageBanner({
|
|
12
|
-
bannerMessages,
|
|
13
|
-
}: FormMessageBannerProps): JSX.Element {
|
|
11
|
+
export function FormMessageBanner({ bannerMessages }: FormMessageBannerProps) {
|
|
14
12
|
return (
|
|
15
13
|
<>
|
|
16
14
|
{bannerMessages?.map((message, index) => (
|
|
@@ -13,7 +13,7 @@ export function FormSaveButton({
|
|
|
13
13
|
setSecondaryActionLoading,
|
|
14
14
|
onOpenBottomSheet,
|
|
15
15
|
onCloseBottomSheet,
|
|
16
|
-
}: FormSaveButtonProps)
|
|
16
|
+
}: FormSaveButtonProps) {
|
|
17
17
|
const { t } = useAtlantisI18n();
|
|
18
18
|
const formContext = useFormContext();
|
|
19
19
|
const buttonActions = useButtonGroupAction(secondaryActions);
|
package/src/Form/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MutableRefObject, RefObject } from "react";
|
|
1
|
+
import type { MutableRefObject, ReactElement, RefObject } from "react";
|
|
2
2
|
import type {
|
|
3
3
|
ControllerProps,
|
|
4
4
|
DefaultValues,
|
|
@@ -142,7 +142,7 @@ export interface FormProps<T extends FieldValues, SubmitResponseType> {
|
|
|
142
142
|
onSubmit: () => void,
|
|
143
143
|
label: string | undefined,
|
|
144
144
|
isSubmitting: boolean,
|
|
145
|
-
) =>
|
|
145
|
+
) => ReactElement;
|
|
146
146
|
|
|
147
147
|
/**
|
|
148
148
|
* Adding a key will save a local copy of the form data that will be used to
|
|
@@ -88,7 +88,7 @@ function FormatFileContent({
|
|
|
88
88
|
styleInGrid,
|
|
89
89
|
onUploadComplete,
|
|
90
90
|
isMedia,
|
|
91
|
-
}: FormatFileContentProps)
|
|
91
|
+
}: FormatFileContentProps) {
|
|
92
92
|
const styles = useStyles();
|
|
93
93
|
|
|
94
94
|
return (
|
|
@@ -204,7 +204,7 @@ export function FormatFile<T extends File | FileUpload>({
|
|
|
204
204
|
showFileTypeIndicator = true,
|
|
205
205
|
createThumbnail,
|
|
206
206
|
onPreviewPress,
|
|
207
|
-
}: FormatFileProps<T>)
|
|
207
|
+
}: FormatFileProps<T>) {
|
|
208
208
|
const onTapModified = onTap ? () => onTap(file) : () => undefined;
|
|
209
209
|
|
|
210
210
|
const formattedFile = parseFile(file, showFileTypeIndicator);
|
|
@@ -236,7 +236,7 @@ function FormatFileInternal({
|
|
|
236
236
|
onPreviewPress,
|
|
237
237
|
testID,
|
|
238
238
|
createThumbnail: createThumbnailProp,
|
|
239
|
-
}: FormatFileInternalProps)
|
|
239
|
+
}: FormatFileInternalProps) {
|
|
240
240
|
const [showOverlay, setShowOverlay] = useState<boolean>(
|
|
241
241
|
file.status !== StatusCode.Completed,
|
|
242
242
|
);
|
|
@@ -20,7 +20,7 @@ export const FormatFileBottomSheet = ({
|
|
|
20
20
|
onPreviewPress,
|
|
21
21
|
onRemovePress,
|
|
22
22
|
bottomSheetOptionsSuffix,
|
|
23
|
-
}: FormatFileBottomSheetProps)
|
|
23
|
+
}: FormatFileBottomSheetProps) => {
|
|
24
24
|
const { t } = useAtlantisI18n();
|
|
25
25
|
|
|
26
26
|
const handlePress = (onPressAction: () => void) => {
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render, waitFor } from "@testing-library/react-native";
|
|
3
|
+
import { MediaView } from "./MediaView";
|
|
4
|
+
import type { FormattedFile } from "../../types";
|
|
5
|
+
import { StatusCode } from "../../types";
|
|
6
|
+
import { AtlantisFormatFileContext } from "../../context/FormatFileContext";
|
|
7
|
+
|
|
8
|
+
jest.mock("../../../hooks/useAtlantisI18n", () => ({
|
|
9
|
+
useAtlantisI18n: () => ({ t: (key: string) => key }),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe("MediaView", () => {
|
|
13
|
+
const mockFile: FormattedFile = {
|
|
14
|
+
showPreview: true,
|
|
15
|
+
source: "https://example.com/image1.jpg",
|
|
16
|
+
thumbnailUrl: undefined,
|
|
17
|
+
name: "test.jpg",
|
|
18
|
+
size: 1024,
|
|
19
|
+
external: false,
|
|
20
|
+
progress: 0,
|
|
21
|
+
status: StatusCode.Completed,
|
|
22
|
+
error: false,
|
|
23
|
+
type: "image/jpeg",
|
|
24
|
+
isMedia: true,
|
|
25
|
+
showFileTypeIndicator: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const defaultProps = {
|
|
29
|
+
accessibilityLabel: "Test image",
|
|
30
|
+
showOverlay: false,
|
|
31
|
+
showError: false,
|
|
32
|
+
file: mockFile,
|
|
33
|
+
styleInGrid: false,
|
|
34
|
+
onUploadComplete: jest.fn(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const mockContextValue = {
|
|
38
|
+
useCreateThumbnail: () => ({ thumbnail: undefined, error: false }),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const renderWithContext = (props = defaultProps) => {
|
|
42
|
+
return render(
|
|
43
|
+
<AtlantisFormatFileContext.Provider value={mockContextValue}>
|
|
44
|
+
<MediaView {...props} />
|
|
45
|
+
</AtlantisFormatFileContext.Provider>,
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
describe("Normal loading flow", () => {
|
|
50
|
+
it("shows loading indicator when onLoadStart fires", () => {
|
|
51
|
+
const { getByTestId, queryByTestId } = renderWithContext();
|
|
52
|
+
const image = getByTestId("test-image");
|
|
53
|
+
|
|
54
|
+
expect(queryByTestId("ActivityIndicator")).toBeNull();
|
|
55
|
+
|
|
56
|
+
fireEvent(image, "loadStart");
|
|
57
|
+
|
|
58
|
+
expect(queryByTestId("ActivityIndicator")).toBeTruthy();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("hides loading indicator when onLoadEnd fires", () => {
|
|
62
|
+
const { getByTestId, queryByTestId } = renderWithContext();
|
|
63
|
+
const image = getByTestId("test-image");
|
|
64
|
+
|
|
65
|
+
fireEvent(image, "loadStart");
|
|
66
|
+
expect(queryByTestId("ActivityIndicator")).toBeTruthy();
|
|
67
|
+
|
|
68
|
+
fireEvent(image, "loadEnd");
|
|
69
|
+
|
|
70
|
+
expect(queryByTestId("ActivityIndicator")).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("Race condition handling (cached images)", () => {
|
|
75
|
+
it("does not get stuck loading when onLoadEnd fires before onLoadStart", () => {
|
|
76
|
+
const { getByTestId, queryByTestId } = renderWithContext();
|
|
77
|
+
const image = getByTestId("test-image");
|
|
78
|
+
|
|
79
|
+
// Simulate cached image: LoadEnd fires BEFORE LoadStart
|
|
80
|
+
fireEvent(image, "loadEnd");
|
|
81
|
+
expect(queryByTestId("ActivityIndicator")).toBeNull();
|
|
82
|
+
|
|
83
|
+
fireEvent(image, "loadStart");
|
|
84
|
+
|
|
85
|
+
expect(queryByTestId("ActivityIndicator")).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("does not show infinite spinner when load events fire out of order", () => {
|
|
89
|
+
const { getByTestId, queryByTestId } = renderWithContext();
|
|
90
|
+
const image = getByTestId("test-image");
|
|
91
|
+
|
|
92
|
+
// Race condition scenario
|
|
93
|
+
fireEvent(image, "loadEnd");
|
|
94
|
+
fireEvent(image, "loadStart");
|
|
95
|
+
fireEvent(image, "loadEnd");
|
|
96
|
+
|
|
97
|
+
expect(queryByTestId("ActivityIndicator")).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("URI changes", () => {
|
|
102
|
+
it("shows loading indicator when URI changes to a new image", async () => {
|
|
103
|
+
const { getByTestId, queryByTestId, rerender } = renderWithContext();
|
|
104
|
+
const image = getByTestId("test-image");
|
|
105
|
+
|
|
106
|
+
// First image: simulate cached load (LoadEnd before LoadStart)
|
|
107
|
+
fireEvent(image, "loadEnd");
|
|
108
|
+
fireEvent(image, "loadStart");
|
|
109
|
+
expect(queryByTestId("ActivityIndicator")).toBeNull();
|
|
110
|
+
|
|
111
|
+
// Change URI to a new image
|
|
112
|
+
const newFile = {
|
|
113
|
+
...mockFile,
|
|
114
|
+
source: "https://example.com/image2.jpg",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
rerender(
|
|
118
|
+
<AtlantisFormatFileContext.Provider value={mockContextValue}>
|
|
119
|
+
<MediaView {...defaultProps} file={newFile} />
|
|
120
|
+
</AtlantisFormatFileContext.Provider>,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
const updatedImage = getByTestId("test-image");
|
|
125
|
+
|
|
126
|
+
// New image starts loading
|
|
127
|
+
fireEvent(updatedImage, "loadStart");
|
|
128
|
+
|
|
129
|
+
expect(queryByTestId("ActivityIndicator")).toBeTruthy();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("shows loading indicator on second URI change", async () => {
|
|
134
|
+
const { getByTestId, queryByTestId, rerender } = renderWithContext();
|
|
135
|
+
|
|
136
|
+
const image1 = getByTestId("test-image");
|
|
137
|
+
fireEvent(image1, "loadStart");
|
|
138
|
+
fireEvent(image1, "loadEnd");
|
|
139
|
+
expect(queryByTestId("ActivityIndicator")).toBeNull();
|
|
140
|
+
|
|
141
|
+
const file2 = { ...mockFile, source: "https://example.com/image2.jpg" };
|
|
142
|
+
rerender(
|
|
143
|
+
<AtlantisFormatFileContext.Provider value={mockContextValue}>
|
|
144
|
+
<MediaView {...defaultProps} file={file2} />
|
|
145
|
+
</AtlantisFormatFileContext.Provider>,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
const image2 = getByTestId("test-image");
|
|
150
|
+
fireEvent(image2, "loadStart");
|
|
151
|
+
|
|
152
|
+
expect(queryByTestId("ActivityIndicator")).toBeTruthy();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("shows loading indicator on third URI change", async () => {
|
|
157
|
+
const { getByTestId, queryByTestId, rerender } = renderWithContext();
|
|
158
|
+
|
|
159
|
+
const image1 = getByTestId("test-image");
|
|
160
|
+
fireEvent(image1, "loadStart");
|
|
161
|
+
fireEvent(image1, "loadEnd");
|
|
162
|
+
|
|
163
|
+
const file2 = { ...mockFile, source: "https://example.com/image2.jpg" };
|
|
164
|
+
rerender(
|
|
165
|
+
<AtlantisFormatFileContext.Provider value={mockContextValue}>
|
|
166
|
+
<MediaView {...defaultProps} file={file2} />
|
|
167
|
+
</AtlantisFormatFileContext.Provider>,
|
|
168
|
+
);
|
|
169
|
+
const image2 = getByTestId("test-image");
|
|
170
|
+
fireEvent(image2, "loadEnd");
|
|
171
|
+
|
|
172
|
+
const file3 = { ...mockFile, source: "https://example.com/image3.jpg" };
|
|
173
|
+
rerender(
|
|
174
|
+
<AtlantisFormatFileContext.Provider value={mockContextValue}>
|
|
175
|
+
<MediaView {...defaultProps} file={file3} />
|
|
176
|
+
</AtlantisFormatFileContext.Provider>,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await waitFor(() => {
|
|
180
|
+
const image3 = getByTestId("test-image");
|
|
181
|
+
fireEvent(image3, "loadStart");
|
|
182
|
+
|
|
183
|
+
expect(queryByTestId("ActivityIndicator")).toBeTruthy();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("handles URI change from cached to uncached image correctly", async () => {
|
|
188
|
+
const { getByTestId, queryByTestId, rerender } = renderWithContext();
|
|
189
|
+
|
|
190
|
+
// First image: cached (LoadEnd before LoadStart)
|
|
191
|
+
const image1 = getByTestId("test-image");
|
|
192
|
+
fireEvent(image1, "loadEnd");
|
|
193
|
+
fireEvent(image1, "loadStart");
|
|
194
|
+
expect(queryByTestId("ActivityIndicator")).toBeNull();
|
|
195
|
+
|
|
196
|
+
// Second image: uncached (normal LoadStart then LoadEnd)
|
|
197
|
+
const file2 = { ...mockFile, source: "https://example.com/image2.jpg" };
|
|
198
|
+
rerender(
|
|
199
|
+
<AtlantisFormatFileContext.Provider value={mockContextValue}>
|
|
200
|
+
<MediaView {...defaultProps} file={file2} />
|
|
201
|
+
</AtlantisFormatFileContext.Provider>,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
await waitFor(() => {
|
|
205
|
+
const image2 = getByTestId("test-image");
|
|
206
|
+
fireEvent(image2, "loadStart");
|
|
207
|
+
|
|
208
|
+
expect(queryByTestId("ActivityIndicator")).toBeTruthy();
|
|
209
|
+
|
|
210
|
+
fireEvent(image2, "loadEnd");
|
|
211
|
+
expect(queryByTestId("ActivityIndicator")).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("thumbnailUrl changes", () => {
|
|
217
|
+
it("shows loading indicator when thumbnailUrl changes", async () => {
|
|
218
|
+
const { getByTestId, queryByTestId, rerender } = renderWithContext();
|
|
219
|
+
|
|
220
|
+
// First load completes
|
|
221
|
+
const image1 = getByTestId("test-image");
|
|
222
|
+
fireEvent(image1, "loadStart");
|
|
223
|
+
fireEvent(image1, "loadEnd");
|
|
224
|
+
expect(queryByTestId("ActivityIndicator")).toBeNull();
|
|
225
|
+
|
|
226
|
+
// Thumbnail URL changes (common when thumbnail generation completes)
|
|
227
|
+
const fileWithThumbnail = {
|
|
228
|
+
...mockFile,
|
|
229
|
+
thumbnailUrl: "https://example.com/thumbnail.jpg",
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
rerender(
|
|
233
|
+
<AtlantisFormatFileContext.Provider value={mockContextValue}>
|
|
234
|
+
<MediaView {...defaultProps} file={fileWithThumbnail} />
|
|
235
|
+
</AtlantisFormatFileContext.Provider>,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
await waitFor(() => {
|
|
239
|
+
const image2 = getByTestId("test-image");
|
|
240
|
+
fireEvent(image2, "loadStart");
|
|
241
|
+
|
|
242
|
+
expect(queryByTestId("ActivityIndicator")).toBeTruthy();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("Context thumbnail changes", () => {
|
|
248
|
+
it("shows loading indicator when context thumbnail changes", async () => {
|
|
249
|
+
const mockContextWithThumbnail = {
|
|
250
|
+
useCreateThumbnail: () => ({
|
|
251
|
+
thumbnail: "https://example.com/context-thumbnail.jpg",
|
|
252
|
+
error: false,
|
|
253
|
+
}),
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const { getByTestId, queryByTestId, rerender } = render(
|
|
257
|
+
<AtlantisFormatFileContext.Provider value={mockContextValue}>
|
|
258
|
+
<MediaView {...defaultProps} />
|
|
259
|
+
</AtlantisFormatFileContext.Provider>,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// First load completes
|
|
263
|
+
const image1 = getByTestId("test-image");
|
|
264
|
+
fireEvent(image1, "loadStart");
|
|
265
|
+
fireEvent(image1, "loadEnd");
|
|
266
|
+
expect(queryByTestId("ActivityIndicator")).toBeNull();
|
|
267
|
+
|
|
268
|
+
// Context provides a new thumbnail
|
|
269
|
+
rerender(
|
|
270
|
+
<AtlantisFormatFileContext.Provider value={mockContextWithThumbnail}>
|
|
271
|
+
<MediaView {...defaultProps} />
|
|
272
|
+
</AtlantisFormatFileContext.Provider>,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
await waitFor(() => {
|
|
276
|
+
const image2 = getByTestId("test-image");
|
|
277
|
+
fireEvent(image2, "loadStart");
|
|
278
|
+
|
|
279
|
+
expect(queryByTestId("ActivityIndicator")).toBeTruthy();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { ImageBackground, View } from "react-native";
|
|
3
3
|
import { useStyles } from "./MediaView.style";
|
|
4
4
|
import type { FormattedFile } from "../../types";
|
|
@@ -27,11 +27,17 @@ export function MediaView({
|
|
|
27
27
|
file,
|
|
28
28
|
styleInGrid,
|
|
29
29
|
onUploadComplete,
|
|
30
|
-
}: MediaViewProps)
|
|
30
|
+
}: MediaViewProps) {
|
|
31
31
|
const { t } = useAtlantisI18n();
|
|
32
32
|
const { useCreateThumbnail } = useAtlantisFormatFileContext();
|
|
33
33
|
const { thumbnail, error } = useCreateThumbnail(file);
|
|
34
34
|
const [isLoading, setIsLoading] = useState(false);
|
|
35
|
+
/**
|
|
36
|
+
* Tracks whether onLoadEnd has fired to prevent race conditions.
|
|
37
|
+
* ImageBackground can fire onLoadEnd before onLoadStart when loading cached images,
|
|
38
|
+
* which would cause isLoading to get stuck at true, showing an infinite spinner.
|
|
39
|
+
*/
|
|
40
|
+
const hasLoadedRef = useRef(false);
|
|
35
41
|
|
|
36
42
|
const a11yLabel = computeA11yLabel({
|
|
37
43
|
accessibilityLabel,
|
|
@@ -40,10 +46,25 @@ export function MediaView({
|
|
|
40
46
|
t,
|
|
41
47
|
});
|
|
42
48
|
|
|
43
|
-
const hasError = showError || error
|
|
44
|
-
|
|
49
|
+
const hasError = showError || error,
|
|
50
|
+
uri = thumbnail || file.thumbnailUrl || file.source,
|
|
51
|
+
styles = useStyles();
|
|
45
52
|
|
|
46
|
-
const
|
|
53
|
+
const handleLoadStart = () => {
|
|
54
|
+
if (!hasLoadedRef.current) {
|
|
55
|
+
setIsLoading(true);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleLoadEnd = () => {
|
|
60
|
+
hasLoadedRef.current = true;
|
|
61
|
+
setIsLoading(false);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
hasLoadedRef.current = false;
|
|
66
|
+
setIsLoading(false);
|
|
67
|
+
}, [uri]);
|
|
47
68
|
|
|
48
69
|
return (
|
|
49
70
|
<View accessible={true} accessibilityLabel={a11yLabel}>
|
|
@@ -55,8 +76,8 @@ export function MediaView({
|
|
|
55
76
|
resizeMode={styleInGrid ? "cover" : "contain"}
|
|
56
77
|
source={{ uri }}
|
|
57
78
|
testID={"test-image"}
|
|
58
|
-
onLoadStart={
|
|
59
|
-
onLoadEnd={
|
|
79
|
+
onLoadStart={handleLoadStart}
|
|
80
|
+
onLoadEnd={handleLoadEnd}
|
|
60
81
|
>
|
|
61
82
|
<Overlay
|
|
62
83
|
isLoading={isLoading}
|
|
@@ -22,7 +22,7 @@ export const ProgressBar = ({
|
|
|
22
22
|
progress,
|
|
23
23
|
status,
|
|
24
24
|
onComplete,
|
|
25
|
-
}: ProgressBarProps)
|
|
25
|
+
}: ProgressBarProps) => {
|
|
26
26
|
const barWidth = useRef(new Animated.Value(0)).current;
|
|
27
27
|
const progressPercentage = barWidth.interpolate({
|
|
28
28
|
inputRange: [0, 1],
|
package/src/Heading/Heading.tsx
CHANGED
|
@@ -71,7 +71,7 @@ export function Heading<T extends HeadingLevel = "heading">({
|
|
|
71
71
|
maxLines = "unlimited",
|
|
72
72
|
allowFontScaling = true,
|
|
73
73
|
selectable,
|
|
74
|
-
}: HeadingProps<T>)
|
|
74
|
+
}: HeadingProps<T>) {
|
|
75
75
|
const headingStyle = getHeadingStyle(level, variation);
|
|
76
76
|
const accessibilityRole = "header";
|
|
77
77
|
|
package/src/Icon/Icon.tsx
CHANGED