Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

M2-7457: Character Counter #851

Merged
3 changes: 3 additions & 0 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,9 @@
"type_placeholder": "Please type text",
"paragraph_placeholder": "Type in your answer here."
},
"character_counter":{
"characters":"{{numberOfCharacters}}/{{limit}} characters"
},
"additional": {
"server-error": "Server error occurred",
"time-end": "Time is up!",
Expand Down
3 changes: 3 additions & 0 deletions assets/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,9 @@
"type_placeholder": "Veuiller saisir du texte",
"paragraph_placeholder": "Tapez votre réponse ici."
},
"character_counter":{
"characters":"{{numberOfCharacters}}/{{limit}} caractères"
},
"language_screen": {
"app_language": " Langue de l'application",
"change_app_language": "Changer la langue",
Expand Down
12 changes: 12 additions & 0 deletions src/features/pass-survey/model/AnswerValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
PipelineItem,
RadioResponse,
TimeRangeResponse,
ParagraphTextResponse,
} from '../lib';

type AnswerValidatorArgs = {
Expand Down Expand Up @@ -106,6 +107,17 @@ function AnswerValidator(params?: AnswerValidatorArgs): IAnswerValidator {

return true;
}
case 'ParagraphText': {
const paragraphResponse =
currentAnswer?.answer as ParagraphTextResponse;
const limit = currentPipelineItem.payload.maxLength;

if (limit < paragraphResponse.length) {
return false;
}

return true;
}
default:
return true;
}
Expand Down
64 changes: 64 additions & 0 deletions src/shared/ui/CharacterCounter.tsx
felipeMetaLab marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { FC } from 'react';
import { StyleSheet } from 'react-native';

import { useTranslation } from 'react-i18next';

import { Logger } from '@app/shared/lib';
import { Text } from '@shared/ui';

import { colors } from '../lib';

type Props = {
limit: number;
numberOfCharacters: number;
fontSize?: number;
focused?: boolean;
};

const CharacterCounter: FC<Props> = ({
numberOfCharacters,
limit,
focused = false,
}) => {
const { t } = useTranslation();
let colorStyle = focused ? styles.focusedColor : styles.unfocusedColor;

if (limit < numberOfCharacters) colorStyle = styles.warnColor;

if (limit <= 0) {
Logger.error('[CharacterCounter] Limit should be higher than 0');
return null;
}

if (numberOfCharacters < 0) {
Logger.error('[CharacterCounter] numberOfCharacters Cannot be less than 0');
return null;
}

return (
<Text style={[styles.characterCounterText, colorStyle]}>
{t('character_counter:characters', { numberOfCharacters, limit })}
</Text>
);
};

const styles = StyleSheet.create({
characterCounterText: {
padding: 2,
margin: 2,
marginRight: 10,
fontWeight: '400',
fontSize: 14,
},
focusedColor: {
color: colors.primary,
},
unfocusedColor: {
color: colors.grey4,
},
warnColor: {
color: colors.errorRed,
},
});

export default CharacterCounter;
7 changes: 4 additions & 3 deletions src/shared/ui/LongTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TextInput } from 'react-native';

import { GetProps, setupReactNative, styled } from '@tamagui/core';
import { focusableInputHOC } from '@tamagui/focusable';
import { isTablet } from 'react-native-device-info';

setupReactNative({
TextInput,
Expand All @@ -17,8 +18,8 @@ export const LongTextInput = styled(
alignSelf: 'stretch',
flex: 1,

minHeight: 56,
maxHeight: 350,
minHeight: 176,
maxHeight: isTablet() ? 350 : null,
width: '100%',
borderRadius: 12,
borderWidth: 2,
Expand All @@ -39,7 +40,7 @@ export const LongTextInput = styled(

focusable: true,
multiline: true,
scrollEnabled: true,
scrollEnabled: isTablet(),
},
{
isInput: true,
Expand Down
2 changes: 2 additions & 0 deletions src/shared/ui/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CheckBox from '@react-native-community/checkbox';

import Center from './Center';
import CharacterCounter from './CharacterCounter';
import Input from './Input';
import KeyboardAvoidingView from './KeyboardAvoidingView';
import Link from './Link';
Expand Down Expand Up @@ -48,6 +49,7 @@ export {
KeyboardAvoidingView,
Input,
LongTextInput,
CharacterCounter,
CheckBox,
ScrollView,
RowButton,
Expand Down
14 changes: 11 additions & 3 deletions src/shared/ui/survey/ParagraphText.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { FC } from 'react';
import React, { FC, useState } from 'react';
import { StyleSheet, TextInputProps, View } from 'react-native';

import { useTranslation } from 'react-i18next';

import { colors } from '@shared/lib';
import { LongTextInput } from '@shared/ui';
import { LongTextInput, CharacterCounter } from '@shared/ui';

type Props = {
onChange: (text: string) => void;
Expand All @@ -15,6 +15,7 @@ type Props = {
} & Omit<TextInputProps, 'value' | 'onChange'>;

const ParagraphText: FC<Props> = ({ value, onChange, config, ...props }) => {
const [paragraphOnFocus, setParagraphOnFocus] = useState(false);
const { maxLength = 50 } = config;
const { t } = useTranslation();

Expand All @@ -29,13 +30,19 @@ const ParagraphText: FC<Props> = ({ value, onChange, config, ...props }) => {
placeholder={t('text_entry:paragraph_placeholder')}
placeholderTextColor={colors.mediumGrey}
onChangeText={onChangeText}
maxLength={maxLength}
value={value}
autoCorrect={false}
multiline={true}
keyboardType={'default'}
onFocus={() => setParagraphOnFocus(true)}
onBlur={() => setParagraphOnFocus(false)}
{...props}
/>
<CharacterCounter
focused={paragraphOnFocus}
limit={maxLength}
numberOfCharacters={value.length}
/>
</View>
);
};
Expand All @@ -45,5 +52,6 @@ export default ParagraphText;
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'flex-end',
},
});
97 changes: 67 additions & 30 deletions src/shared/ui/survey/tests/TextParagraph.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';

import renderer from 'react-test-renderer';
import renderer, { act } from 'react-test-renderer';

import TamaguiProvider from '@app/app/ui/AppProvider/TamaguiProvider';
import { LongTextInput } from '@shared/ui';
import { LongTextInput, CharacterCounter } from '@shared/ui';

import ParagraphText from '../ParagraphText';

Expand All @@ -13,12 +13,12 @@ jest.mock('react-i18next', () => ({
})),
}));

