Skip to content

Commit

Permalink
fix(InputCode): use inner state to avoid desync state (#159)
Browse files Browse the repository at this point in the history
  • Loading branch information
hcourthias authored Feb 27, 2024
1 parent 49e5881 commit 17bd09d
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 46 deletions.
31 changes: 14 additions & 17 deletions sandbox/src/app/sandbox/docs/input-code.doc.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, InputCode, Text } from '@getluko/streamline';
import { useState } from 'react';
import { useEffect, useState } from 'react';

const SectionTitle = ({ title, desc }: { title: string; desc: string }) => {
return (
Expand All @@ -11,29 +11,23 @@ const SectionTitle = ({ title, desc }: { title: string; desc: string }) => {
};

export const InputCodeSandbox = () => {
const [code, setCode] = useState('');
const [errorCode, setErrorCode] = useState('');
const [isError, setIsError] = useState(true);
const [isDisabled, setIsDisabled] = useState(false);
const [code, setCode] = useState('');

// Fake API call with invalid code, then set isError to true
const handleErrorCodeChange = (value: string) => {
setErrorCode(value);
if (value.length > 0) {
useEffect(() => {
if (code.length > 0) {
setIsError(false);
}

if (value.length === 6) {
if (code.length === 6) {
setIsDisabled(true);
setTimeout(() => {
setIsDisabled(false);
setTimeout(() => {
// Fix to wait for disabled state to be updated on Android
setIsError(true);
}, 0);
setIsError(true);
}, 1000);
}
};
}, [code, setIsError, setIsDisabled]);

return (
<Box marginHorizontal="lg">
Expand All @@ -42,20 +36,23 @@ export const InputCodeSandbox = () => {
desc="Fake API call with invalid code, then set isError to true"
/>
<InputCode
code={errorCode}
onChange={handleErrorCodeChange}
onChange={setCode}
isError={isError}
length={6}
isDisabled={isDisabled}
/>
<SectionTitle title="InputCode with 4 digits" desc="" />
<InputCode code={code} onChange={setCode} length={4} />
<InputCode
onChange={() => {
return;
}}
length={4}
/>
<SectionTitle
title="Disabled InputCode"
desc="Should not be interactive"
/>
<InputCode
code=""
onChange={() => {
return;
}}
Expand Down
42 changes: 23 additions & 19 deletions src/components/inputs/input-code/input-code.hook.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import {
LayoutChangeEvent,
NativeSyntheticEvent,
Expand All @@ -14,29 +14,36 @@ import {
import { InputCodeProps } from './input-code.types';

const useInputCode = ({
code,
onChange,
length = DEFAULT_CODE_LENGTH,
isDisabled,
isError,
}: InputCodeProps) => {
const [code, setCode] = useState('');
const ref = useRef<TextInput>(null);

const [focused, setFocused] = useState(false);
const [width, setWidth] = useState(0);

useEffect(() => {
onChange(code);
}, [code, onChange]);

useEffect(() => {
if (isError && code.length === length) {
onChange('');
ref.current?.focus();
setCode('');
// This is a workaround to fix the issue with the input not being focused on android
setTimeout(() => {
ref.current?.focus();
}, 0);
}
}, [code.length, isError, length, onChange]);

const handleBackspace = (
e: NativeSyntheticEvent<TextInputKeyPressEventData>
) => {
if (e.nativeEvent.key === 'Backspace') {
onChange(code.substring(0, code.length - 1));
setCode((prev) => prev.substring(0, prev.length - 1));
}
};

Expand All @@ -47,20 +54,17 @@ const useInputCode = ({
ref.current?.focus();
};

const handleCodeChange = useCallback(
(text: string) => {
if (text.length === length) {
const sanitizedCode = text.replace(/[^0-9]/g, '');
onChange(sanitizedCode);
return;
}
if (code.length < length) {
const valueInput = text.substring(text.length - 1, text.length);
onChange((code + valueInput).substring(0, length));
}
},
[code, length, onChange]
);
const handleCodeChange = (text: string) => {
if (text.length === length) {
const sanitizedCode = text.replace(/[^0-9]/g, '');
setCode(sanitizedCode);
return;
}
if (code.length < length) {
const valueInput = text.substring(text.length - 1, text.length);
setCode((prev) => (prev + valueInput).substring(0, length));
}
};

const handleOnBlur = () => {
setFocused(false);
Expand Down
9 changes: 2 additions & 7 deletions src/components/inputs/input-code/input-code.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,15 @@ import { renderWithProvider } from '../../../testing/render-with-provider';
describe('InputCode', () => {
it('should render successfully', () => {
const { UNSAFE_root } = renderWithProvider(
<InputCode code="" onChange={jest.fn()} length={4} />
<InputCode onChange={jest.fn()} length={4} />
);
expect(UNSAFE_root).toBeTruthy();
});

it('should call onChange with the correct code when inputting values', () => {
const onChangeMock = jest.fn();
const { getByTestId } = renderWithProvider(
<InputCode
code=""
onChange={onChangeMock}
length={4}
testID="input-code"
/>
<InputCode onChange={onChangeMock} length={4} testID="input-code" />
);

fireEvent.changeText(getByTestId('input-code-text-field'), '1234');
Expand Down
3 changes: 1 addition & 2 deletions src/components/inputs/input-code/input-code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export const InputCode = ({
length = DEFAULT_CODE_LENGTH,
isDisabled,
isError,
code,
onChange,
autoFocus = false,
testID,
Expand All @@ -26,10 +25,10 @@ export const InputCode = ({
isFocused,
inputStyle,
inputRef,
code,
handleOnContainerPress,
} = useInputCode({
length,
code,
onChange,
isDisabled,
isError,
Expand Down
1 change: 0 additions & 1 deletion src/components/inputs/input-code/input-code.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export interface InputCodeProps {
code: string;
onChange: (code: string) => void;
length?: number;
isError?: boolean;
Expand Down

0 comments on commit 17bd09d

Please sign in to comment.