Skip to content

Commit

Permalink
feat(PinField): new component (#902)
Browse files Browse the repository at this point in the history
* feat(OtpField): new component

* complete component features

* add labels for individual inputs. Prevent multiple calls for otp sms

* usability fixes

* acceptance tests

* snippets

* add web otp api url comment

* placeholder for hideCode case

* fixes from CR

* prevent focusing next input when a non-number is typed

* prevent glitches reported in CR

* rename to PinField

* increase test timeout in sheet-test

* allow to disable sms read with a prop

* fix input background when focused

* feat(skin): update design tokens (#893)

* feat(skin): update design tokens

* reverse movistar-legacy

---------

Co-authored-by: yceballost <[email protected]>
Co-authored-by: Abel Toledano <[email protected]>

* chore(release): 14.25.0 [skip ci]

# [14.25.0](v14.24.1...v14.25.0) (2023-10-09)

### Features

* **skin:** update design tokens ([#893](#893)) ([736a655](736a655))
* **Slider:** new component ([#867](#867)) ([4e02a0d](4e02a0d))

* feat(Menu): fix component styles and add MenuItem/MenuSection (#892)

* deprecate Menu and rename it to DropdownMenu

* multiple fixes in menu component

* update styles, add menu items and section components

* revert name change and add check to menuItem

* fix error, update screenshots and add playroom snippet

* fix close menu animations

* add aria props to menu items

* add keyboard interactivity

* fix logic for keyboard navigation

* resolve comments

* update screenshots

* updates in component interaction

* fix test

* fix playroom snippet

* udpate menu story and screenshot test

* remove obsolete tabIndex logic in checkbox

* avoid user pressing an option multiple times while menu is closing

* rename focusable to focused in variables

* fix aria label prop name

* add focus to target on menu close

* remove close function call in story

* update transitions and cleanup code (#901)

* resolve comments and fix overlay hidden children

* resolve comments and update screenshots

* add accessibility props to target

* fix typo

* use item indices instead of labels for focusing logic

* fix aria-expanded

* fix accesibility issue in mistica lab story

* code cleanup

* remove hack for menu container role

* chore(SonarQube): Setup SonarQube (#903)

* WEB-1570 configure sonarqube

* WEB-1570 remove and ignore .scannerwork

* WEB-1570 setup gh workflow

* WEB-1570 setup gh workflow

* WEB-1570 setup gh workflow

* WEB-1570 setup gh workflow

* WEB-1570 setup gh workflow

* WEB-1570 revert workflow

* WEB-1570 add comment

---------

Co-authored-by: Pedro Ladaria <[email protected]>

* feat(Accordion): create Accordion and BoxedAccordion components (#900)

* first POC

* fix chevron styling and overflow bug

* update accordions

* unmount panel on close and use CSSTransition

* add singleOpen and update opened items internal logic

* add unit tests and playroom snippets

* add accordion stories

* add screenshot tests

* resolve comments and remove GroupedAccordion

* resolve comments

* fix accordion test

* use waitFor in accordion test

* remove unnecesary async in accordion tests

* fix(Video, DisplayMediaCard, PosterCard): show fallback on empty src (#910)

* show fallback on empty src for video/cards

* remove console log and update tests

* fix screenshot tests

* remove placeholder

---------

Co-authored-by: Flow <[email protected]>
Co-authored-by: yceballost <[email protected]>
Co-authored-by: semantic-release-bot <[email protected]>
Co-authored-by: Marcos Kolodny <[email protected]>
Co-authored-by: Pedro Ladaria <[email protected]>
Co-authored-by: Pedro Ladaria <[email protected]>
  • Loading branch information
7 people authored Oct 16, 2023
1 parent 4e366d3 commit 484dbc7
Show file tree
Hide file tree
Showing 17 changed files with 636 additions and 30 deletions.
2 changes: 2 additions & 0 deletions playroom/snippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ const formSnippets: Array<Snippet> = [
' </Stack>\n' +
'</RadioGroup>',
],
['PinField', '<PinField name="otp" aria-label="OTP" />'],
['PinField (hideCode)', '<PinField hideCode name="pin" aria-label="PIN" />'],
[
'Form',
`<Form
Expand Down
90 changes: 90 additions & 0 deletions src/__acceptance_tests__/form-fields-acceptance-test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {openStoryPage, screen, waitFor} from '../test-utils';
import {within} from '@telefonica/acceptance-testing';

import type {ElementHandle, PageApi} from '../test-utils';

Expand Down Expand Up @@ -201,3 +202,92 @@ test.each(STORY_TYPES)('SearchField (%s)', async (storyType) => {
await page.click(await screen.findByLabelText('Borrar búsqueda'));
expect(await getValue(field)).toBe('');
});

test.each(STORY_TYPES)('PinField (%s)', async (storyType) => {
await openStoryPage(getStoryOfType(storyType));

const fieldGroup = await screen.findByLabelText('OTP');
const firstDigitField = await within(fieldGroup).findByLabelText('Dígito 1 de 6');
await firstDigitField.type('123456');

await screen.findByText("onChange: (string) '123456'");
await screen.findByText("onChangeValue: (string) '123456'");
});

test.each(STORY_TYPES)('PinField (hideCode) (%s)', async (storyType) => {
await openStoryPage(getStoryOfType(storyType));

const fieldGroup = await screen.findByLabelText('PIN');
const firstDigitField = await within(fieldGroup).findByLabelText('Dígito 1 de 6');
await firstDigitField.type('123456');

await screen.findByText("onChange: (string) '123456'");
await screen.findByText("onChangeValue: (string) '123456'");
});

test('PinField focus management', async () => {
await openStoryPage(CONTROLLED_STORY);

const fieldGroup = await screen.findByLabelText('OTP');

const firstDigitField = await within(fieldGroup).findByLabelText('Dígito 1 de 6');
const secondDigitField = await within(fieldGroup).findByLabelText('Dígito 2 de 6');
const thirdDigitField = await within(fieldGroup).findByLabelText('Dígito 3 de 6');
const forthDigitField = await within(fieldGroup).findByLabelText('Dígito 4 de 6');

// try to focus forth field, but the first one is focused instead
await forthDigitField.focus();
expect(await forthDigitField.evaluate((el) => el === document.activeElement)).toBe(false);
expect(await firstDigitField.evaluate((el) => el === document.activeElement)).toBe(true);

// focus is moved to second field after typing
await firstDigitField.type('1');
expect(await secondDigitField.evaluate((el) => el === document.activeElement)).toBe(true);

await secondDigitField.evaluate((el) => (el as HTMLInputElement).blur());
expect(await secondDigitField.evaluate((el) => el === document.activeElement)).toBe(false);

// try to focus forth field, but the second one is focused instead
await forthDigitField.focus();
expect(await secondDigitField.evaluate((el) => el === document.activeElement)).toBe(true);

// focus is moved to third field after typing
await secondDigitField.type('2');
expect(await thirdDigitField.evaluate((el) => el === document.activeElement)).toBe(true);

// move to previous field with left arrow
await thirdDigitField.press('ArrowLeft');
expect(await secondDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
await secondDigitField.press('ArrowLeft');
expect(await firstDigitField.evaluate((el) => el === document.activeElement)).toBe(true);

// type a number to overwrite the first field value
await firstDigitField.type('9');
await screen.findByText("onChange: (string) '92'");
expect(await secondDigitField.evaluate((el) => el === document.activeElement)).toBe(true);

// move to next field with right arrow
await secondDigitField.press('ArrowRight');
expect(await thirdDigitField.evaluate((el) => el === document.activeElement)).toBe(true);

// type a new number
await thirdDigitField.type('3');
await screen.findByText("onChange: (string) '923'");
expect(await forthDigitField.evaluate((el) => el === document.activeElement)).toBe(true);

// go back with Backspace
await forthDigitField.press('Backspace');
await screen.findByText("onChange: (string) '923'");
expect(await thirdDigitField.evaluate((el) => el === document.activeElement)).toBe(true);

// delete with Backspace
await thirdDigitField.press('Backspace');
await screen.findByText("onChange: (string) '92'");
expect(await secondDigitField.evaluate((el) => el === document.activeElement)).toBe(true);

// move left with left arrow and delete with Delete key
await secondDigitField.press('ArrowLeft');
expect(await firstDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
await firstDigitField.press('Delete');
await screen.findByText("onChange: (string) '2'");
}, 1200000);
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions src/__screenshot_tests__/form-fields-screenshot-test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {openStoryPage, screen} from '../test-utils';
import {within} from '@telefonica/acceptance-testing';

import type {Device} from '../test-utils';

Expand Down Expand Up @@ -219,3 +220,33 @@ test('Very long label should show ellipsis', async () => {

expect(await fieldWrapper.screenshot()).toMatchImageSnapshot();
});

test('PinField', async () => {
await openStoryPage({
id: 'components-input-fields--types-uncontrolled',
device: 'MOBILE_IOS',
});

const fieldGroup = await screen.findByLabelText('OTP');
expect(await fieldGroup.screenshot()).toMatchImageSnapshot();

const firstDigitField = await within(fieldGroup).findByLabelText('Dígito 1 de 6');
await firstDigitField.focus();
expect(await fieldGroup.screenshot()).toMatchImageSnapshot();

await firstDigitField.type('1');
expect(await fieldGroup.screenshot()).toMatchImageSnapshot();
});

test('PinField (hideCode)', async () => {
await openStoryPage({
id: 'components-input-fields--types-uncontrolled',
device: 'MOBILE_IOS',
});

const fieldGroup = await screen.findByLabelText('PIN');

const firstDigitField = await within(fieldGroup).findByLabelText('Dígito 1 de 6');
await firstDigitField.type('1');
expect(await fieldGroup.screenshot()).toMatchImageSnapshot();
});
51 changes: 51 additions & 0 deletions src/__stories__/field-story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
Form,
Title1,
Stack,
PinField,
} from '..';
import {inspect} from 'util';
import IconMusicRegular from '../generated/mistica-icons/icon-music-regular';
Expand Down Expand Up @@ -461,6 +462,31 @@ export const TypesUncontrolled: StoryComponent = () => (
/>
)}
</Uncontrolled>

<Uncontrolled title="PinField">
{(handleChange, handleChangeValue) => (
<PinField
name="otp"
aria-label="OTP"
defaultValue=""
onChange={handleChange}
onChangeValue={handleChangeValue}
/>
)}
</Uncontrolled>

<Uncontrolled title="PinField (hideCode)">
{(handleChange, handleChangeValue) => (
<PinField
hideCode
name="pin"
aria-label="PIN"
defaultValue=""
onChange={handleChange}
onChangeValue={handleChangeValue}
/>
)}
</Uncontrolled>
</>
);

Expand Down Expand Up @@ -708,6 +734,31 @@ export const TypesControlled = (): React.ReactNode => (
</div>
)}
</Controlled>

<Controlled title="PinField" initialValue="">
{(handleChange, handleChangeValue, value) => (
<PinField
name="otp"
aria-label="OTP"
onChange={handleChange}
onChangeValue={handleChangeValue}
value={value}
/>
)}
</Controlled>

<Controlled title="PinField (hideCode)" initialValue="">
{(handleChange, handleChangeValue, value) => (
<PinField
hideCode
name="pin"
aria-label="PIN"
onChange={handleChange}
onChangeValue={handleChangeValue}
value={value}
/>
)}
</Controlled>
</>
);

Expand Down
26 changes: 13 additions & 13 deletions src/__tests__/sheet-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ test('Sheet', async () => {

await userEvent.click(closeButton);
await waitForElementToBeRemoved(sheet);
});
}, 20000);

test('RadioListSheet', async () => {
const selectSpy = jest.fn();
Expand Down Expand Up @@ -95,7 +95,7 @@ test('RadioListSheet', async () => {

await waitForElementToBeRemoved(sheet);
expect(selectSpy).toHaveBeenCalledWith('1');
}, 15000);
}, 20000);

test('ActionsListSheet', async () => {
const selectSpy = jest.fn();
Expand Down Expand Up @@ -144,7 +144,7 @@ test('ActionsListSheet', async () => {

await waitForElementToBeRemoved(sheet);
expect(selectSpy).toHaveBeenCalledWith('1');
});
}, 20000);

test('InfoSheet', async () => {
const TestComponent = () => {
Expand Down Expand Up @@ -193,7 +193,7 @@ test('InfoSheet', async () => {

const items = await within(itemList).findAllByRole('listitem');
expect(items).toHaveLength(2);
});
}, 20000);

test('ActionsSheet', async () => {
const onPressButtonSpy = jest.fn();
Expand Down Expand Up @@ -249,7 +249,7 @@ test('ActionsSheet', async () => {

await waitForElementToBeRemoved(sheet);
expect(onPressButtonSpy).toHaveBeenCalledWith('SECONDARY');
});
}, 20000);

test('showSheet INFO', async () => {
const resultSpy = jest.fn();
Expand Down Expand Up @@ -277,7 +277,7 @@ test('showSheet INFO', async () => {

await waitForElementToBeRemoved(sheet);
expect(resultSpy).toHaveBeenCalledWith(undefined);
});
}, 20000);

test('showSheet ACTIONS_LIST', async () => {
const resultSpy = jest.fn();
Expand Down Expand Up @@ -309,7 +309,7 @@ test('showSheet ACTIONS_LIST', async () => {

await waitForElementToBeRemoved(sheet);
expect(resultSpy).toHaveBeenCalledWith({action: 'SUBMIT', selectedId: '2'});
});
}, 20000);

test('showSheet ACTIONS_LIST dismiss', async () => {
const resultSpy = jest.fn();
Expand Down Expand Up @@ -340,7 +340,7 @@ test('showSheet ACTIONS_LIST dismiss', async () => {

await waitForElementToBeRemoved(sheet);
expect(resultSpy).toHaveBeenCalledWith({action: 'DISMISS'});
});
}, 20000);

test('showSheet RADIO_LIST', async () => {
const resultSpy = jest.fn();
Expand Down Expand Up @@ -374,7 +374,7 @@ test('showSheet RADIO_LIST', async () => {

await waitForElementToBeRemoved(sheet);
expect(resultSpy).toHaveBeenCalledWith({action: 'SUBMIT', selectedId: '2'});
});
}, 20000);

test('showSheet RADIO_LIST dismiss', async () => {
const resultSpy = jest.fn();
Expand Down Expand Up @@ -405,7 +405,7 @@ test('showSheet RADIO_LIST dismiss', async () => {

await waitForElementToBeRemoved(sheet);
expect(resultSpy).toHaveBeenCalledWith({action: 'DISMISS'});
});
}, 20000);

test('showSheet ACTIONS', async () => {
const resultSpy = jest.fn();
Expand Down Expand Up @@ -444,7 +444,7 @@ test('showSheet ACTIONS', async () => {

await waitForElementToBeRemoved(sheet);
expect(resultSpy).toHaveBeenCalledWith({action: 'LINK'});
});
}, 20000);

test('showSheet ACTIONS dismiss', async () => {
const resultSpy = jest.fn();
Expand Down Expand Up @@ -476,7 +476,7 @@ test('showSheet ACTIONS dismiss', async () => {

await waitForElementToBeRemoved(sheet);
expect(resultSpy).toHaveBeenCalledWith({action: 'DISMISS'});
});
}, 20000);

test('showSheet fails if SheetRoot is not rendered', async () => {
await expect(
Expand Down Expand Up @@ -743,4 +743,4 @@ test('showSheet with native implementation fallbacks to web if native fails', as

await waitForElementToBeRemoved(sheet);
expect(resultSpy).toHaveBeenCalledWith({action: 'LINK'});
});
}, 20000);
18 changes: 9 additions & 9 deletions src/form-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,17 @@ export const useFieldProps = ({
onChangeValue,
}: {
name: string;
value: string | undefined;
defaultValue: string | undefined;
value?: string;
defaultValue?: string;
processValue: (value: string) => unknown;
helperText: string | undefined;
optional: boolean | undefined;
error: boolean | undefined;
disabled: boolean | undefined;
helperText?: string;
optional?: boolean;
error?: boolean;
disabled?: boolean;
onBlur?: React.FocusEventHandler;
validate: undefined | ((value: any, rawValue: string) => string | undefined);
onChange: undefined | ((event: React.ChangeEvent<HTMLInputElement>) => void);
onChangeValue: undefined | ((value: any, rawValue: string) => void);
validate?: (value: any, rawValue: string) => string | undefined;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onChangeValue?: (value: any, rawValue: string) => void;
}): {
value?: string;
defaultValue?: string;
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export {default as StackingGroup} from './stacking-group';
export {default as Form} from './form';
export {default as Select} from './select';
export {default as TextField} from './text-field';
export {default as PinField} from './pin-field';
export {TextFieldBase} from './text-field-base';
export {default as SearchField} from './search-field';
export {default as EmailField} from './email-field';
Expand Down
Loading

0 comments on commit 484dbc7

Please sign in to comment.