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

fix(PN-10282): implement pasteHandler for OTP #1159

Merged
merged 27 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cdc8cd7
fix(PN-10282): fix paste OTP code in CodeInput component
SarahDonvito Mar 15, 2024
cc5344c
fix(PN-10282): add test on pasteHandler
SarahDonvito Mar 15, 2024
7ff4b73
Merge branch 'develop' of github.com:pagopa/pn-frontend into fix/PN-1…
SarahDonvito Mar 19, 2024
258d429
fix(PN-10282): change request - improve pasteHandler function and fi…
SarahDonvito Mar 20, 2024
b18d4bb
Merge branch 'develop' of github.com:pagopa/pn-frontend into fix/PN-1…
SarahDonvito Mar 20, 2024
bb02875
fix(PN-10282): change request
SarahDonvito Mar 20, 2024
2783364
Merge branch 'develop' of github.com:pagopa/pn-frontend into fix/PN-1…
SarahDonvito Mar 26, 2024
ec464f0
fix(PN-10282): wip on code input paste handler
SarahDonvito Mar 26, 2024
2040387
Merge branch 'develop' of github.com:pagopa/pn-frontend into fix/PN-1…
SarahDonvito Mar 26, 2024
363d3dd
fix(PN-10282): update ux when user pastes wrong code (longer, shorter…
SarahDonvito Mar 27, 2024
20c8a8c
fix(PN-10282): fix focus input when less than 5 char
SarahDonvito Mar 27, 2024
13912e2
fix(PN-10282): wip on tests
SarahDonvito Mar 27, 2024
f8ef93b
fix(PN-102828): wip on tests
SarahDonvito Mar 27, 2024
244a4bb
fix(PN-10282): implement tests on Code Input and Code Modal
SarahDonvito Mar 27, 2024
63d092c
fix(PN-10282): change request applied
SarahDonvito Mar 29, 2024
97b749c
Merge branch 'develop' of github.com:pagopa/pn-frontend into fix/PN-1…
SarahDonvito Mar 29, 2024
4240209
fix(PN-10282): fix tests, rename key for error message in case of let…
SarahDonvito Mar 29, 2024
7bbc83f
enforced check on code value inserted
AndreaCimini90 Mar 29, 2024
6493a30
added some comment
AndreaCimini90 Mar 29, 2024
9b11e50
fix(PN-10282): fix test and wip on fix bug on input 0 and backspace a…
SarahDonvito Mar 29, 2024
a6710c4
Merge branch 'fix/PN-10282' of github.com:pagopa/pn-frontend into fix…
SarahDonvito Mar 29, 2024
5541107
fix(PN-10282): change requests
SarahDonvito Mar 29, 2024
7b09ad9
fix(PN-10282): fix test
SarahDonvito Mar 29, 2024
9657172
fix(PN-10282): fix tests
SarahDonvito Mar 29, 2024
449c78e
Merge branch 'develop' of github.com:pagopa/pn-frontend into fix/PN-1…
SarahDonvito Mar 29, 2024
a386aa6
fix(PN-10282): update copy for error, fix change handler and wip on test
SarahDonvito Mar 29, 2024
25b19dd
fixed tests
AndreaCimini90 Apr 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions packages/pn-commons/src/components/CodeModal/CodeInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ChangeEvent,
ClipboardEvent,
Fragment,
KeyboardEvent,
memo,
Expand All @@ -14,6 +15,7 @@ import { TextField } from '@mui/material';
type Props = {
initialValues: Array<string>;
onChange: (values: Array<string>) => void;
onInputError: () => void;
isReadOnly?: boolean;
hasError?: boolean;
};
Expand All @@ -24,8 +26,9 @@ type Props = {
* @param isReadOnly set if code is in readonly mode
* @param hasError set if there is an error
* @param onChange function to listen on inputs changes
* @param onInputError function to listen on inputs type errors
*/
const CodeInput = ({ initialValues, isReadOnly, hasError, onChange }: Props) => {
const CodeInput = ({ initialValues, isReadOnly, hasError, onChange, onInputError }: Props) => {
const [currentValues, setCurrentValues] = useState(initialValues);
const inputsRef = useRef(new Array(initialValues.length).fill(undefined));

Expand Down Expand Up @@ -90,6 +93,9 @@ const CodeInput = ({ initialValues, isReadOnly, hasError, onChange }: Props) =>
const inputsValues = [...previousValues];
// eslint-disable-next-line functional/immutable-data
inputsValues[index] = value;
if (!Number(inputsValues[index])) {
onInputError();
}
return inputsValues;
});
};
Expand All @@ -102,8 +108,6 @@ const CodeInput = ({ initialValues, isReadOnly, hasError, onChange }: Props) =>
changeInputValue(value, index);
return;
}
// remove non numeric char from value
value = value.replace(/[^\d]/g, '');
if (value !== '') {
// case maxLength 2
if (value.length > 1) {
Expand All @@ -116,6 +120,38 @@ const CodeInput = ({ initialValues, isReadOnly, hasError, onChange }: Props) =>
}
};

