mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
Taxonomy component, TaxonDetails (#978)
* Style taxonomy ancestors and current taxon * Link to TaxonDetails pages * Add children to TaxonDetails taxonomy * Add children taxa toggle * Display children after button tap * Wrap text' * Fix italic font size in DisplayTaxonName * Add tests to Taxonomy component * Change Taxonomy test
This commit is contained in:
committed by
GitHub
parent
cad36a7253
commit
cd18e970d7
BIN
android/app/src/main/assets/fonts/Whitney Book Regular.otf
Normal file
BIN
android/app/src/main/assets/fonts/Whitney Book Regular.otf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Whitney Semibold Italic.otf
Normal file
BIN
android/app/src/main/assets/fonts/Whitney Semibold Italic.otf
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/fonts/Whitney Semibold Regular.otf
Normal file
BIN
android/app/src/main/assets/fonts/Whitney Semibold Regular.otf
Normal file
Binary file not shown.
@@ -5,6 +5,18 @@
|
||||
"path": "assets/fonts/INatIcon.ttf",
|
||||
"sha1": "495181444f9a2d8275f8bd86ff4988cd54e1fcb4"
|
||||
},
|
||||
{
|
||||
"path": "assets/fonts/Whitney Book Regular.otf",
|
||||
"sha1": "9b6e4749dcfa702e3fd6cc27d5c2bef1fdb966f9"
|
||||
},
|
||||
{
|
||||
"path": "assets/fonts/Whitney Semibold Italic.otf",
|
||||
"sha1": "829391a59c4305fe4733009da64fec983091cde1"
|
||||
},
|
||||
{
|
||||
"path": "assets/fonts/Whitney Semibold Regular.otf",
|
||||
"sha1": "fc560886d72adf37cccbad5bf8c58d7e0e546027"
|
||||
},
|
||||
{
|
||||
"path": "assets/fonts/Whitney-BookItalic-Pro.otf",
|
||||
"sha1": "15854f60175a0e82b794c259431ec45ea4b40103"
|
||||
|
||||
BIN
assets/fonts/Whitney Book Regular.otf
Normal file
BIN
assets/fonts/Whitney Book Regular.otf
Normal file
Binary file not shown.
BIN
assets/fonts/Whitney Semibold Italic.otf
Normal file
BIN
assets/fonts/Whitney Semibold Italic.otf
Normal file
Binary file not shown.
BIN
assets/fonts/Whitney Semibold Regular.otf
Normal file
BIN
assets/fonts/Whitney Semibold Regular.otf
Normal file
Binary file not shown.
@@ -62,6 +62,9 @@
|
||||
<string>Whitney-BookItalic-Pro.otf</string>
|
||||
<string>inaturalisticons.ttf</string>
|
||||
<string>INatIcon.ttf</string>
|
||||
<string>Whitney Semibold Regular.otf</string>
|
||||
<string>Whitney Semibold Italic.otf</string>
|
||||
<string>Whitney Book Regular.otf</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
"path": "assets/fonts/INatIcon.ttf",
|
||||
"sha1": "495181444f9a2d8275f8bd86ff4988cd54e1fcb4"
|
||||
},
|
||||
{
|
||||
"path": "assets/fonts/Whitney Book Regular.otf",
|
||||
"sha1": "9b6e4749dcfa702e3fd6cc27d5c2bef1fdb966f9"
|
||||
},
|
||||
{
|
||||
"path": "assets/fonts/Whitney Semibold Italic.otf",
|
||||
"sha1": "829391a59c4305fe4733009da64fec983091cde1"
|
||||
},
|
||||
{
|
||||
"path": "assets/fonts/Whitney Semibold Regular.otf",
|
||||
"sha1": "fc560886d72adf37cccbad5bf8c58d7e0e546027"
|
||||
},
|
||||
{
|
||||
"path": "assets/fonts/Whitney-BookItalic-Pro.otf",
|
||||
"sha1": "15854f60175a0e82b794c259431ec45ea4b40103"
|
||||
|
||||
@@ -7,7 +7,8 @@ import handleError from "./error";
|
||||
const ANCESTOR_FIELDS = {
|
||||
name: true,
|
||||
preferred_common_name: true,
|
||||
rank: true
|
||||
rank: true,
|
||||
rank_level: true
|
||||
};
|
||||
|
||||
const PHOTO_FIELDS = {
|
||||
@@ -19,6 +20,7 @@ const PHOTO_FIELDS = {
|
||||
|
||||
const FIELDS = {
|
||||
ancestors: ANCESTOR_FIELDS,
|
||||
children: ANCESTOR_FIELDS,
|
||||
default_photo: {
|
||||
url: true
|
||||
},
|
||||
|
||||
@@ -96,7 +96,7 @@ const DisplayTaxonName = ( {
|
||||
<INatText
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`DisplayTaxonName-${keyBase}-${taxon.id}-${rankLevel}-${piece}-${index}`}
|
||||
className={classNames( "italic", textClass() )}
|
||||
className={classNames( "italic font-normal", textClass() )}
|
||||
>
|
||||
{text}
|
||||
</INatText>
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import HTML, { defaultSystemFonts } from "react-native-render-html";
|
||||
import useTranslation from "sharedHooks/useTranslation";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
import Taxonomy from "./Taxonomy";
|
||||
|
||||
type Props = {
|
||||
taxon?: Object,
|
||||
@@ -25,37 +27,6 @@ const About = ( { taxon, isLoading, isError }: Props ): React.Node => {
|
||||
const { width } = useWindowDimensions();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const displayTaxonomyList = React.useMemo( () => {
|
||||
if ( !taxon || taxon.ancestors?.length === 0 ) {
|
||||
return <View />;
|
||||
}
|
||||
return taxon.ancestors?.map( ( ancestor, i ) => {
|
||||
const currentTaxon = `${taxon.preferred_common_name} (${taxon.name})`;
|
||||
// TODO: make sure this design accounts for undefined common names
|
||||
const formattedAncestor = ancestor.preferred_common_name
|
||||
? `${ancestor.preferred_common_name} (${ancestor.rank} ${ancestor.name})`
|
||||
: `(${ancestor.rank} ${ancestor.name})`;
|
||||
const displayAncestor = (
|
||||
<Body2>{formattedAncestor}</Body2>
|
||||
);
|
||||
const displayTaxon = (
|
||||
<Body2>{currentTaxon}</Body2>
|
||||
);
|
||||
|
||||
const lastAncestor = i === taxon.ancestors.length - 1;
|
||||
|
||||
return (
|
||||
<View key={lastAncestor
|
||||
? taxon.id
|
||||
: ancestor.id}
|
||||
>
|
||||
{displayAncestor}
|
||||
{lastAncestor && displayTaxon}
|
||||
</View>
|
||||
);
|
||||
} );
|
||||
}, [taxon] );
|
||||
|
||||
const openWikipedia = () => {
|
||||
if ( taxon?.wikipedia_url ) {
|
||||
Linking.openURL( taxon.wikipedia_url );
|
||||
@@ -106,10 +77,7 @@ const About = ( { taxon, isLoading, isError }: Props ): React.Node => {
|
||||
|
||||
</Body2>
|
||||
) }
|
||||
<Heading4 className="my-3">
|
||||
{t( "TAXONOMY-header" )}
|
||||
</Heading4>
|
||||
{displayTaxonomyList}
|
||||
<Taxonomy taxon={taxon} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
191
src/components/TaxonDetails/Taxonomy.js
Normal file
191
src/components/TaxonDetails/Taxonomy.js
Normal file
@@ -0,0 +1,191 @@
|
||||
// @flow
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import classnames from "classnames";
|
||||
import {
|
||||
Body2,
|
||||
Button,
|
||||
Heading4,
|
||||
INatIcon
|
||||
} from "components/SharedComponents";
|
||||
import { Pressable, View } from "components/styledComponents";
|
||||
import type { Node } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import Taxon from "realmModels/Taxon";
|
||||
import { generateTaxonPieces } from "sharedHelpers/taxon";
|
||||
import { useTranslation } from "sharedHooks";
|
||||
|
||||
type Props = {
|
||||
taxon?: Object
|
||||
}
|
||||
|
||||
const Taxonomy = ( { taxon: currentTaxon }: Props ): Node => {
|
||||
const [viewChildren, setViewChildren] = useState( false );
|
||||
const navigation = useNavigation( );
|
||||
const { t } = useTranslation( );
|
||||
|
||||
const displayCommonName = ( commonName, options ) => (
|
||||
<Body2 className={
|
||||
classnames( "font-bold mr-1", {
|
||||
"text-inatGreen": options?.isCurrentTaxon,
|
||||
underline: !options?.isCurrentTaxon
|
||||
} )
|
||||
}
|
||||
>
|
||||
{commonName}
|
||||
</Body2>
|
||||
);
|
||||
|
||||
const displayScientificName = ( rank, scientificNamePieces, rankLevel, rankPiece, options ) => {
|
||||
const isCurrentTaxon = options?.isCurrentTaxon;
|
||||
const hasCommonName = options?.hasCommonName;
|
||||
const underline = !isCurrentTaxon && !hasCommonName;
|
||||
// italics part ported over from DisplayTaxonName
|
||||
const scientificNameComponent = scientificNamePieces?.map( ( piece, index ) => {
|
||||
const isItalics = piece !== rankPiece && (
|
||||
rankLevel <= Taxon.SPECIES_LEVEL || rankLevel === Taxon.GENUS_LEVEL
|
||||
);
|
||||
const spaceChar = ( ( index !== scientificNamePieces.length - 1 ) )
|
||||
? " "
|
||||
: "";
|
||||
const text = piece + spaceChar;
|
||||
|
||||
return (
|
||||
<Body2
|
||||
key={text}
|
||||
className={
|
||||
classnames( {
|
||||
italic: isItalics,
|
||||
"font-bold": underline,
|
||||
"text-inatGreen": isCurrentTaxon
|
||||
} )
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</Body2>
|
||||
);
|
||||
} );
|
||||
|
||||
return (
|
||||
<Body2 className={
|
||||
classnames( {
|
||||
underline,
|
||||
"text-inatGreen": isCurrentTaxon,
|
||||
"-ml-1 ": !hasCommonName
|
||||
} )
|
||||
}
|
||||
>
|
||||
{hasCommonName && (
|
||||
<Body2 className={
|
||||
classnames( {
|
||||
"text-inatGreen": isCurrentTaxon
|
||||
} )
|
||||
}
|
||||
>
|
||||
(
|
||||
</Body2>
|
||||
)}
|
||||
{rankLevel > 10 && (
|
||||
<Body2
|
||||
className={
|
||||
classnames( {
|
||||
"font-bold": !hasCommonName,
|
||||
"text-inatGreen": isCurrentTaxon
|
||||
} )
|
||||
}
|
||||
>
|
||||
{`${rank} `}
|
||||
</Body2>
|
||||
)}
|
||||
{scientificNameComponent}
|
||||
{hasCommonName && (
|
||||
<Body2 className={
|
||||
classnames( {
|
||||
"text-inatGreen": isCurrentTaxon
|
||||
} )
|
||||
}
|
||||
>
|
||||
)
|
||||
</Body2>
|
||||
)}
|
||||
</Body2>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTaxon = useCallback( ( taxon, options ) => {
|
||||
const id = taxon?.id || "";
|
||||
const isCurrentTaxon = options?.isCurrentTaxon;
|
||||
const isChild = options?.isChild;
|
||||
const {
|
||||
commonName,
|
||||
scientificNamePieces,
|
||||
rankPiece,
|
||||
rankLevel,
|
||||
rank
|
||||
} = generateTaxonPieces( taxon );
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
className="flex-row py-2 flex-wrap"
|
||||
key={id}
|
||||
disabled={isCurrentTaxon}
|
||||
onPress={( ) => navigation.navigate( "TaxonDetails", { id } )}
|
||||
accessibilityState={{
|
||||
disabled: isCurrentTaxon
|
||||
}}
|
||||
accessibilityLabel={t( "Navigate-to-taxon-details" )}
|
||||
testID={`TaxonomyRow.${id}`}
|
||||
>
|
||||
{isChild && (
|
||||
<View className="ml-2 mr-1">
|
||||
<INatIcon name="arrow-turn-down-right" size={11} />
|
||||
</View>
|
||||
)}
|
||||
{displayCommonName( commonName, { isCurrentTaxon } )}
|
||||
{displayScientificName(
|
||||
rank,
|
||||
scientificNamePieces,
|
||||
rankLevel,
|
||||
rankPiece,
|
||||
{
|
||||
isCurrentTaxon,
|
||||
hasCommonName: commonName
|
||||
}
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
}, [navigation, t] );
|
||||
|
||||
const displayTaxonomy = useCallback(
|
||||
( ) => (
|
||||
<>
|
||||
{currentTaxon?.ancestors?.map( ancestor => renderTaxon( ancestor ) )}
|
||||
{renderTaxon( currentTaxon, { isCurrentTaxon: true } )}
|
||||
{viewChildren
|
||||
&& currentTaxon?.children?.map( child => renderTaxon( child, {
|
||||
isChild: true
|
||||
} ) )}
|
||||
</>
|
||||
),
|
||||
[currentTaxon, renderTaxon, viewChildren]
|
||||
);
|
||||
|
||||
return (
|
||||
<View className="mb-5">
|
||||
<Heading4 className="my-3">
|
||||
{t( "TAXONOMY-header" )}
|
||||
</Heading4>
|
||||
{displayTaxonomy( )}
|
||||
{!viewChildren && currentTaxon?.children && (
|
||||
<Button
|
||||
className="mt-3"
|
||||
onPress={( ) => setViewChildren( true )}
|
||||
text={t( "VIEW-CHILDREN-TAXA" )}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Taxonomy;
|
||||
@@ -854,6 +854,8 @@ Username = Username
|
||||
# Appears above the text fields
|
||||
USERNAME-OR-EMAIL = USERNAME OR EMAIL
|
||||
|
||||
VIEW-CHILDREN-TAXA = VIEW CHILDREN TAXA
|
||||
|
||||
VIEW-DATA-QUALITY-ASSESSEMENT = VIEW DATA QUALITY ASSESSEMENT
|
||||
|
||||
View-in-browser = View in Browser
|
||||
|
||||
@@ -565,6 +565,7 @@
|
||||
"comment": "Appears above the text fields",
|
||||
"val": "USERNAME OR EMAIL"
|
||||
},
|
||||
"VIEW-CHILDREN-TAXA": "VIEW CHILDREN TAXA",
|
||||
"VIEW-DATA-QUALITY-ASSESSEMENT": "VIEW DATA QUALITY ASSESSEMENT",
|
||||
"View-in-browser": "View in Browser",
|
||||
"View-photo": {
|
||||
|
||||
@@ -854,6 +854,8 @@ Username = Username
|
||||
# Appears above the text fields
|
||||
USERNAME-OR-EMAIL = USERNAME OR EMAIL
|
||||
|
||||
VIEW-CHILDREN-TAXA = VIEW CHILDREN TAXA
|
||||
|
||||
VIEW-DATA-QUALITY-ASSESSEMENT = VIEW DATA QUALITY ASSESSEMENT
|
||||
|
||||
View-in-browser = View in Browser
|
||||
|
||||
@@ -14,7 +14,6 @@ const useAuthenticatedMutation = (
|
||||
// one is expired. We *could* store the token in state with useState if
|
||||
// fetching from RNSInfo becomes a performance issue
|
||||
const apiToken = await getJWT( );
|
||||
console.log( apiToken, "api token jwt" );
|
||||
const options = {
|
||||
api_token: apiToken
|
||||
};
|
||||
|
||||
@@ -70,6 +70,12 @@ module.exports = {
|
||||
"Whitney-Light-Pro": ["Whitney-Light-Pro"], // Android naming convention
|
||||
"Whitney-BookItalic": ["Whitney-BookItalic"],
|
||||
"Whitney-BookItalic-Pro": ["Whitney-BookItalic-Pro"], // Android naming convention
|
||||
"Whitney-Semibold": ["Whitney-Semibold"],
|
||||
"Whitney-Semibold-Pro": ["Whitney-Semibold-Pro"], // Android naming convention
|
||||
"Whitney-Semibold-Italic": ["Whitney-Semibold-Italic"],
|
||||
"Whitney-Semibold-Italic-Pro": ["Whitney-Semibold-Italic-Pro"], // Android naming convention
|
||||
"Whitney-Book": ["Whitney-Book"],
|
||||
"Whitney-Book-Pro": ["Whitney-Book-Pro"], // Android naming convention
|
||||
// selected from list of fonts already available in RN
|
||||
// https://infinitbility.com/react-native-font-family-list/
|
||||
"Papyrus-Condensed": ["Papyrus-Condensed"],
|
||||
|
||||
109
tests/unit/components/TaxonDetails/Taxonomy.test.js
Normal file
109
tests/unit/components/TaxonDetails/Taxonomy.test.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react-native";
|
||||
import Taxonomy from "components/TaxonDetails/Taxonomy";
|
||||
import initI18next from "i18n/initI18next";
|
||||
import React from "react";
|
||||
import factory from "tests/factory";
|
||||
|
||||
const capitalizeFirstLetter = s => s.charAt( 0 ).toUpperCase( ) + s.slice( 1 );
|
||||
|
||||
const ancestors = [
|
||||
factory( "RemoteTaxon", {
|
||||
name: "Fungi",
|
||||
preferred_common_name: "Fungi Including Lichens",
|
||||
rank: "kingdom",
|
||||
rank_level: 80
|
||||
} ),
|
||||
factory( "RemoteTaxon", {
|
||||
name: "Agaricomycetes",
|
||||
preferred_common_name: "Mushrooms, Bracket Fungi, Puffballs, and Allies",
|
||||
rank: "class",
|
||||
rank_level: 60
|
||||
} )
|
||||
];
|
||||
|
||||
const children = [factory( "RemoteTaxon", {
|
||||
name: "Amanitina",
|
||||
preferred_common_name: "Amanita Subg. Amanitina",
|
||||
rank_level: 15,
|
||||
rank: "subgenus"
|
||||
} )
|
||||
];
|
||||
|
||||
const currentTaxon = factory( "RemoteTaxon", {
|
||||
name: "Amanita",
|
||||
preferred_common_name: "Amanita Mushrooms",
|
||||
rank: "genus",
|
||||
rank_level: 20,
|
||||
ancestors,
|
||||
children
|
||||
} );
|
||||
|
||||
describe( "Taxonomy", ( ) => {
|
||||
beforeAll( async ( ) => {
|
||||
await initI18next( );
|
||||
} );
|
||||
|
||||
test( "renders current taxon", ( ) => {
|
||||
render( <Taxonomy taxon={currentTaxon} /> );
|
||||
|
||||
const rankAndName = `${capitalizeFirstLetter( currentTaxon.rank )} ${currentTaxon.name}`;
|
||||
const commonName = currentTaxon.preferred_common_name;
|
||||
const currentTaxonRow = screen.getByTestId( `TaxonomyRow.${currentTaxon.id}` );
|
||||
|
||||
expect( currentTaxonRow ).toHaveTextContent( `${commonName}(${rankAndName})` );
|
||||
} );
|
||||
|
||||
test( "renders current taxon", ( ) => {
|
||||
render( <Taxonomy taxon={currentTaxon} /> );
|
||||
|
||||
const name = screen.getByText( currentTaxon.name );
|
||||
const commonName = screen.getByText( currentTaxon.preferred_common_name );
|
||||
|
||||
expect( commonName ).toBeVisible( );
|
||||
expect( name ).toBeVisible( );
|
||||
} );
|
||||
|
||||
test( "renders all ancestors", ( ) => {
|
||||
render( <Taxonomy taxon={currentTaxon} /> );
|
||||
|
||||
currentTaxon.ancestors.forEach( ancestor => {
|
||||
const ancestorName = screen.getByText( ancestor.name );
|
||||
const ancestorCommonName = screen.getByText( ancestor.preferred_common_name );
|
||||
|
||||
expect( ancestorName ).toBeVisible( );
|
||||
expect( ancestorCommonName ).toBeVisible( );
|
||||
} );
|
||||
} );
|
||||
|
||||
test( "renders button to optionally show children", ( ) => {
|
||||
render( <Taxonomy taxon={currentTaxon} /> );
|
||||
|
||||
const buttonText = screen.getByText( /VIEW CHILDREN TAXA/ );
|
||||
expect( buttonText ).toBeVisible( );
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
currentTaxon.children.forEach( child => {
|
||||
const childName = screen.queryByText( child.name );
|
||||
const childCommonName = screen.queryByText( child.preferred_common_name );
|
||||
|
||||
expect( childName ).toBeFalsy( );
|
||||
expect( childCommonName ).toBeFalsy( );
|
||||
} );
|
||||
} );
|
||||
|
||||
test( "shows children when button pressed", ( ) => {
|
||||
render( <Taxonomy taxon={currentTaxon} /> );
|
||||
|
||||
const buttonText = screen.getByText( /VIEW CHILDREN TAXA/ );
|
||||
fireEvent.press( buttonText );
|
||||
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
currentTaxon.children.forEach( child => {
|
||||
const childName = screen.getByText( child.name );
|
||||
const childCommonName = screen.getByText( child.preferred_common_name );
|
||||
|
||||
expect( childName ).toBeVisible( );
|
||||
expect( childCommonName ).toBeVisible( );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
Reference in New Issue
Block a user