diff --git a/src/components/ExerciseHeader.tsx b/src/components/ExerciseHeader.tsx index 5c64cc23a..2ba5342df 100644 --- a/src/components/ExerciseHeader.tsx +++ b/src/components/ExerciseHeader.tsx @@ -3,13 +3,16 @@ import React, { useEffect, useState } from 'react' import { BackHandler } from 'react-native' import { ProgressBar as RNProgressBar } from 'react-native-paper' import { widthPercentageToDP as wp } from 'react-native-responsive-screen' +// import { HiddenItem } from 'react-navigation-header-buttons' import styled, { useTheme } from 'styled-components/native' +// import { MenuIcon } from '../../assets/images' import labels from '../constants/labels.json' -import { RoutesParams } from '../navigation/NavigationTypes' +import { Route, RoutesParams } from '../navigation/NavigationTypes' import CustomModal from './CustomModal' import FeedbackModal from './FeedbackModal' import NavigationHeaderLeft from './NavigationHeaderLeft' +// import OverflowMenu from './OverflowMenu' import { ContentSecondary } from './text/Content' const ProgressBar = styled(RNProgressBar)` @@ -25,22 +28,29 @@ const HeaderRightContainer = styled.View` const ProgressText = styled(ContentSecondary)` margin-right: ${props => props.theme.spacings.sm}; ` -// /* TODO Remove comment when LUN-269 is ready */ +// TODO Remove comment when LUN-269 is ready // const MenuIconPrimary = styled(MenuIcon)` // color: ${props => props.theme.colors.primary}; // ` interface ExerciseHeaderProps { - navigation: StackNavigationProp - currentWord: number - numberOfWords: number + navigation: StackNavigationProp + currentWord?: number + numberOfWords?: number + confirmClose?: boolean } -const ExerciseHeader = ({ navigation, currentWord, numberOfWords }: ExerciseHeaderProps): JSX.Element => { +const ExerciseHeader = ({ + navigation, + currentWord, + numberOfWords, + confirmClose = true +}: ExerciseHeaderProps): JSX.Element => { const [isModalVisible, setIsModalVisible] = useState(false) const [isFeedbackModalVisible, setIsFeedbackModalVisible] = useState(false) const theme = useTheme() - const progressText = numberOfWords !== 0 ? `${currentWord + 1} / ${numberOfWords}` : '' + const showProgress = numberOfWords !== undefined && numberOfWords > 0 && currentWord !== undefined + const progressText = showProgress ? `${currentWord + 1} / ${numberOfWords}` : '' useEffect( () => @@ -48,7 +58,7 @@ const ExerciseHeader = ({ navigation, currentWord, numberOfWords }: ExerciseHead headerLeft: () => ( setIsModalVisible(true)} + onPress={confirmClose ? () => setIsModalVisible(true) : navigation.goBack} isCloseButton /> ), @@ -66,17 +76,19 @@ const ExerciseHeader = ({ navigation, currentWord, numberOfWords }: ExerciseHead maxWidth: wp('25%') } }), - [navigation, progressText, setIsModalVisible, setIsFeedbackModalVisible] + [navigation, progressText, setIsModalVisible, setIsFeedbackModalVisible, confirmClose] ) useEffect(() => { const showModal = (): boolean => { - setIsModalVisible(true) - return true + if (confirmClose) { + setIsModalVisible(true) + return true + } + return false } - const bEvent = BackHandler.addEventListener('hardwareBackPress', showModal) - return bEvent.remove - }, []) + return BackHandler.addEventListener('hardwareBackPress', showModal).remove + }, [confirmClose]) const goBack = (): void => { setIsModalVisible(false) @@ -85,10 +97,12 @@ const ExerciseHeader = ({ navigation, currentWord, numberOfWords }: ExerciseHead return ( <> - 0 ? currentWord / numberOfWords : 0} - color={theme.colors.progressIndicator} - /> + {showProgress && ( + 0 ? currentWord / numberOfWords : 0} + color={theme.colors.progressIndicator} + /> + )} props.theme.colors.background}; + height: 100%; + width: 100%; + padding: 0 ${props => props.theme.spacings.sm}; +` + +interface VocabularyListScreenProps { + documents: Document[] + onItemPress: (index: number) => void +} + +const VocabularyList = ({ documents, onItemPress }: VocabularyListScreenProps): JSX.Element => { + const renderItem = ({ item, index }: { item: Document; index: number }): JSX.Element => ( + onItemPress(index)} /> + ) + + return ( + + + } + data={documents} + renderItem={renderItem} + keyExtractor={item => `${item.id}`} + showsVerticalScrollIndicator={false} + /> + + ) +} + +export default VocabularyList diff --git a/src/routes/vocabulary-list/components/VocabularyListItem.tsx b/src/components/VocabularyListItem.tsx similarity index 73% rename from src/routes/vocabulary-list/components/VocabularyListItem.tsx rename to src/components/VocabularyListItem.tsx index 1514aaff8..d3caf3a46 100644 --- a/src/routes/vocabulary-list/components/VocabularyListItem.tsx +++ b/src/components/VocabularyListItem.tsx @@ -2,11 +2,11 @@ import React, { ReactElement } from 'react' import { widthPercentageToDP as wp } from 'react-native-responsive-screen' import styled from 'styled-components/native' -import AudioPlayer from '../../../components/AudioPlayer' -import ListItem from '../../../components/ListItem' -import { ContentTextLight } from '../../../components/text/Content' -import { Document } from '../../../constants/endpoints' -import { getArticleColor } from '../../../services/helpers' +import { Document } from '../constants/endpoints' +import { getArticleColor } from '../services/helpers' +import AudioPlayer from './AudioPlayer' +import ListItem from './ListItem' +import { ContentTextLight } from './text/Content' const StyledImage = styled.Image` margin-right: ${props => props.theme.spacings.sm}; @@ -29,12 +29,12 @@ const Speaker = styled.View` padding-top: ${props => props.theme.spacings.sm}; ` -export interface VocabularyListItemProp { +interface VocabularyListItemProps { document: Document - setIsModalVisible?: () => void + onPress: () => void } -const VocabularyListItem = ({ document, setIsModalVisible }: VocabularyListItemProp): ReactElement => { +const VocabularyListItem = ({ document, onPress }: VocabularyListItemProps): ReactElement => { const { article, word, document_image: documentImage } = document const title = {article.value} @@ -43,13 +43,11 @@ const VocabularyListItem = ({ document, setIsModalVisible }: VocabularyListItemP ) : undefined - const noop = () => undefined - return ( diff --git a/src/components/__tests__/VocabularyList.spec.tsx b/src/components/__tests__/VocabularyList.spec.tsx new file mode 100644 index 000000000..04affd344 --- /dev/null +++ b/src/components/__tests__/VocabularyList.spec.tsx @@ -0,0 +1,55 @@ +import { fireEvent } from '@testing-library/react-native' +import React from 'react' + +import labels from '../../constants/labels.json' +import DocumentBuilder from '../../testing/DocumentBuilder' +import { mockUseLoadAsyncWithData } from '../../testing/mockUseLoadFromEndpoint' +import render from '../../testing/render' +import VocabularyList from '../VocabularyList' + +jest.mock('../AudioPlayer', () => { + const Text = require('react-native').Text + return () => AudioPlayer +}) + +describe('VocabularyList', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + const onItemPress = jest.fn() + const documents = new DocumentBuilder(2).build() + + it('should display vocabulary list', () => { + mockUseLoadAsyncWithData(documents) + + const { getByText, getAllByText, getAllByTestId } = render( + + ) + + expect(getByText(labels.exercises.vocabularyList.title)).toBeTruthy() + expect(getByText(`2 ${labels.general.words}`)).toBeTruthy() + expect(getByText('Der')).toBeTruthy() + expect(getByText('Spachtel')).toBeTruthy() + expect(getByText('Das')).toBeTruthy() + expect(getByText('Auto')).toBeTruthy() + expect(getAllByText('AudioPlayer')).toHaveLength(2) + expect(getAllByTestId('image')).toHaveLength(2) + }) + + it('should call onItemPress with correct index', () => { + const { getByText } = render() + + expect(onItemPress).toHaveBeenCalledTimes(0) + + fireEvent.press(getByText('Auto')) + + expect(onItemPress).toHaveBeenCalledTimes(1) + expect(onItemPress).toHaveBeenCalledWith(1) + + fireEvent.press(getByText('Spachtel')) + + expect(onItemPress).toHaveBeenCalledTimes(2) + expect(onItemPress).toHaveBeenCalledWith(0) + }) +}) diff --git a/src/routes/vocabulary-list/components/__tests__/VocabularyListItem.spec.tsx b/src/components/__tests__/VocabularyListItem.spec.tsx similarity index 57% rename from src/routes/vocabulary-list/components/__tests__/VocabularyListItem.spec.tsx rename to src/components/__tests__/VocabularyListItem.spec.tsx index a2ea20c39..ed668dc66 100644 --- a/src/routes/vocabulary-list/components/__tests__/VocabularyListItem.spec.tsx +++ b/src/components/__tests__/VocabularyListItem.spec.tsx @@ -1,14 +1,17 @@ +import { fireEvent } from '@testing-library/react-native' import React from 'react' import 'react-native' -import { ARTICLES } from '../../../../constants/data' -import { Document } from '../../../../constants/endpoints' -import render from '../../../../testing/render' +import { ARTICLES } from '../../constants/data' +import { Document } from '../../constants/endpoints' +import render from '../../testing/render' import VocabularyListItem from '../VocabularyListItem' -jest.mock('../../../../components/AudioPlayer', () => () => null) +jest.mock('../AudioPlayer', () => () => null) describe('VocabularyListItem', () => { + const onPress = jest.fn() + const document: Document = { article: ARTICLES[1], audio: '', @@ -19,17 +22,21 @@ describe('VocabularyListItem', () => { } it('should display image passed to it', () => { - const { getByTestId } = render() + const { getByTestId } = render() expect(getByTestId('image')).toHaveProp('source', { uri: document.document_image[0].image }) }) it('should display article passed to it', () => { - const { queryByText } = render() + const { queryByText } = render() expect(queryByText(document.article.value)).toBeTruthy() }) it('should display word passed to it', () => { - const { queryByText } = render() - expect(queryByText(document.word)).toBeTruthy() + const { getByText } = render() + expect(getByText(document.word)).toBeTruthy() + + expect(onPress).toHaveBeenCalledTimes(0) + fireEvent.press(getByText(document.word)) + expect(onPress).toHaveBeenCalledTimes(1) }) }) diff --git a/src/navigation/NavigationTypes.ts b/src/navigation/NavigationTypes.ts index a7c2019e9..b91faec9b 100644 --- a/src/navigation/NavigationTypes.ts +++ b/src/navigation/NavigationTypes.ts @@ -16,6 +16,10 @@ interface ExerciseParams { closeExerciseAction: CommonNavigationAction } +type DetailExerciseParams = ExerciseParams & { + documentIndex: number +} + export interface ExercisesParams extends Omit { discipline: Discipline documents: Document[] | null @@ -47,6 +51,7 @@ export type RoutesParams = { discipline: Discipline initialSelection: boolean } + VocabularyDetail: DetailExerciseParams Exercises: ExercisesParams VocabularyList: ExerciseParams WordChoiceExercise: ExerciseParams diff --git a/src/navigation/Navigator.tsx b/src/navigation/Navigator.tsx index d6ecd3007..24b7f89e8 100644 --- a/src/navigation/Navigator.tsx +++ b/src/navigation/Navigator.tsx @@ -7,10 +7,11 @@ import labels from '../constants/labels.json' import { useTabletHeaderHeight } from '../hooks/useTabletHeaderHeight' import ResultDetailScreen from '../routes/ResultDetailScreen' import ResultScreen from '../routes/ResultScreen' +import VocabularyDetailScreen from '../routes/VocabularyDetailScreen' +import VocabularyListScreen from '../routes/VocabularyListScreen' import ArticleChoiceExerciseScreen from '../routes/choice-exercises/ArticleChoiceExerciseScreen' import WordChoiceExerciseScreen from '../routes/choice-exercises/WordChoiceExerciseScreen' import ExerciseFinishedScreen from '../routes/exercise-finished/ExerciseFinishedScreen' -import VocabularyListScreen from '../routes/vocabulary-list/VocabularyListScreen' import WriteExerciseScreen from '../routes/write-exercise/WriteExerciseScreen' import BottomTabNavigator from './BottomTabNavigator' import { RoutesParams } from './NavigationTypes' @@ -33,6 +34,11 @@ const HomeStackNavigator = (): JSX.Element | null => { component={VocabularyListScreen} options={({ navigation }) => options(overviewExercises, navigation, false)} /> + options('', navigation, true)} + /> prop.theme.colors.background}; @@ -73,7 +73,12 @@ const ResultDetailScreen = ({ route, navigation }: ResultScreenProps): JSX.Eleme /> ) - const Item = ({ item }: { item: DocumentResult }): JSX.Element => + const Item = ({ item, index }: { item: DocumentResult; index: number }): JSX.Element => ( + navigation.navigate('VocabularyDetail', { ...route.params, documentIndex: index })} + /> + ) const repeatIncorrectEntries = (): void => navigation.navigate('WriteExercise', { diff --git a/src/routes/VocabularyDetailScreen.tsx b/src/routes/VocabularyDetailScreen.tsx new file mode 100644 index 000000000..e3d873359 --- /dev/null +++ b/src/routes/VocabularyDetailScreen.tsx @@ -0,0 +1,75 @@ +import { RouteProp } from '@react-navigation/native' +import { StackNavigationProp } from '@react-navigation/stack' +import React, { ReactElement } from 'react' +import styled from 'styled-components/native' + +import { ArrowRightIcon } from '../../assets/images' +import AudioPlayer from '../components/AudioPlayer' +import Button from '../components/Button' +import ExerciseHeader from '../components/ExerciseHeader' +import ImageCarousel from '../components/ImageCarousel' +import WordItem from '../components/WordItem' +import { BUTTONS_THEME } from '../constants/data' +import labels from '../constants/labels.json' +import { RoutesParams } from '../navigation/NavigationTypes' + +const ItemContainer = styled.View` + margin: ${props => props.theme.spacings.xl} 0; + height: 10%; + width: 85%; + align-self: center; +` + +const ButtonContainer = styled.View` + display: flex; + align-self: center; +` + +interface VocabularyDetailScreenProps { + route: RouteProp + navigation: StackNavigationProp +} + +const VocabularyDetailScreen = ({ route, navigation }: VocabularyDetailScreenProps): ReactElement => { + const { documents, documentIndex } = route.params + const document = documents[documentIndex] + const { word, article, document_image: image } = document + const hasNextDocument = documentIndex + 1 < documents.length + + const goToNextWord = () => + navigation.navigate('VocabularyDetail', { ...route.params, documentIndex: documentIndex + 1 }) + + return ( + <> + + + + + + + + {hasNextDocument ? ( +