Mob 1239 obs sheet not dismissing (#3576)

* use bottom sheet api instead of returning null when hidden

* dedupe inside modal dismiss logic

* rename handleSnapPress -> openSheet

* ts fixes and split render into two branches

* fix integration test so we check for BottomSheet behavior rather than child absence

* snapshot updates + comments

* prevent hidden-state BottomSheet dismiss from triggering onPressClose

* port changes from BottomSheet to BottomSheetV2
This commit is contained in:
Abbey Campbell
2026-05-04 11:40:10 -07:00
committed by GitHub
parent 5427c106c1
commit 92d81d7be8
7 changed files with 1918 additions and 78 deletions

View File

@@ -1,10 +1,12 @@
import type {
BottomSheetBackdropProps,
} from "@gorhom/bottom-sheet";
import BottomSheet, {
BottomSheetModal, BottomSheetScrollView,
} from "@gorhom/bottom-sheet";
import classnames from "classnames";
import { BottomSheetStandardBackdrop, Heading4, INatIconButton } from "components/SharedComponents";
import { View } from "components/styledComponents";
import type { Node } from "react";
import React, {
useCallback,
useEffect,
@@ -45,6 +47,8 @@ interface Props {
enableContentPanningGesture?: boolean;
}
type SheetHandle = BottomSheet & Partial<Pick<BottomSheetModal, "dismiss" | "present">>;
const StandardBottomSheet = ( {
children,
hidden,
@@ -60,110 +64,147 @@ const StandardBottomSheet = ( {
scrollEnabled = true,
enablePanDownToClose = true,
enableContentPanningGesture = true,
}: Props ): Node => {
}: Props ) => {
if ( snapPoints ) {
throw new Error( "BottomSheet does not accept snapPoints as a prop." );
}
const { t } = useTranslation( );
const sheetRef = useRef<BottomSheet>( null );
const sheetRef = useRef<SheetHandle>( null );
const skipNextOnPressCloseRef = useRef( false );
const insets = useSafeAreaInsets( );
const handleClose = useCallback( ( ) => {
if ( onPressClose ) onPressClose( );
// The optional `sheet` arg lets the unmount cleanup pass a captured handle;
// see the cleanup effect below for why that matters.
const dismissSheet = useCallback( ( sheet = sheetRef.current ) => {
if ( insideModal ) {
sheetRef.current?.collapse( );
sheet?.close( );
} else {
sheetRef.current?.dismiss( );
sheet?.dismiss?.( );
}
}, [insideModal, onPressClose] );
}, [insideModal] );
const renderBackdrop = props => (
const handleClose = useCallback( ( ) => {
if ( skipNextOnPressCloseRef.current ) {
skipNextOnPressCloseRef.current = false;
} else if ( onPressClose ) {
onPressClose( );
}
dismissSheet( );
}, [dismissSheet, onPressClose] );
const renderBackdrop = ( props: BottomSheetBackdropProps ) => (
<BottomSheetStandardBackdrop
props={props}
onPress={onPressClose}
onPress={onPressClose ?? ( ( ) => {} )}
/>
);
const handleSnapPress = useCallback( ( ) => {
const openSheet = useCallback( ( ) => {
if ( insideModal ) {
sheetRef.current?.expand( );
} else {
sheetRef.current?.present( );
sheetRef.current?.present?.( );
}
}, [insideModal] );
useEffect( ( ) => {
if ( hidden ) { return; }
handleSnapPress( );
}, [hidden, handleSnapPress] );
if ( hidden ) {
skipNextOnPressCloseRef.current = true;
dismissSheet( );
return;
}
skipNextOnPressCloseRef.current = false;
openSheet( );
}, [hidden, openSheet, dismissSheet] );
// To me, this implies this is a good candidate for splitting into 2 components
const BottomSheetComponent = insideModal
? BottomSheet
: BottomSheetModal;
// Capture sheetRef.current now: it's null by the time this cleanup runs on unmount,
// so the captured handle is what actually dismisses the sheet.
useEffect( ( ) => {
const sheet = sheetRef.current;
return ( ) => dismissSheet( sheet );
}, [dismissSheet] );
if ( hidden ) {
return null;
const content = (
<BottomSheetScrollView
keyboardShouldPersistTaps={keyboardShouldPersistTaps}
scrollEnabled={scrollEnabled}
>
<View
className={classnames(
"pt-7",
insets.bottom > 0
? "pb-7"
: null,
containerClass,
)}
onLayout={onLayout}
// Not ideal, but @gorhom/bottom-sheet components don't support
// testID
testID={testID}
>
{!headerText
? null
: (
<View className="mx-12 flex">
<Heading4
testID="bottom-sheet-header"
className="w-full text-center"
>
{headerText}
</Heading4>
</View>
)}
{children}
{!hideCloseButton && (
<INatIconButton
icon="close"
onPress={handleClose}
disabled={hidden}
size={19}
className="absolute top-3.5 right-3"
accessibilityLabel={t( "Close" )}
/>
)}
</View>
</BottomSheetScrollView>
);
// Consider splitting into separate files/components or removing `insideModal` usage
if ( insideModal ) {
return (
<BottomSheet
backdropComponent={renderBackdrop}
enableDynamicSizing
handleComponent={noHandle}
index={0}
ref={sheetRef}
style={marginOnWide}
accessible={false}
onClose={handleClose}
enablePanDownToClose={enablePanDownToClose}
enableContentPanningGesture={enableContentPanningGesture}
>
{content}
</BottomSheet>
);
}
return (
<BottomSheetComponent
<BottomSheetModal
backdropComponent={renderBackdrop}
enableDynamicSizing
handleComponent={noHandle}
index={0}
ref={sheetRef}
ref={sheetRef as React.RefObject<BottomSheetModal>}
style={marginOnWide}
accessible={false}
onDismiss={handleClose}
enablePanDownToClose={enablePanDownToClose}
enableContentPanningGesture={enableContentPanningGesture}
>
<BottomSheetScrollView
keyboardShouldPersistTaps={keyboardShouldPersistTaps}
scrollEnabled={scrollEnabled}
>
<View
className={classnames(
"pt-7",
insets.bottom > 0
? "pb-7"
: null,
containerClass,
)}
onLayout={onLayout}
// Not ideal, but @gorhom/bottom-sheet components don't support
// testID
testID={testID}
>
{!headerText
? null
: (
<View className="mx-12 flex">
<Heading4
testID="bottom-sheet-header"
className="w-full text-center"
>
{headerText}
</Heading4>
</View>
)}
{children}
{!hideCloseButton && (
<INatIconButton
icon="close"
onPress={handleClose}
size={19}
className="absolute top-3.5 right-3"
accessibilityState={{ disabled: hidden }}
accessibilityLabel={t( "Close" )}
/>
)}
</View>
</BottomSheetScrollView>
</BottomSheetComponent>
{content}
</BottomSheetModal>
);
};

View File

@@ -51,6 +51,7 @@ const BottomSheetV2 = ( {
}: Props ): React.JSX.Element | null => {
const { t } = useTranslation( );
const sheetRef = useRef<BottomSheetModal>( null );
const skipNextOnDismissRef = useRef( false );
const insets = useSafeAreaInsets( );
const handlePressClose = useCallback( ( ) => {
@@ -59,6 +60,16 @@ const BottomSheetV2 = ( {
}
}, [onPressClose] );
const handleDismiss = useCallback( ( ) => {
if ( skipNextOnDismissRef.current ) {
skipNextOnDismissRef.current = false;
return;
}
if ( onDismiss ) {
onDismiss( );
}
}, [onDismiss] );
const renderBackdrop = ( props: BottomSheetBackdropProps ) => (
<BottomSheetStandardBackdrop
props={props}
@@ -67,13 +78,21 @@ const BottomSheetV2 = ( {
);
useEffect( ( ) => {
if ( hidden ) { return; }
if ( hidden ) {
skipNextOnDismissRef.current = true;
sheetRef.current?.dismiss( );
return;
}
skipNextOnDismissRef.current = false;
sheetRef.current?.present( );
}, [hidden] );
if ( hidden ) {
return null;
}
useEffect( ( ) => {
const sheet = sheetRef.current;
return ( ) => {
sheet?.dismiss( );
};
}, [] );
return (
<BottomSheetModal
@@ -84,7 +103,7 @@ const BottomSheetV2 = ( {
ref={sheetRef}
style={styles.marginOnWide}
accessible={false}
onDismiss={onDismiss}
onDismiss={handleDismiss}
>
<BottomSheetScrollView>
<View

View File

@@ -1,3 +1,4 @@
import { BottomSheetModal } from "@gorhom/bottom-sheet";
import { screen, userEvent } from "@testing-library/react-native";
import AddObsButton from "components/AddObsBottomSheet/AddObsButton";
import i18next from "i18next";
@@ -106,13 +107,12 @@ describe( "with advanced user layout", ( ) => {
} );
it( "does not open model on long press", async ( ) => {
const presentSpy = jest.spyOn( BottomSheetModal.prototype, "present" );
renderComponent( <AddObsButton /> );
await longPress( );
const noEvidenceButton = screen.queryByLabelText(
i18next.t( "Observation-with-no-evidence" ),
);
expect( noEvidenceButton ).toBeFalsy( );
expect( presentSpy ).not.toHaveBeenCalled( );
presentSpy.mockRestore( );
} );
describe( "with advanced AICamera-only setting", ( ) => {

View File

@@ -30,7 +30,9 @@ describe( "AddObsButton", () => {
it( "renders correctly", () => {
renderComponent( <AddObsButton /> );
// Snapshot test
// Snapshot includes AddObs BottomSheet subtree due @gorhom/bottom-sheet test mock
// rendering children unconditionally. Hidden/open behavior is asserted in
// integration tests.
expect( screen ).toMatchSnapshot();
} );
it( "does not render tooltip in default state", () => {

View File

@@ -18,6 +18,894 @@ exports[`AddObsButton renders correctly 1`] = `
}
}
>
<RCTScrollView
keyboardShouldPersistTaps="never"
scrollEnabled={false}
>
<View>
<View
style={
[
[
{
"paddingTop": 28,
},
{
"backgroundColor": "#E8E8E8",
},
{
"paddingTop": 16,
},
],
]
}
>
<View
style={
[
[
{
"marginTop": -16,
},
{
"paddingBottom": 16,
},
{
"paddingLeft": 16,
"paddingRight": 16,
},
],
]
}
>
<View
style={
[
[
{
"flexDirection": "row",
},
{
"justifyContent": "center",
},
{
"width": "100%",
},
{
"flexBasis": "0%",
"flexGrow": 1,
"flexShrink": 1,
},
[
{
"marginTop": 16,
},
],
],
]
}
>
<View
accessibilityHint="Navigates to camera"
accessibilityLabel="Camera"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
[
{
"backgroundColor": "#ffffff",
},
{
"width": "50%",
},
{
"alignItems": "center",
},
{
"paddingBottom": 16,
"paddingTop": 16,
},
{
"borderBottomLeftRadius": 8,
"borderBottomRightRadius": 8,
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
},
{
"flexBasis": "0%",
"flexGrow": 1,
"flexShrink": 1,
},
{
"shadowColor": "rgba(0, 0, 0, 0.1)",
"shadowOffset": {
"height": 1,
"width": 0,
},
"shadowOpacity": 1,
"shadowRadius": 2,
},
{
"shadowColor": "rgba(0, 0, 0, 0.25)",
},
],
]
}
testID="camera-button"
>
<View
accessibilityHint="Navigates to camera"
accessibilityLabel="Camera"
accessibilityRole="button"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
{
"height": 44,
"width": 44,
},
{
"alignItems": "center",
"justifyContent": "center",
},
false,
[
{
"backgroundColor": "#74AC00",
},
{
"borderBottomLeftRadius": 9999,
"borderBottomRightRadius": 9999,
"borderTopLeftRadius": 9999,
"borderTopRightRadius": 9999,
},
{
"height": 36,
},
{
"width": 36,
},
{
"marginBottom": 8,
},
],
{
"opacity": 1,
},
]
}
>
<View
style={
[
[
{
"position": "relative",
},
{
"left": 0.2,
},
{
"top": 0.1,
},
],
]
}
>
<Text
allowFontScaling={false}
selectable={false}
style={
[
{
"color": "#ffffff",
"fontSize": 20,
},
null,
{
"fontFamily": "INatIcon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
}
>
</Text>
</View>
</View>
<Text
maxFontSizeMultiplier={2}
style={
[
{
"color": "#454545",
},
{
"fontFamily": "Lato-Regular",
},
[
{
"textAlign": "left",
},
[
{
"fontSize": 13,
"lineHeight": 18,
},
{
"fontFamily": "Lato-Medium",
},
],
],
]
}
>
Take photos
</Text>
</View>
</View>
<View
style={
[
[
{
"flexDirection": "row",
},
{
"justifyContent": "center",
},
{
"width": "100%",
},
{
"flexBasis": "0%",
"flexGrow": 1,
"flexShrink": 1,
},
[
{
"marginTop": 16,
},
],
],
]
}
>
<View
accessibilityHint="Navigates to sound recorder"
accessibilityLabel="Sound recorder"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
[
{
"backgroundColor": "#ffffff",
},
{
"width": "50%",
},
{
"alignItems": "center",
},
{
"paddingBottom": 16,
"paddingTop": 16,
},
{
"borderBottomLeftRadius": 8,
"borderBottomRightRadius": 8,
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
},
{
"flexBasis": "0%",
"flexGrow": 1,
"flexShrink": 1,
},
{
"shadowColor": "rgba(0, 0, 0, 0.1)",
"shadowOffset": {
"height": 1,
"width": 0,
},
"shadowOpacity": 1,
"shadowRadius": 2,
},
{
"shadowColor": "rgba(0, 0, 0, 0.25)",
},
],
]
}
testID="record-sound-button"
>
<View
accessibilityHint="Navigates to sound recorder"
accessibilityLabel="Sound recorder"
accessibilityRole="button"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
{
"height": 44,
"width": 44,
},
{
"alignItems": "center",
"justifyContent": "center",
},
false,
[
{
"backgroundColor": "#74AC00",
},
{
"borderBottomLeftRadius": 9999,
"borderBottomRightRadius": 9999,
"borderTopLeftRadius": 9999,
"borderTopRightRadius": 9999,
},
{
"height": 36,
},
{
"width": 36,
},
{
"marginBottom": 8,
},
],
{
"opacity": 1,
},
]
}
>
<View
style={
[
[
{
"position": "relative",
},
{
"left": 0.2,
},
{
"top": 0.1,
},
],
]
}
>
<Text
allowFontScaling={false}
selectable={false}
style={
[
{
"color": "#ffffff",
"fontSize": 20,
},
null,
{
"fontFamily": "INatIcon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
}
>
</Text>
</View>
</View>
<Text
maxFontSizeMultiplier={2}
style={
[
{
"color": "#454545",
},
{
"fontFamily": "Lato-Regular",
},
[
{
"textAlign": "left",
},
[
{
"fontSize": 13,
"lineHeight": 18,
},
{
"fontFamily": "Lato-Medium",
},
],
],
]
}
>
Record a sound
</Text>
</View>
<View
accessibilityHint="Navigates to photo importer"
accessibilityLabel="Photo importer"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
[
{
"backgroundColor": "#ffffff",
},
{
"width": "50%",
},
{
"alignItems": "center",
},
{
"paddingBottom": 16,
"paddingTop": 16,
},
{
"borderBottomLeftRadius": 8,
"borderBottomRightRadius": 8,
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
},
{
"flexBasis": "0%",
"flexGrow": 1,
"flexShrink": 1,
},
{
"shadowColor": "rgba(0, 0, 0, 0.1)",
"shadowOffset": {
"height": 1,
"width": 0,
},
"shadowOpacity": 1,
"shadowRadius": 2,
},
{
"shadowColor": "rgba(0, 0, 0, 0.25)",
},
[
{
"marginLeft": 16,
},
],
],
]
}
testID="import-media-button"
>
<View
accessibilityHint="Navigates to photo importer"
accessibilityLabel="Photo importer"
accessibilityRole="button"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
{
"height": 44,
"width": 44,
},
{
"alignItems": "center",
"justifyContent": "center",
},
false,
[
{
"backgroundColor": "#74AC00",
},
{
"borderBottomLeftRadius": 9999,
"borderBottomRightRadius": 9999,
"borderTopLeftRadius": 9999,
"borderTopRightRadius": 9999,
},
{
"height": 36,
},
{
"width": 36,
},
{
"marginBottom": 8,
},
],
{
"opacity": 1,
},
]
}
>
<View
style={
[
[
{
"position": "relative",
},
{
"left": 0.2,
},
{
"top": 0.1,
},
],
]
}
>
<Text
allowFontScaling={false}
selectable={false}
style={
[
{
"color": "#ffffff",
"fontSize": 20,
},
null,
{
"fontFamily": "INatIcon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
}
>
</Text>
</View>
</View>
<Text
maxFontSizeMultiplier={2}
style={
[
{
"color": "#454545",
},
{
"fontFamily": "Lato-Regular",
},
[
{
"textAlign": "left",
},
[
{
"fontSize": 13,
"lineHeight": 18,
},
{
"fontFamily": "Lato-Medium",
},
],
],
]
}
>
Upload photos
</Text>
</View>
</View>
<View
accessibilityHint="Navigate to observation edit screen"
accessibilityLabel="Observation with no evidence"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
[
{
"backgroundColor": "#BFBFBF",
},
{
"width": "100%",
},
{
"flexDirection": "row",
},
{
"alignItems": "center",
},
{
"paddingBottom": 10,
"paddingTop": 10,
},
{
"paddingLeft": 20,
"paddingRight": 20,
},
{
"borderBottomLeftRadius": 8,
"borderBottomRightRadius": 8,
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
},
[
{
"marginTop": 16,
},
],
],
]
}
testID="observe-without-evidence-button"
>
<View
style={
[
[
{
"marginRight": 8,
},
],
]
}
>
<Text
allowFontScaling={false}
selectable={false}
style={
[
{
"color": "#454545",
"fontSize": 24,
},
null,
{
"fontFamily": "INatIcon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
}
>
</Text>
</View>
<Text
maxFontSizeMultiplier={1.5}
numberOfLines={1}
style={
[
{
"color": "#454545",
},
{
"fontFamily": "Lato-Regular",
},
[
{
"textAlign": "left",
},
[
{
"fontSize": 13,
"lineHeight": 18,
},
{
"fontFamily": "Lato-Medium",
},
],
],
]
}
>
Create observation with no evidence
</Text>
</View>
</View>
</View>
</View>
</RCTScrollView>
<View
accessibilityHint="Opens AI camera."
accessibilityLabel="Add observations"

View File

@@ -99,7 +99,9 @@ describe( "CustomTabBar with advanced user layout", () => {
it( "should render correctly", async () => {
renderComponent( <CustomTabBarContainer navigation={jest.fn( )} /> );
// Snapshot includes AddObs BottomSheet subtree due @gorhom/bottom-sheet test mock
// rendering children unconditionally. Hidden/open behavior is asserted in
// integration tests.
await expect( screen ).toMatchSnapshot();
} );

View File

@@ -355,6 +355,894 @@ exports[`CustomTabBar with advanced user layout should render correctly 1`] = `
]
}
>
<RCTScrollView
keyboardShouldPersistTaps="never"
scrollEnabled={false}
>
<View>
<View
style={
[
[
{
"paddingTop": 28,
},
{
"backgroundColor": "#E8E8E8",
},
{
"paddingTop": 16,
},
],
]
}
>
<View
style={
[
[
{
"marginTop": -16,
},
{
"paddingBottom": 16,
},
{
"paddingLeft": 16,
"paddingRight": 16,
},
],
]
}
>
<View
style={
[
[
{
"flexDirection": "row",
},
{
"justifyContent": "center",
},
{
"width": "100%",
},
{
"flexBasis": "0%",
"flexGrow": 1,
"flexShrink": 1,
},
[
{
"marginTop": 16,
},
],
],
]
}
>
<View
accessibilityHint="Navigates to camera"
accessibilityLabel="Camera"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
[
{
"backgroundColor": "#ffffff",
},
{
"width": "50%",
},
{
"alignItems": "center",
},
{
"paddingBottom": 16,
"paddingTop": 16,
},
{
"borderBottomLeftRadius": 8,
"borderBottomRightRadius": 8,
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
},
{
"flexBasis": "0%",
"flexGrow": 1,
"flexShrink": 1,
},
{
"shadowColor": "rgba(0, 0, 0, 0.1)",
"shadowOffset": {
"height": 1,
"width": 0,
},
"shadowOpacity": 1,
"shadowRadius": 2,
},
{
"shadowColor": "rgba(0, 0, 0, 0.25)",
},
],
]
}
testID="camera-button"
>
<View
accessibilityHint="Navigates to camera"
accessibilityLabel="Camera"
accessibilityRole="button"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
{
"height": 44,
"width": 44,
},
{
"alignItems": "center",
"justifyContent": "center",
},
false,
[
{
"backgroundColor": "#74AC00",
},
{
"borderBottomLeftRadius": 9999,
"borderBottomRightRadius": 9999,
"borderTopLeftRadius": 9999,
"borderTopRightRadius": 9999,
},
{
"height": 36,
},
{
"width": 36,
},
{
"marginBottom": 8,
},
],
{
"opacity": 1,
},
]
}
>
<View
style={
[
[
{
"position": "relative",
},
{
"left": 0.2,
},
{
"top": 0.1,
},
],
]
}
>
<Text
allowFontScaling={false}
selectable={false}
style={
[
{
"color": "#ffffff",
"fontSize": 20,
},
null,
{
"fontFamily": "INatIcon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
}
>
</Text>
</View>
</View>
<Text
maxFontSizeMultiplier={2}
style={
[
{
"color": "#454545",
},
{
"fontFamily": "Lato-Regular",
},
[
{
"textAlign": "left",
},
[
{
"fontSize": 13,
"lineHeight": 18,
},
{
"fontFamily": "Lato-Medium",
},
],
],
]
}
>
Take photos
</Text>
</View>
</View>
<View
style={
[
[
{
"flexDirection": "row",
},
{
"justifyContent": "center",
},
{
"width": "100%",
},
{
"flexBasis": "0%",
"flexGrow": 1,
"flexShrink": 1,
},
[
{
"marginTop": 16,
},
],
],
]
}
>
<View
accessibilityHint="Navigates to sound recorder"
accessibilityLabel="Sound recorder"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
[
{
"backgroundColor": "#ffffff",
},
{
"width": "50%",
},
{
"alignItems": "center",
},
{
"paddingBottom": 16,
"paddingTop": 16,
},
{
"borderBottomLeftRadius": 8,
"borderBottomRightRadius": 8,
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
},
{
"flexBasis": "0%",
"flexGrow": 1,
"flexShrink": 1,
},
{
"shadowColor": "rgba(0, 0, 0, 0.1)",
"shadowOffset": {
"height": 1,
"width": 0,
},
"shadowOpacity": 1,
"shadowRadius": 2,
},
{
"shadowColor": "rgba(0, 0, 0, 0.25)",
},
],
]
}
testID="record-sound-button"
>
<View
accessibilityHint="Navigates to sound recorder"
accessibilityLabel="Sound recorder"
accessibilityRole="button"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
{
"height": 44,
"width": 44,
},
{
"alignItems": "center",
"justifyContent": "center",
},
false,
[
{
"backgroundColor": "#74AC00",
},
{
"borderBottomLeftRadius": 9999,
"borderBottomRightRadius": 9999,
"borderTopLeftRadius": 9999,
"borderTopRightRadius": 9999,
},
{
"height": 36,
},
{
"width": 36,
},
{
"marginBottom": 8,
},
],
{
"opacity": 1,
},
]
}
>
<View
style={
[
[
{
"position": "relative",
},
{
"left": 0.2,
},
{
"top": 0.1,
},
],
]
}
>
<Text
allowFontScaling={false}
selectable={false}
style={
[
{
"color": "#ffffff",
"fontSize": 20,
},
null,
{
"fontFamily": "INatIcon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
}
>
</Text>
</View>
</View>
<Text
maxFontSizeMultiplier={2}
style={
[
{
"color": "#454545",
},
{
"fontFamily": "Lato-Regular",
},
[
{
"textAlign": "left",
},
[
{
"fontSize": 13,
"lineHeight": 18,
},
{
"fontFamily": "Lato-Medium",
},
],
],
]
}
>
Record a sound
</Text>
</View>
<View
accessibilityHint="Navigates to photo importer"
accessibilityLabel="Photo importer"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
[
{
"backgroundColor": "#ffffff",
},
{
"width": "50%",
},
{
"alignItems": "center",
},
{
"paddingBottom": 16,
"paddingTop": 16,
},
{
"borderBottomLeftRadius": 8,
"borderBottomRightRadius": 8,
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
},
{
"flexBasis": "0%",
"flexGrow": 1,
"flexShrink": 1,
},
{
"shadowColor": "rgba(0, 0, 0, 0.1)",
"shadowOffset": {
"height": 1,
"width": 0,
},
"shadowOpacity": 1,
"shadowRadius": 2,
},
{
"shadowColor": "rgba(0, 0, 0, 0.25)",
},
[
{
"marginLeft": 16,
},
],
],
]
}
testID="import-media-button"
>
<View
accessibilityHint="Navigates to photo importer"
accessibilityLabel="Photo importer"
accessibilityRole="button"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": false,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
{
"height": 44,
"width": 44,
},
{
"alignItems": "center",
"justifyContent": "center",
},
false,
[
{
"backgroundColor": "#74AC00",
},
{
"borderBottomLeftRadius": 9999,
"borderBottomRightRadius": 9999,
"borderTopLeftRadius": 9999,
"borderTopRightRadius": 9999,
},
{
"height": 36,
},
{
"width": 36,
},
{
"marginBottom": 8,
},
],
{
"opacity": 1,
},
]
}
>
<View
style={
[
[
{
"position": "relative",
},
{
"left": 0.2,
},
{
"top": 0.1,
},
],
]
}
>
<Text
allowFontScaling={false}
selectable={false}
style={
[
{
"color": "#ffffff",
"fontSize": 20,
},
null,
{
"fontFamily": "INatIcon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
}
>
</Text>
</View>
</View>
<Text
maxFontSizeMultiplier={2}
style={
[
{
"color": "#454545",
},
{
"fontFamily": "Lato-Regular",
},
[
{
"textAlign": "left",
},
[
{
"fontSize": 13,
"lineHeight": 18,
},
{
"fontFamily": "Lato-Medium",
},
],
],
]
}
>
Upload photos
</Text>
</View>
</View>
<View
accessibilityHint="Navigate to observation edit screen"
accessibilityLabel="Observation with no evidence"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
[
{
"backgroundColor": "#BFBFBF",
},
{
"width": "100%",
},
{
"flexDirection": "row",
},
{
"alignItems": "center",
},
{
"paddingBottom": 10,
"paddingTop": 10,
},
{
"paddingLeft": 20,
"paddingRight": 20,
},
{
"borderBottomLeftRadius": 8,
"borderBottomRightRadius": 8,
"borderTopLeftRadius": 8,
"borderTopRightRadius": 8,
},
[
{
"marginTop": 16,
},
],
],
]
}
testID="observe-without-evidence-button"
>
<View
style={
[
[
{
"marginRight": 8,
},
],
]
}
>
<Text
allowFontScaling={false}
selectable={false}
style={
[
{
"color": "#454545",
"fontSize": 24,
},
null,
{
"fontFamily": "INatIcon",
"fontStyle": "normal",
"fontWeight": "normal",
},
{},
]
}
>
</Text>
</View>
<Text
maxFontSizeMultiplier={1.5}
numberOfLines={1}
style={
[
{
"color": "#454545",
},
{
"fontFamily": "Lato-Regular",
},
[
{
"textAlign": "left",
},
[
{
"fontSize": 13,
"lineHeight": 18,
},
{
"fontFamily": "Lato-Medium",
},
],
],
]
}
>
Create observation with no evidence
</Text>
</View>
</View>
</View>
</View>
</RCTScrollView>
<View
accessibilityHint="Shows observation creation options"
accessibilityLabel="Add observations"