1746 radio bottom sheet (#1769)

* Update radio bottom sheet element margins and paddings

* Update radio button row element margins and paddings

* Revert "Update radio button row element margins and paddings"

This reverts commit 269882b93c.

* Label margin

* Explainer

* Add four pixel between radio button and text

* Actually, let's patch the radio button to not have intrinsic margin, that makes everything easy peasy

* Create react-native-paper+5.12.3.patch

* Add margin between label and description

* RadioButtonRow TS

* Icon is optional

* Update RadioButtonSheet.js

* RadioButtonSheet TS

* Radio labels in ExploreFilters should be smaller

* BottomSheetStandardBackdrop TS

* Move key

* Update Settings radio button containers

* Update containers of radio filters

* Modal TS

* Update types

* Update types

* Update interface

* Update types

* Update types
This commit is contained in:
Johannes Klein
2024-07-04 12:24:03 +02:00
committed by GitHub
parent 57cb7cba85
commit 3fd5b717c1
17 changed files with 226 additions and 192 deletions

View File

@@ -0,0 +1,12 @@
diff --git a/node_modules/react-native-paper/src/components/RadioButton/RadioButtonAndroid.tsx b/node_modules/react-native-paper/src/components/RadioButton/RadioButtonAndroid.tsx
index 515efe5..67082de 100644
--- a/node_modules/react-native-paper/src/components/RadioButton/RadioButtonAndroid.tsx
+++ b/node_modules/react-native-paper/src/components/RadioButton/RadioButtonAndroid.tsx
@@ -187,7 +187,6 @@ const styles = StyleSheet.create({
height: 20,
width: 20,
borderRadius: 10,
- margin: 8,
},
dot: {
height: 10,

View File

@@ -1,6 +1,6 @@
// @flow
import Modal from "components/SharedComponents/Modal";
import Modal from "components/SharedComponents/Modal.tsx";
import type { Node } from "react";
import React from "react";

View File

@@ -1,21 +1,19 @@
// @flow
import ExploreLocationSearch from "components/Explore/SearchScreens/ExploreLocationSearch";
import Modal from "components/SharedComponents/Modal";
import type { Node } from "react";
import Modal from "components/SharedComponents/Modal.tsx";
import React from "react";
interface Props {
showModal: boolean,
closeModal: Function,
updateLocation: Function
showModal: boolean;
closeModal: () => void;
// TODO: Param not typed yet, because ExploreLocationSearch is not typed yet
updateLocation: ( _location: any ) => void;
}
const ExploreLocationSearchModal = ( {
showModal,
closeModal,
updateLocation
}: Props ): Node => (
}: Props ) => (
<Modal
showModal={showModal}
fullScreen

View File

@@ -1,21 +1,19 @@
// @flow
import ExploreProjectSearch from "components/Explore/SearchScreens/ExploreProjectSearch";
import Modal from "components/SharedComponents/Modal";
import type { Node } from "react";
import Modal from "components/SharedComponents/Modal.tsx";
import React from "react";
interface Props {
showModal: boolean,
closeModal: Function,
updateProject: Function
showModal: boolean;
closeModal: () => void;
// TODO: Param not typed yet, because ExploreProjectSearch is not typed yet
updateProject: ( _project: any ) => void;
}
const ExploreProjectSearchModal = ( {
showModal,
closeModal,
updateProject
}: Props ): Node => (
}: Props ) => (
<Modal
showModal={showModal}
fullScreen

View File

@@ -2,7 +2,7 @@
import { useNavigation } from "@react-navigation/native";
import ExploreTaxonSearch from "components/Explore/SearchScreens/ExploreTaxonSearch";
import Modal from "components/SharedComponents/Modal";
import Modal from "components/SharedComponents/Modal.tsx";
import type { Node } from "react";
import React, { useState } from "react";

View File

@@ -1,21 +1,19 @@
// @flow
import ExploreUserSearch from "components/Explore/SearchScreens/ExploreUserSearch";
import Modal from "components/SharedComponents/Modal";
import type { Node } from "react";
import Modal from "components/SharedComponents/Modal.tsx";
import React from "react";
interface Props {
showModal: boolean,
closeModal: Function,
updateUser: Function
showModal: boolean;
closeModal: () => void;
// TODO: Param not typed yet, because ExploreUserSearch is not typed yet
updateUser: ( _user: any ) => void;
}
const ExploreUserSearchModal = ( {
showModal,
closeModal,
updateUser
}: Props ): Node => (
}: Props ) => (
<Modal
showModal={showModal}
fullScreen

View File

@@ -57,12 +57,15 @@ const DROP_SHADOW = getShadowForColor( colors.darkGray, {
const { useRealm } = RealmContext;
interface Props {
closeModal: Function,
filterByIconicTaxonUnknown: Function
updateTaxon: Function,
updateLocation: Function,
updateUser: Function,
updateProject: Function
closeModal: () => void;
filterByIconicTaxonUnknown: () => void;
updateTaxon: ( _taxon: Object | null ) => void;
// TODO: Param not typed yet, because ExploreLocationSearch is not typed yet
updateLocation: ( _location: any ) => void;
// TODO: Param not typed yet, because ExploreUserSearch is not typed yet
updateUser: ( _user: any ) => void;
// TODO: Param not typed yet, because ExploreProjectSearch is not typed yet
updateProject: ( _project: any ) => void;
}
const FilterModal = ( {
@@ -1084,75 +1087,84 @@ const FilterModal = ( {
</View>
{/* Media section */}
<View className="mb-7">
<View className="mb-3">
<Heading4 className="mb-5">{t( "MEDIA" )}</Heading4>
{Object.keys( mediaValues ).map( mediaKey => (
<RadioButtonRow
key={mediaKey}
value={mediaValues[mediaKey]}
checked={mediaValues[mediaKey].value === media}
onPress={() => dispatch( {
type: EXPLORE_ACTION.SET_MEDIA,
media: mediaValues[mediaKey].value
} )}
label={mediaValues[mediaKey].label}
/>
<View key={mediaKey} className="mb-4">
<RadioButtonRow
smallLabel
value={mediaValues[mediaKey]}
checked={mediaValues[mediaKey].value === media}
onPress={() => dispatch( {
type: EXPLORE_ACTION.SET_MEDIA,
media: mediaValues[mediaKey].value
} )}
label={mediaValues[mediaKey].label}
/>
</View>
) )}
</View>
{/* Establishment Means section */}
<View className="mb-7">
<View className="mb-3">
<Heading4 className="mb-5">{t( "ESTABLISHMENT-MEANS" )}</Heading4>
{Object.keys( establishmentValues ).map( establishmentKey => (
<RadioButtonRow
key={establishmentKey}
value={establishmentValues[establishmentKey]}
checked={
establishmentValues[establishmentKey].value
=== establishmentMean
}
onPress={() => dispatch( {
type: EXPLORE_ACTION.SET_ESTABLISHMENT_MEAN,
establishmentMean:
<View key={establishmentKey} className="mb-4">
<RadioButtonRow
smallLabel
value={establishmentValues[establishmentKey]}
checked={
establishmentValues[establishmentKey].value
} )}
label={establishmentValues[establishmentKey].label}
/>
=== establishmentMean
}
onPress={() => dispatch( {
type: EXPLORE_ACTION.SET_ESTABLISHMENT_MEAN,
establishmentMean:
establishmentValues[establishmentKey].value
} )}
label={establishmentValues[establishmentKey].label}
/>
</View>
) )}
</View>
{/* Wild Status section */}
<View className="mb-7">
<View className="mb-3">
<Heading4 className="mb-5">{t( "WILD-STATUS" )}</Heading4>
{Object.keys( wildValues ).map( wildKey => (
<RadioButtonRow
key={wildKey}
value={wildValues[wildKey]}
checked={wildValues[wildKey].value === wildStatus}
onPress={() => dispatch( {
type: EXPLORE_ACTION.SET_WILD_STATUS,
wildStatus: wildValues[wildKey].value
} )}
label={wildValues[wildKey].label}
/>
<View key={wildKey} className="mb-4">
<RadioButtonRow
smallLabel
value={wildValues[wildKey]}
checked={wildValues[wildKey].value === wildStatus}
onPress={() => dispatch( {
type: EXPLORE_ACTION.SET_WILD_STATUS,
wildStatus: wildValues[wildKey].value
} )}
label={wildValues[wildKey].label}
/>
</View>
) )}
</View>
{/* Reviewed section */}
{currentUser && (
<View className="mb-7">
<View className="mb-3">
<Heading4 className="mb-5">{t( "REVIEWED" )}</Heading4>
{Object.keys( reviewedValues ).map( reviewedKey => (
<RadioButtonRow
key={reviewedKey}
value={reviewedValues[reviewedKey]}
checked={reviewedValues[reviewedKey].value === reviewedFilter}
onPress={() => dispatch( {
type: EXPLORE_ACTION.SET_REVIEWED,
reviewedFilter: reviewedValues[reviewedKey].value
} )}
label={reviewedValues[reviewedKey].label}
/>
<View key={reviewedKey} className="mb-4">
<RadioButtonRow
key={reviewedKey}
smallLabel
value={reviewedValues[reviewedKey]}
checked={reviewedValues[reviewedKey].value === reviewedFilter}
onPress={() => dispatch( {
type: EXPLORE_ACTION.SET_REVIEWED,
reviewedFilter: reviewedValues[reviewedKey].value
} )}
label={reviewedValues[reviewedKey].label}
/>
</View>
) )}
</View>
)}

View File

@@ -1,7 +1,7 @@
// @flow
import MediaViewer from "components/MediaViewer/MediaViewer";
import Modal from "components/SharedComponents/Modal";
import Modal from "components/SharedComponents/Modal.tsx";
import type { Node } from "react";
import React from "react";

View File

@@ -9,7 +9,7 @@ import {
Heading2
} from "components/SharedComponents";
import GradientButton from "components/SharedComponents/Buttons/GradientButton";
import Modal from "components/SharedComponents/Modal";
import Modal from "components/SharedComponents/Modal.tsx";
import { View } from "components/styledComponents";
import type { Node } from "react";
import React, { useState } from "react";

View File

@@ -143,7 +143,7 @@ const Settings = ( ) => {
<>
<Heading4>{t( "OBSERVATION-BUTTON" )}</Heading4>
<Body2 className="mt-3">{t( "When-tapping-the-green-observation-button" )}</Body2>
<View className="mt-5">
<View className="mt-[22px] pr-5">
<RadioButtonRow
smallLabel
checked={!isAdvancedUser}
@@ -151,7 +151,7 @@ const Settings = ( ) => {
label={t( "iNaturalist-AI-Camera" )}
/>
</View>
<View className="mt-2 pr-5">
<View className="mt-4 pr-5">
<RadioButtonRow
testID="all-observation-option"
smallLabel
@@ -167,7 +167,7 @@ const Settings = ( ) => {
<>
<Heading4 className="mt-7">{t( "TAXON-NAMES-DISPLAY" )}</Heading4>
<Body2 className="mt-3">{t( "This-is-how-taxon-names-will-be-displayed" )}</Body2>
<View className="mt-5">
<View className="mt-[22px]">
<RadioButtonRow
smallLabel
checked={settings.prefers_common_names && !settings.prefers_scientific_name_first}
@@ -175,7 +175,7 @@ const Settings = ( ) => {
label={t( "Common-Name-Scientific-Name" )}
/>
</View>
<View className="mt-2">
<View className="mt-4">
<RadioButtonRow
smallLabel
checked={settings.prefers_common_names && settings.prefers_scientific_name_first}
@@ -183,7 +183,7 @@ const Settings = ( ) => {
label={t( "Scientific-Name-Common-Name" )}
/>
</View>
<View className="mt-2">
<View className="mt-4">
<RadioButtonRow
smallLabel
checked={!settings.prefers_common_names && !settings.prefers_scientific_name_first}

View File

@@ -1,32 +1,31 @@
// @flow
import * as React from "react";
import { ViewStyle } from "react-native";
import RNModal from "react-native-modal";
// repurposed from Seek: https://github.com/inaturalist/SeekReactNative/blob/main/components/UIComponents/Modals/Modal.js
type Props = {
showModal: boolean,
closeModal: Function,
// $FlowIgnore
modal: unknown,
backdropOpacity?: number,
fullScreen?: boolean,
onModalHide?: Function,
style?: Object,
animationIn?: string,
animationOut?: string,
disableSwipeDirection?: boolean
interface Props {
showModal: boolean;
closeModal: () => void;
modal: React.ReactNode,
backdropOpacity?: number;
fullScreen?: boolean;
onModalHide?: () => void,
style?: ViewStyle,
animationIn?: string;
animationOut?: string;
disableSwipeDirection?: boolean;
}
const modalStyle = {
flex: 1,
justifyContent: "flex-end"
};
} as const;
const fullScreenModalStyle = {
...modalStyle,
margin: 0
};
} as const;
// accessibility might not work on Android because of backdrop
// https://github.com/react-native-modal/react-native-modal/issues/525
@@ -42,9 +41,9 @@ const Modal = ( {
onModalHide,
showModal,
style
}: Props ): React.Node => {
}: Props ) => {
const swipeDirection = disableSwipeDirection
? null
? undefined
: "down";
return (
<RNModal

View File

@@ -1,7 +1,7 @@
// @flow
import { useNavigation } from "@react-navigation/native";
import Modal from "components/SharedComponents/Modal";
import Modal from "components/SharedComponents/Modal.tsx";
import _ from "lodash";
import type { Node } from "react";
import React, { useCallback, useEffect, useState } from "react";

View File

@@ -1,4 +1,3 @@
// @flow
import {
Body1,
Body2,
@@ -6,19 +5,19 @@ import {
List2
} from "components/SharedComponents";
import { Pressable, View } from "components/styledComponents";
import type { Node } from "react";
import React from "react";
import { GestureResponderEvent } from "react-native";
import { RadioButton, useTheme } from "react-native-paper";
type Props = {
testID: string,
checked: boolean,
description: ?string,
icon: string,
label: string,
onPress: Function,
value: string,
smallLabel: ?boolean
interface Props {
testID?: string;
icon?: string;
label: string;
smallLabel?: boolean;
description?: string;
onPress: ( _e: GestureResponderEvent ) => void;
checked: boolean;
value: string;
}
const RadioButtonRow = ( {
@@ -30,7 +29,7 @@ const RadioButtonRow = ( {
icon,
value,
smallLabel = false
}: Props ): Node => {
}: Props ) => {
const theme = useTheme( );
const status = checked
@@ -50,13 +49,13 @@ const RadioButtonRow = ( {
status={status}
accessibilityLabel={label}
/>
<View className="flex-row">
<Label className="mr-2">{label}</Label>
<View className="ml-3 flex-row">
<Label className="mr-[10px]">{label}</Label>
{icon && <INatIcon name={icon} size={19} color={theme.colors.secondary} />}
</View>
</View>
{description && (
<List2 className="ml-[37px] mr-[33px] py-1">{description}</List2>
<List2 className="ml-[32px] mt-[3px]">{description}</List2>
)}
</Pressable>
);

View File

@@ -1,16 +1,14 @@
// @flow
import {
BottomSheetBackdrop
BottomSheetBackdrop,
BottomSheetBackdropProps
} from "@gorhom/bottom-sheet";
import type { Node } from "react";
import React from "react";
type Props = {
props: Object
props: BottomSheetBackdropProps
}
const BottomSheetStandardBackdrop = ( { props }: Props ): Node => (
const BottomSheetStandardBackdrop = ( { props }: Props ) => (
<BottomSheetBackdrop
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}

View File

@@ -1,65 +0,0 @@
// @flow
import {
BottomSheet,
Button,
RadioButtonRow
} from "components/SharedComponents";
import { View } from "components/styledComponents";
import type { Node } from "react";
import React, { useState } from "react";
import useTranslation from "sharedHooks/useTranslation";
type Props = {
handleClose: Function,
confirm: Function,
headerText: string,
radioValues: Object,
selectedValue?: string,
insideModal?: boolean
}
const RadioButtonSheet = ( {
handleClose,
confirm,
headerText,
radioValues,
selectedValue = "none",
insideModal
}: Props ): Node => {
const { t } = useTranslation( );
const [checked, setChecked] = useState( selectedValue );
const radioButtonRow = radioRow => (
<RadioButtonRow
key={radioRow}
value={radioValues[radioRow].value}
icon={radioValues[radioRow].icon}
checked={checked === radioValues[radioRow].value}
onPress={() => setChecked( radioValues[radioRow].value )}
label={radioValues[radioRow].label}
description={radioValues[radioRow].text}
/>
);
return (
<BottomSheet
handleClose={handleClose}
headerText={headerText}
insideModal={insideModal}
>
<View className="p-5">
{Object.keys( radioValues ).map( radioRow => radioButtonRow( radioRow ) )}
<Button
level="primary"
onPress={( ) => confirm( checked )}
text={radioValues[checked]?.buttonText ?? t( "CONFIRM" )}
className="mt-[15px]"
accessibilityLabel={radioValues[checked]?.buttonText ?? t( "CONFIRM" )}
/>
</View>
</BottomSheet>
);
};
export default RadioButtonSheet;

View File

@@ -0,0 +1,72 @@
import {
BottomSheet,
Button,
RadioButtonRow
} from "components/SharedComponents";
import { View } from "components/styledComponents";
import React, { useState } from "react";
import useTranslation from "sharedHooks/useTranslation";
interface Props {
handleClose: Function,
confirm: ( _checkedValue: string ) => void;
headerText: string,
radioValues: {
[key: string]: {
value: string,
icon?: string,
label: string,
text?: string,
buttonText?: string,
}
},
selectedValue?: string,
insideModal?: boolean
}
const RadioButtonSheet = ( {
handleClose,
confirm,
headerText,
radioValues,
selectedValue = "none",
insideModal
}: Props ) => {
const { t } = useTranslation( );
const [checkedValue, setCheckedValue] = useState( selectedValue );
const radioButtonRow = ( radioRow: string ) => (
<View key={radioRow} className="pb-4">
<RadioButtonRow
value={radioValues[radioRow].value}
icon={radioValues[radioRow].icon}
checked={checkedValue === radioValues[radioRow].value}
onPress={() => setCheckedValue( radioValues[radioRow].value )}
label={radioValues[radioRow].label}
description={radioValues[radioRow].text}
/>
</View>
);
return (
<BottomSheet
handleClose={handleClose}
headerText={headerText}
insideModal={insideModal}
>
<View className="p-4 pt-2">
<View className="p-3">
{Object.keys( radioValues ).map( radioRow => radioButtonRow( radioRow ) )}
</View>
<Button
level="primary"
onPress={( ) => confirm( checkedValue )}
text={radioValues[checkedValue]?.buttonText ?? t( "CONFIRM" )}
accessibilityLabel={radioValues[checkedValue]?.buttonText ?? t( "CONFIRM" )}
/>
</View>
</BottomSheet>
);
};
export default RadioButtonSheet;

View File

@@ -252,8 +252,21 @@ type Action = {type: EXPLORE_ACTION.RESET}
type Dispatch = ( action: Action ) => void
const ExploreContext = React.createContext<
{state: State; dispatch: Dispatch} | undefined
>( undefined );
{
state: State;
dispatch: Dispatch;
makeSnapshot(): void;
differsFromSnapshot: boolean;
isNotInitialState: boolean;
discardChanges(): void;
numberOfFilters: number;
defaultExploreLocation(): Promise<{
place_guess: string;
lat?: number;
lng?: number;
radius?: number;
}>;
} | undefined>( undefined );
// Every key in this object represents a numbered filter in the UI
const calculatedFilters = {