const pasteHandler = (event: ClipboardEvent<HTMLDivElement>) => {
event.preventDefault();
const pastedCode = event.clipboardData.getData('text');
// Ensure the copied code matches the required length
const maxLengthRequiredCode = pastedCode.slice(0, initialValues.length);
const values = maxLengthRequiredCode.split('');

if (values.length !== initialValues.length) {
const lastInput = values.length;
focusInput(lastInput);
const fillEmptyInputs = () => {
if (initialValues.length - values.length !== 0) {
// eslint-disable-next-line functional/immutable-data
values.push('');
fillEmptyInputs();
}
};
fillEmptyInputs();
} else {
// Focus the last input and set cursor at the end, then remove focus.
// it's needed to focus on lastInput before to step on to lastInput + 1
const lastInput = values.length - 1;
focusInput(lastInput);
focusInput(lastInput + 1);
}
setCurrentValues(values);

if (!Number(maxLengthRequiredCode)) {
onInputError();
}
};

useEffect(() => {
onChange(currentValues);
}, [currentValues]);
Expand All @@ -136,13 +172,12 @@ const CodeInput = ({ initialValues, isReadOnly, hasError, onChange }: Props) =>
maxLength: 2,
sx: { padding: '16.5px 10px', textAlign: 'center' },
readOnly: isReadOnly,
pattern: '^[0-9]{1}$',
inputMode: 'numeric',
'data-testid': `code-input-${index}`,
}}
onKeyDown={(event) => keyDownHandler(event, index)}
onChange={(event) => changeHandler(event, index)}
onFocus={(event) => event.target.select()}
onPaste={(event) => pasteHandler(event)}
value={currentValues[index]}
// eslint-disable-next-line functional/immutable-data
inputRef={(node) => (inputsRef.current[index] = node)}
Expand Down
20 changes: 20 additions & 0 deletions packages/pn-commons/src/components/CodeModal/CodeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Props = {
hasError?: boolean;
errorTitle?: string;
errorMessage?: string;
errorInputTypeMessage: string;
};