describe('Test Paragraph Text', () => {
describe('ParagraphText Component', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('Should renders correctly with expected props', () => {
it('Should render correctly with expected props', () => {
const mockValue = '1234';

const tree = renderer
Expand All @@ -35,23 +35,30 @@ describe('Test Paragraph Text', () => {
)
.toJSON();

if (!tree || Array.isArray(tree)) {
throw new Error('Tree is not rendered correctly or is an array');
if (!tree || Array.isArray(tree) || !tree.children) {
throw new Error(
'Tree is not rendered correctly or is an array without children',
);
}

const view = tree as any;
const longTextInput = view.children[0];
const [longTextInput] = tree.children;

if (
!longTextInput ||
typeof longTextInput !== 'object' ||
!('props' in longTextInput)
) {
throw new Error('LongTextInput is not rendered correctly');
}

expect(longTextInput.props.placeholder).toBe(
'text_entry:paragraph_placeholder',
);
expect(longTextInput.props.value).toBe('1234');
expect(longTextInput.props.maxLength).toBe(300);
expect(longTextInput.props.value).toBe(mockValue);
});

it('Should call onChange when text is modified', () => {
const mockOnChange = jest.fn();

const tree = renderer.create(
<TamaguiProvider>
<ParagraphText
Expand All @@ -65,30 +72,60 @@ describe('Test Paragraph Text', () => {
const instance = tree.root;
const longTextInput = instance.findByType(LongTextInput);

longTextInput.props.onChangeText('new text');
act(() => {
longTextInput.props.onChangeText('new text');
});

expect(mockOnChange).toHaveBeenCalledWith('new text');
});

it('Should handle maxLength configurations correctly', () => {
const tree = renderer
.create(
<TamaguiProvider>
<ParagraphText
onChange={jest.fn()}
value="test"
config={{ maxLength: 150 }}
/>
</TamaguiProvider>,
)
.toJSON();
it('Should update focus state when input is focused or blurred', () => {
const tree = renderer.create(
<TamaguiProvider>
<ParagraphText
onChange={jest.fn()}
value="test"
config={{ maxLength: 300 }}
/>
</TamaguiProvider>,
);

if (!tree || Array.isArray(tree)) {
throw new Error('Tree is not rendered correctly or is an array');
}
const instance = tree.root;
const longTextInput = instance.findByType(LongTextInput);
const characterCounter = instance.findByType(CharacterCounter);

const view = tree as any;
const longTextInput = view.children[0];
expect(characterCounter.props.focused).toBe(false);

act(() => {
longTextInput.props.onFocus();
});

expect(characterCounter.props.focused).toBe(true);

act(() => {
longTextInput.props.onBlur();
});

expect(characterCounter.props.focused).toBe(false);
});

expect(longTextInput.props.maxLength).toBe(150);
it('Should pass correct number of characters and maxLength to CharacterCounter', () => {
const mockValue = '123456';
const maxLength = 100;

const tree = renderer.create(
<TamaguiProvider>
<ParagraphText
onChange={jest.fn()}
value={mockValue}
config={{ maxLength }}
/>
</TamaguiProvider>,
);

const instance = tree.root;
const characterCounter = instance.findByType(CharacterCounter);
expect(characterCounter.props.numberOfCharacters).toBe(mockValue.length);
expect(characterCounter.props.limit).toBe(maxLength);
});
});
Loading
Loading