mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2026-05-05 14:15:27 -04:00
feat: add labels to tab buttons (#2611)
* feat: add text labels to bottom tabs * fix: treat icon and text as single pressable for screen readers in bottom tabs * feat: float AddObs button outside of bottom tab bar Closes MOB-254 --------- Co-authored-by: Kirk van Gorkom <55742+kvangork@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,8 @@ interface Props extends PropsWithChildren {
|
||||
disabled?: boolean;
|
||||
height?: number;
|
||||
icon?: string;
|
||||
// Only show the icon with all the same layout, don't make it a button
|
||||
iconOnly?: boolean;
|
||||
onPress: ( _event?: GestureResponderEvent ) => void;
|
||||
// Inserts a white or colored view under the icon so an holes in the shape show as
|
||||
// white
|
||||
@@ -35,6 +37,15 @@ interface Props extends PropsWithChildren {
|
||||
|
||||
const MIN_ACCESSIBLE_DIM = 44;
|
||||
|
||||
const WRAPPER_STYLE: ViewStyle = {
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
};
|
||||
|
||||
const CONTAINED_WRAPPER_STYLE: ViewStyle = {
|
||||
borderRadius: 9999
|
||||
};
|
||||
|
||||
// Similar to IconButton in react-native-paper, except this allows independent
|
||||
// control over touchable area with `width` and `height` *and* the size of
|
||||
// the icon with `size`
|
||||
@@ -46,6 +57,7 @@ const INatIconButton = ( {
|
||||
disabled = false,
|
||||
height = MIN_ACCESSIBLE_DIM,
|
||||
icon,
|
||||
iconOnly,
|
||||
onPress,
|
||||
preventTransparency,
|
||||
size = 18,
|
||||
@@ -67,12 +79,12 @@ const INatIconButton = ( {
|
||||
`Height cannot be less than ${MIN_ACCESSIBLE_DIM}. Use IconButton for smaller buttons.`
|
||||
);
|
||||
}
|
||||
if ( !accessibilityLabel ) {
|
||||
if ( !accessibilityLabel && !iconOnly ) {
|
||||
throw new Error(
|
||||
"Button needs an accessibility label"
|
||||
);
|
||||
}
|
||||
const opacity = ( pressed: boolean ) => {
|
||||
const getOpacity = React.useCallback( ( pressed: boolean ) => {
|
||||
if ( disabled ) {
|
||||
return 0.5;
|
||||
}
|
||||
@@ -80,7 +92,84 @@ const INatIconButton = ( {
|
||||
return 0.95;
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
}, [disabled] );
|
||||
|
||||
const wrapperStyle = React.useMemo( ( ) => ( [
|
||||
{ width, height },
|
||||
WRAPPER_STYLE,
|
||||
mode === "contained" && {
|
||||
backgroundColor: preventTransparency
|
||||
? undefined
|
||||
: backgroundColor,
|
||||
...CONTAINED_WRAPPER_STYLE
|
||||
},
|
||||
style
|
||||
] ), [
|
||||
backgroundColor,
|
||||
height,
|
||||
mode,
|
||||
preventTransparency,
|
||||
style,
|
||||
width
|
||||
] );
|
||||
|
||||
const content = (
|
||||
<View
|
||||
className={classnames(
|
||||
"relative",
|
||||
// This degree of pixel pushing was meant for a ~22px icon, so it
|
||||
// might have to be made relative, but it's barely noticeable for
|
||||
// most icons
|
||||
Platform.OS === "android"
|
||||
? "top-[0.8px]"
|
||||
: "left-[0.2px] top-[0.1px]"
|
||||
)}
|
||||
>
|
||||
{ backgroundColor && preventTransparency && (
|
||||
<View
|
||||
// Position and size need to be dynamic
|
||||
// eslint-disable-next-line react-native/no-inline-styles
|
||||
style={{
|
||||
opacity: disabled
|
||||
? 0
|
||||
: 1,
|
||||
position: "absolute",
|
||||
top: preventTransparency
|
||||
? 2
|
||||
: -2,
|
||||
start: preventTransparency
|
||||
? 2
|
||||
: -2,
|
||||
width: preventTransparency
|
||||
? size - 4
|
||||
: size + 4,
|
||||
height: preventTransparency
|
||||
? size - 4
|
||||
: size + 4,
|
||||
backgroundColor,
|
||||
borderRadius: 9999
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
children || (
|
||||
<INatIcon
|
||||
name={icon}
|
||||
size={size}
|
||||
color={String( color || colors?.darkGray )}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
);
|
||||
|
||||
if ( iconOnly ) {
|
||||
return (
|
||||
<View style={wrapperStyle} testID={testID}>
|
||||
{ content }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -91,70 +180,12 @@ const INatIconButton = ( {
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
style={( { pressed } ) => [
|
||||
{
|
||||
opacity: opacity( pressed ),
|
||||
width,
|
||||
height,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
},
|
||||
mode === "contained" && {
|
||||
backgroundColor: preventTransparency
|
||||
? undefined
|
||||
: backgroundColor,
|
||||
borderRadius: 9999
|
||||
},
|
||||
style
|
||||
...wrapperStyle,
|
||||
{ opacity: getOpacity( pressed ) }
|
||||
]}
|
||||
testID={testID}
|
||||
>
|
||||
<View
|
||||
className={classnames(
|
||||
"relative",
|
||||
// This degree of pixel pushing was meant for a ~22px icon, so it
|
||||
// might have to be made relative, but it's barely noticeable for
|
||||
// most icons
|
||||
Platform.OS === "android"
|
||||
? "top-[0.8px]"
|
||||
: "left-[0.2px] top-[0.1px]"
|
||||
)}
|
||||
>
|
||||
{ backgroundColor && preventTransparency && (
|
||||
<View
|
||||
// Position and size need to be dynamic
|
||||
// eslint-disable-next-line react-native/no-inline-styles
|
||||
style={{
|
||||
opacity: disabled
|
||||
? 0
|
||||
: 1,
|
||||
position: "absolute",
|
||||
top: preventTransparency
|
||||
? 2
|
||||
: -2,
|
||||
start: preventTransparency
|
||||
? 2
|
||||
: -2,
|
||||
width: preventTransparency
|
||||
? size - 4
|
||||
: size + 4,
|
||||
height: preventTransparency
|
||||
? size - 4
|
||||
: size + 4,
|
||||
backgroundColor,
|
||||
borderRadius: 9999
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
children || (
|
||||
<INatIcon
|
||||
name={icon}
|
||||
size={size}
|
||||
color={color || colors.darkGray}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
{ content }
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user