/**
Expand All @@ -49,6 +50,7 @@ type Props = {
* @param hasError set if there is an error
* @param errorTitle title to show when there is an error
* @param errorMessage message to show when there is an error
* @param errorInputTypeMessage message to show when there is an error on type of input
*/
const CodeModal = memo(
({
Expand All @@ -66,14 +68,21 @@ const CodeModal = memo(
hasError = false,
errorTitle,
errorMessage,
errorInputTypeMessage,
}: Props) => {
const [code, setCode] = useState(initialValues);
const [inputTypeError, setInputTypeError] = useState(false);
const codeIsValid = code.every((v) => v);

const changeHandler = useCallback((inputsValues: Array<string>) => {
setCode(inputsValues);
}, []);

const inputErrorHandler = () => {
console.log('errore di testo');
setInputTypeError(true);
};

const confirmHandler = () => {
if (!confirmCallback) {
return;
Expand Down Expand Up @@ -103,6 +112,7 @@ const CodeModal = memo(
isReadOnly={isReadOnly}
hasError={hasError}
onChange={changeHandler}
onInputError={inputErrorHandler}
/>
{isReadOnly && (
<CopyToClipboardButton
Expand All @@ -127,6 +137,16 @@ const CodeModal = memo(
{errorMessage}
</Alert>
)}
{inputTypeError && !hasError && (
<Alert
id="error-type-alert"
data-testid="errorTypeAlert"
severity="error"
sx={{ mt: 2 }}
>
{errorInputTypeMessage}
</Alert>
)}
</PnDialogContent>
<PnDialogActions>
{cancelLabel && cancelCallback && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { act, fireEvent, render, theme, waitFor } from '../../../test-utils';
import CodeInput from '../CodeInput';

const handleChangeMock = vi.fn();
const inputErrorHandlerMock = vi.fn();

describe('CodeInput Component', () => {
afterEach(() => {
Expand All @@ -15,7 +16,11 @@ describe('CodeInput Component', () => {
it('renders CodeInput (empty inputs)', () => {
// render component
const { getAllByTestId } = render(
<CodeInput initialValues={new Array(5).fill('')} onChange={handleChangeMock} />
<CodeInput
initialValues={new Array(5).fill('')}
onChange={handleChangeMock}
onInputError={inputErrorHandlerMock}
/>
);
const codeInputs = getAllByTestId(/code-input-[0-4]/);
expect(codeInputs).toHaveLength(5);
Expand All @@ -27,7 +32,12 @@ describe('CodeInput Component', () => {
it('renders CodeInput with error (empty inputs)', () => {
// render component
const { getAllByTestId } = render(
<CodeInput initialValues={new Array(5).fill('')} onChange={handleChangeMock} hasError />
<CodeInput
initialValues={new Array(5).fill('')}
onChange={handleChangeMock}
onInputError={inputErrorHandlerMock}
hasError
/>
);
const codeInputs = getAllByTestId(/codeInput\([0-4]\)/);
expect(codeInputs).toHaveLength(5);
Expand All @@ -46,7 +56,12 @@ describe('CodeInput Component', () => {
it('renders CodeInput read only (empty inputs)', () => {
// render component
const { getAllByTestId } = render(
<CodeInput initialValues={new Array(5).fill('')} onChange={handleChangeMock} isReadOnly />
<CodeInput
initialValues={new Array(5).fill('')}
onChange={handleChangeMock}
onInputError={inputErrorHandlerMock}
isReadOnly
/>
);
const codeInputs = getAllByTestId(/codeInput\([0-4]\)/);
expect(codeInputs).toHaveLength(5);
Expand All @@ -62,7 +77,11 @@ describe('CodeInput Component', () => {
it('renders CodeInput (filled inputs)', () => {
// render component
const { getAllByTestId } = render(
<CodeInput initialValues={new Array(5).fill('1')} onChange={handleChangeMock} />
<CodeInput
initialValues={new Array(5).fill('1')}
onChange={handleChangeMock}
onInputError={inputErrorHandlerMock}
/>
);
const codeInputs = getAllByTestId(/code-input-[0-4]/);
expect(codeInputs).toHaveLength(5);
Expand All @@ -74,7 +93,11 @@ describe('CodeInput Component', () => {
it('handles change event', async () => {
// render component
const { getAllByTestId } = render(
<CodeInput initialValues={new Array(5).fill('')} onChange={handleChangeMock} />
<CodeInput
initialValues={new Array(5).fill('')}
onChange={handleChangeMock}
onInputError={inputErrorHandlerMock}
/>
);
const codeInputs = getAllByTestId(/code-input-[0-4]/);
fireEvent.change(codeInputs[2], { target: { value: '3' } });
Expand Down Expand Up @@ -105,14 +128,18 @@ describe('CodeInput Component', () => {
act(() => (codeInputs[2] as HTMLInputElement).focus());
await userEvent.keyboard('{Backspace}');
await waitFor(() => {
expect(codeInputs[2]).toHaveValue('');
expect(codeInputs[2]).toBeEmptyDOMElement();
});
});

it('keyboard events', async () => {
// render component
const { getAllByTestId } = render(
<CodeInput initialValues={new Array(5).fill('1')} onChange={handleChangeMock} />
<CodeInput
initialValues={new Array(5).fill('1')}
onChange={handleChangeMock}
onInputError={inputErrorHandlerMock}
/>
);
// focus on first input and moove to the next
const codeInputs = getAllByTestId(/code-input-[0-4]/);
Expand Down Expand Up @@ -168,4 +195,48 @@ describe('CodeInput Component', () => {
expect(codeInputs[0]).toBe(document.activeElement);
});
});

it('handles paste event', async () => {
// render component
const { getAllByTestId } = render(
<CodeInput
initialValues={new Array(5).fill('')}
onChange={handleChangeMock}
onInputError={inputErrorHandlerMock}
/>
);
const codeInputs = getAllByTestId(/code-input-[0-4]/);

// paste the value of the input and check that it is updated correctly
// set the cursor position to the beggining
act(() => (codeInputs[2] as HTMLInputElement).focus());
(codeInputs[2] as HTMLInputElement).setSelectionRange(0, 0);

// we must use userEvent because the keyboard event must trigger also the change event (fireEvent doesn't do that)
const codePasted = '12345';
await userEvent.paste(codePasted);

await waitFor(() => {
for (let i = 0; i < codeInputs.length; i++) {
expect(codeInputs[i]).toHaveValue(codePasted[i]);
}
});
});

it('accepts first 5 digits in case of too long code (pasted)', async () => {
// render component
const { getAllByTestId } = render(
<CodeInput
initialValues={new Array(5).fill('')}
onChange={handleChangeMock}
onInputError={inputErrorHandlerMock}
/>
);
const codeInputs = getAllByTestId(/code-input-[0-4]/);
act(() => (codeInputs[2] as HTMLInputElement).focus());
(codeInputs[2] as HTMLInputElement).setSelectionRange(0, 0);
const codePasted = '123456'; // too long code
await userEvent.paste(codePasted);
expect(handleChangeMock).toHaveBeenCalledWith(['1', '2', '3', '4', '5']);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { vi } from 'vitest';

import { fireEvent, render, screen, waitFor, within } from '../../../test-utils';
import userEvent from '@testing-library/user-event';

import { act, fireEvent, render, screen, waitFor, within } from '../../../test-utils';
import CodeModal from '../CodeModal';

const cancelButtonMock = vi.fn();
Expand All @@ -24,6 +26,7 @@ const openedModalComponent = (
confirmCallback={confirmButtonMock}
hasError={hasError}
errorMessage="mocked-errorMessage"
errorInputTypeMessage="mocked-errorTypeMessage"
isReadOnly={readonly}
/>
);
Expand Down Expand Up @@ -132,4 +135,42 @@ describe('CodeModal Component', () => {
expect(errorAlert).toBeInTheDocument();
expect(errorAlert).toHaveTextContent('mocked-errorMessage');
});

it('shows error in case of letters as input values', async () => {
// render component
render(openedModalComponent(true));
const dialog = screen.getByTestId('codeDialog');
const codeInputs = within(dialog).getAllByTestId(/code-input-[0-4]/);
fireEvent.change(codeInputs[0], { target: { value: 'A' } });
const errorAlert = screen.getByTestId('errorTypeAlert');
expect(errorAlert).toBeInTheDocument();
expect(errorAlert).toHaveTextContent('mocked-errorTypeMessage');
});

it('error in case of letters (pasted)', async () => {
// render component
render(openedModalComponent(true));
const dialog = screen.getByTestId('codeDialog');
const codeInputs = within(dialog).getAllByTestId(/code-input-[0-4]/);
act(() => (codeInputs[2] as HTMLInputElement).focus());
(codeInputs[2] as HTMLInputElement).setSelectionRange(0, 0);
const codePasted = 'abcd';
await userEvent.paste(codePasted);
const errorAlert = screen.getByTestId('errorTypeAlert');
expect(errorAlert).toBeInTheDocument();
expect(errorAlert).toHaveTextContent('mocked-errorTypeMessage');
});

it('error in case of short code (pasted)', async () => {
// render component
render(openedModalComponent(true));
const dialog = screen.getByTestId('codeDialog');
const codeInputs = within(dialog).getAllByTestId(/code-input-[0-4]/);
act(() => (codeInputs[2] as HTMLInputElement).focus());
(codeInputs[2] as HTMLInputElement).setSelectionRange(0, 0);
const codePasted = '123';
await userEvent.paste(codePasted);
const button = screen.getByTestId('codeConfirmButton');
expect(button).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@
"expired_verification_code": {
"title": "Il codice è scaduto",
"message": "Generane uno nuovo e inseriscilo."
},
"invalid_type_code": {
"title": "Questo campo accetta solo valori numerici"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ const DigitalContactsCodeVerificationProvider: FC<{ children?: ReactNode }> = ({
hasError={codeNotValid}
errorTitle={errorMessage?.title}
errorMessage={errorMessage?.content}
errorInputTypeMessage={t('errors.invalid_type_code.title', { ns: 'recapiti' })}
/>
)}
<PnDialog
Expand Down