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
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;
5 changes: 3 additions & 2 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 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
13 changes: 11 additions & 2 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 @@ -34,8 +35,15 @@ const ParagraphText: FC<Props> = ({ value, onChange, config, ...props }) => {
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 +53,6 @@ export default ParagraphText;
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'flex-end',
},
});
120 changes: 120 additions & 0 deletions src/shared/ui/tests/CharacterCounter.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React from 'react';
import { Text } from 'react-native';

import { useTranslation } from 'react-i18next';
import renderer from 'react-test-renderer';

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

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

jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: jest.fn((key, options) => {
if (key === 'character_counter:characters') {
return `${options.numberOfCharacters}/${options.limit} characters`;
}
return key;
}),
}),
}));

describe('CharacterCounter Component', () => {
it('Should apply the primary color when focused', () => {
const tree = renderer.create(
<TamaguiProvider>
<CharacterCounter numberOfCharacters={10} limit={20} focused={true} />
</TamaguiProvider>,
);

const textElement = tree.root.findByType(Text);
expect(textElement.props.style).toEqual(
expect.arrayContaining([
expect.objectContaining({ color: colors.primary }),
]),
);
});

it('Should apply the grey color when not focused', () => {
const tree = renderer.create(
<TamaguiProvider>
<CharacterCounter numberOfCharacters={10} limit={20} focused={false} />
</TamaguiProvider>,
);

const textElement = tree.root.findByType(Text);
expect(textElement.props.style).toEqual(
expect.arrayContaining([
expect.objectContaining({ color: colors.grey4 }),
]),
);
});

it('Should display the correct character count', () => {
const tree = renderer.create(
<TamaguiProvider>
<CharacterCounter numberOfCharacters={150} limit={200} />
</TamaguiProvider>,
);

const textElement = tree.root.findByType(Text);

const characterCountText = Array.isArray(textElement.props.children)
? textElement.props.children.join('')
: textElement.props.children;

expect(characterCountText).toBe('150/200 characters');
});

it('Should handle extreme values correctly', () => {
const tree = renderer.create(
<TamaguiProvider>
<CharacterCounter numberOfCharacters={9999} limit={10000} />
</TamaguiProvider>,
);

const textElement = tree.root.findByType(Text);

const characterCountText = Array.isArray(textElement.props.children)
? textElement.props.children.join('')
: textElement.props.children;

expect(characterCountText).toBe('9999/10000 characters');
});

it('Should handle missing translation key gracefully', () => {
jest
.spyOn(useTranslation(), 't')
.mockImplementation(() => 'Translation missing');

const tree = renderer.create(
<TamaguiProvider>
<CharacterCounter numberOfCharacters={50} limit={100} />
</TamaguiProvider>,
);

const textElement = tree.root.findByType(Text);

const characterCountText = Array.isArray(textElement.props.children)
? textElement.props.children.join('')
: textElement.props.children;

expect(characterCountText).toBe('50/100 characters');
});

it('Should apply custom styles from the stylesheet', () => {
const tree = renderer.create(
<TamaguiProvider>
<CharacterCounter numberOfCharacters={25} limit={50} />
</TamaguiProvider>,
);

const textElement = tree.root.findByType(Text);
expect(textElement.props.style).toEqual(
expect.arrayContaining([
expect.objectContaining({ padding: 2, margin: 2, marginRight: 10 }),
]),
);
});
});
Loading