Skip to content

Commit 484dbc7

Browse files
atabeltuentisreyceballostsemantic-release-botmarcoskolodny
authored
feat(PinField): new component (#902)
* 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 <yceballost@users.noreply.github.com> Co-authored-by: Abel Toledano <atoledano@tuenti.com> * 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 <pedro.jose.ladaria.linden@telefonica.com> * 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 <lifecycle-github@telefonica.com> Co-authored-by: yceballost <yceballost@users.noreply.github.com> Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net> Co-authored-by: Marcos Kolodny <marcoskolodny@gmail.com> Co-authored-by: Pedro Ladaria <pladaria@tuenti.com> Co-authored-by: Pedro Ladaria <pedro.jose.ladaria.linden@telefonica.com>
1 parent 4e366d3 commit 484dbc7

17 files changed

+636
-30
lines changed

playroom/snippets.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,8 @@ const formSnippets: Array<Snippet> = [
274274
' </Stack>\n' +
275275
'</RadioGroup>',
276276
],
277+
['PinField', '<PinField name="otp" aria-label="OTP" />'],
278+
['PinField (hideCode)', '<PinField hideCode name="pin" aria-label="PIN" />'],
277279
[
278280
'Form',
279281
`<Form

src/__acceptance_tests__/form-fields-acceptance-test.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {openStoryPage, screen, waitFor} from '../test-utils';
2+
import {within} from '@telefonica/acceptance-testing';
23

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

@@ -201,3 +202,92 @@ test.each(STORY_TYPES)('SearchField (%s)', async (storyType) => {
201202
await page.click(await screen.findByLabelText('Borrar búsqueda'));
202203
expect(await getValue(field)).toBe('');
203204
});
205+
206+
test.each(STORY_TYPES)('PinField (%s)', async (storyType) => {
207+
await openStoryPage(getStoryOfType(storyType));
208+
209+
const fieldGroup = await screen.findByLabelText('OTP');
210+
const firstDigitField = await within(fieldGroup).findByLabelText('Dígito 1 de 6');
211+
await firstDigitField.type('123456');
212+
213+
await screen.findByText("onChange: (string) '123456'");
214+
await screen.findByText("onChangeValue: (string) '123456'");
215+
});
216+
217+
test.each(STORY_TYPES)('PinField (hideCode) (%s)', async (storyType) => {
218+
await openStoryPage(getStoryOfType(storyType));
219+
220+
const fieldGroup = await screen.findByLabelText('PIN');
221+
const firstDigitField = await within(fieldGroup).findByLabelText('Dígito 1 de 6');
222+
await firstDigitField.type('123456');
223+
224+
await screen.findByText("onChange: (string) '123456'");
225+
await screen.findByText("onChangeValue: (string) '123456'");
226+
});
227+
228+
test('PinField focus management', async () => {
229+
await openStoryPage(CONTROLLED_STORY);
230+
231+
const fieldGroup = await screen.findByLabelText('OTP');
232+
233+
const firstDigitField = await within(fieldGroup).findByLabelText('Dígito 1 de 6');
234+
const secondDigitField = await within(fieldGroup).findByLabelText('Dígito 2 de 6');
235+
const thirdDigitField = await within(fieldGroup).findByLabelText('Dígito 3 de 6');
236+
const forthDigitField = await within(fieldGroup).findByLabelText('Dígito 4 de 6');
237+
238+
// try to focus forth field, but the first one is focused instead
239+
await forthDigitField.focus();
240+
expect(await forthDigitField.evaluate((el) => el === document.activeElement)).toBe(false);
241+
expect(await firstDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
242+
243+
// focus is moved to second field after typing
244+
await firstDigitField.type('1');
245+
expect(await secondDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
246+
247+
await secondDigitField.evaluate((el) => (el as HTMLInputElement).blur());
248+
expect(await secondDigitField.evaluate((el) => el === document.activeElement)).toBe(false);
249+
250+
// try to focus forth field, but the second one is focused instead
251+
await forthDigitField.focus();
252+
expect(await secondDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
253+
254+
// focus is moved to third field after typing
255+
await secondDigitField.type('2');
256+
expect(await thirdDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
257+
258+
// move to previous field with left arrow
259+
await thirdDigitField.press('ArrowLeft');
260+
expect(await secondDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
261+
await secondDigitField.press('ArrowLeft');
262+
expect(await firstDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
263+
264+
// type a number to overwrite the first field value
265+
await firstDigitField.type('9');
266+
await screen.findByText("onChange: (string) '92'");
267+
expect(await secondDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
268+
269+
// move to next field with right arrow
270+
await secondDigitField.press('ArrowRight');
271+
expect(await thirdDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
272+
273+
// type a new number
274+
await thirdDigitField.type('3');
275+
await screen.findByText("onChange: (string) '923'");
276+
expect(await forthDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
277+
278+
// go back with Backspace
279+
await forthDigitField.press('Backspace');
280+
await screen.findByText("onChange: (string) '923'");
281+
expect(await thirdDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
282+
283+
// delete with Backspace
284+
await thirdDigitField.press('Backspace');
285+
await screen.findByText("onChange: (string) '92'");
286+
expect(await secondDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
287+
288+
// move left with left arrow and delete with Delete key
289+
await secondDigitField.press('ArrowLeft');
290+
expect(await firstDigitField.evaluate((el) => el === document.activeElement)).toBe(true);
291+
await firstDigitField.press('Delete');
292+
await screen.findByText("onChange: (string) '2'");
293+
}, 1200000);

src/__screenshot_tests__/form-fields-screenshot-test.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {openStoryPage, screen} from '../test-utils';
2+
import {within} from '@telefonica/acceptance-testing';
23

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

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

220221
expect(await fieldWrapper.screenshot()).toMatchImageSnapshot();
221222
});
223+
224+
test('PinField', async () => {
225+
await openStoryPage({
226+
id: 'components-input-fields--types-uncontrolled',
227+
device: 'MOBILE_IOS',
228+
});
229+
230+
const fieldGroup = await screen.findByLabelText('OTP');
231+
expect(await fieldGroup.screenshot()).toMatchImageSnapshot();
232+
233+
const firstDigitField = await within(fieldGroup).findByLabelText('Dígito 1 de 6');
234+
await firstDigitField.focus();
235+
expect(await fieldGroup.screenshot()).toMatchImageSnapshot();
236+
237+
await firstDigitField.type('1');
238+
expect(await fieldGroup.screenshot()).toMatchImageSnapshot();
239+
});
240+
241+
test('PinField (hideCode)', async () => {
242+
await openStoryPage({
243+
id: 'components-input-fields--types-uncontrolled',
244+
device: 'MOBILE_IOS',
245+
});
246+
247+
const fieldGroup = await screen.findByLabelText('PIN');
248+
249+
const firstDigitField = await within(fieldGroup).findByLabelText('Dígito 1 de 6');
250+
await firstDigitField.type('1');
251+
expect(await fieldGroup.screenshot()).toMatchImageSnapshot();
252+
});

src/__stories__/field-story.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
Form,
2323
Title1,
2424
Stack,
25+
PinField,
2526
} from '..';
2627
import {inspect} from 'util';
2728
import IconMusicRegular from '../generated/mistica-icons/icon-music-regular';
@@ -461,6 +462,31 @@ export const TypesUncontrolled: StoryComponent = () => (
461462
/>
462463
)}
463464
</Uncontrolled>
465+
466+
<Uncontrolled title="PinField">
467+
{(handleChange, handleChangeValue) => (
468+
<PinField
469+
name="otp"
470+
aria-label="OTP"
471+
defaultValue=""
472+
onChange={handleChange}
473+
onChangeValue={handleChangeValue}
474+
/>
475+
)}
476+
</Uncontrolled>
477+
478+
<Uncontrolled title="PinField (hideCode)">
479+
{(handleChange, handleChangeValue) => (
480+
<PinField
481+
hideCode
482+
name="pin"
483+
aria-label="PIN"
484+
defaultValue=""
485+
onChange={handleChange}
486+
onChangeValue={handleChangeValue}
487+
/>
488+
)}
489+
</Uncontrolled>
464490
</>
465491
);
466492

@@ -708,6 +734,31 @@ export const TypesControlled = (): React.ReactNode => (
708734
</div>
709735
)}
710736
</Controlled>
737+
738+
<Controlled title="PinField" initialValue="">
739+
{(handleChange, handleChangeValue, value) => (
740+
<PinField
741+
name="otp"
742+
aria-label="OTP"
743+
onChange={handleChange}
744+
onChangeValue={handleChangeValue}
745+
value={value}
746+
/>
747+
)}
748+
</Controlled>
749+
750+
<Controlled title="PinField (hideCode)" initialValue="">
751+
{(handleChange, handleChangeValue, value) => (
752+
<PinField
753+
hideCode
754+
name="pin"
755+
aria-label="PIN"
756+
onChange={handleChange}
757+
onChangeValue={handleChangeValue}
758+
value={value}
759+
/>
760+
)}
761+
</Controlled>
711762
</>
712763
);
713764

src/__tests__/sheet-test.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ test('Sheet', async () => {
4545

4646
await userEvent.click(closeButton);
4747
await waitForElementToBeRemoved(sheet);
48-
});
48+
}, 20000);
4949

5050
test('RadioListSheet', async () => {
5151
const selectSpy = jest.fn();
@@ -95,7 +95,7 @@ test('RadioListSheet', async () => {
9595

9696
await waitForElementToBeRemoved(sheet);
9797
expect(selectSpy).toHaveBeenCalledWith('1');
98-
}, 15000);
98+
}, 20000);
9999

100100
test('ActionsListSheet', async () => {
101101
const selectSpy = jest.fn();
@@ -144,7 +144,7 @@ test('ActionsListSheet', async () => {
144144

145145
await waitForElementToBeRemoved(sheet);
146146
expect(selectSpy).toHaveBeenCalledWith('1');
147-
});
147+
}, 20000);
148148

149149
test('InfoSheet', async () => {
150150
const TestComponent = () => {
@@ -193,7 +193,7 @@ test('InfoSheet', async () => {
193193

194194
const items = await within(itemList).findAllByRole('listitem');
195195
expect(items).toHaveLength(2);
196-
});
196+
}, 20000);
197197

198198
test('ActionsSheet', async () => {
199199
const onPressButtonSpy = jest.fn();
@@ -249,7 +249,7 @@ test('ActionsSheet', async () => {
249249

250250
await waitForElementToBeRemoved(sheet);
251251
expect(onPressButtonSpy).toHaveBeenCalledWith('SECONDARY');
252-
});
252+
}, 20000);
253253

254254
test('showSheet INFO', async () => {
255255
const resultSpy = jest.fn();
@@ -277,7 +277,7 @@ test('showSheet INFO', async () => {
277277

278278
await waitForElementToBeRemoved(sheet);
279279
expect(resultSpy).toHaveBeenCalledWith(undefined);
280-
});
280+
}, 20000);
281281

282282
test('showSheet ACTIONS_LIST', async () => {
283283
const resultSpy = jest.fn();
@@ -309,7 +309,7 @@ test('showSheet ACTIONS_LIST', async () => {
309309

310310
await waitForElementToBeRemoved(sheet);
311311
expect(resultSpy).toHaveBeenCalledWith({action: 'SUBMIT', selectedId: '2'});
312-
});
312+
}, 20000);
313313

314314
test('showSheet ACTIONS_LIST dismiss', async () => {
315315
const resultSpy = jest.fn();
@@ -340,7 +340,7 @@ test('showSheet ACTIONS_LIST dismiss', async () => {
340340

341341
await waitForElementToBeRemoved(sheet);
342342
expect(resultSpy).toHaveBeenCalledWith({action: 'DISMISS'});
343-
});
343+
}, 20000);
344344

345345
test('showSheet RADIO_LIST', async () => {
346346
const resultSpy = jest.fn();
@@ -374,7 +374,7 @@ test('showSheet RADIO_LIST', async () => {
374374

375375
await waitForElementToBeRemoved(sheet);
376376
expect(resultSpy).toHaveBeenCalledWith({action: 'SUBMIT', selectedId: '2'});
377-
});
377+
}, 20000);
378378

379379
test('showSheet RADIO_LIST dismiss', async () => {
380380
const resultSpy = jest.fn();
@@ -405,7 +405,7 @@ test('showSheet RADIO_LIST dismiss', async () => {
405405

406406
await waitForElementToBeRemoved(sheet);
407407
expect(resultSpy).toHaveBeenCalledWith({action: 'DISMISS'});
408-
});
408+
}, 20000);
409409

410410
test('showSheet ACTIONS', async () => {
411411
const resultSpy = jest.fn();
@@ -444,7 +444,7 @@ test('showSheet ACTIONS', async () => {
444444

445445
await waitForElementToBeRemoved(sheet);
446446
expect(resultSpy).toHaveBeenCalledWith({action: 'LINK'});
447-
});
447+
}, 20000);
448448

449449
test('showSheet ACTIONS dismiss', async () => {
450450
const resultSpy = jest.fn();
@@ -476,7 +476,7 @@ test('showSheet ACTIONS dismiss', async () => {
476476

477477
await waitForElementToBeRemoved(sheet);
478478
expect(resultSpy).toHaveBeenCalledWith({action: 'DISMISS'});
479-
});
479+
}, 20000);
480480

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

744744
await waitForElementToBeRemoved(sheet);
745745
expect(resultSpy).toHaveBeenCalledWith({action: 'LINK'});
746-
});
746+
}, 20000);

src/form-context.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,17 @@ export const useFieldProps = ({
107107
onChangeValue,
108108
}: {
109109
name: string;
110-
value: string | undefined;
111-
defaultValue: string | undefined;
110+
value?: string;
111+
defaultValue?: string;
112112
processValue: (value: string) => unknown;
113-
helperText: string | undefined;
114-
optional: boolean | undefined;
115-
error: boolean | undefined;
116-
disabled: boolean | undefined;
113+
helperText?: string;
114+
optional?: boolean;
115+
error?: boolean;
116+
disabled?: boolean;
117117
onBlur?: React.FocusEventHandler;
118-
validate: undefined | ((value: any, rawValue: string) => string | undefined);
119-
onChange: undefined | ((event: React.ChangeEvent<HTMLInputElement>) => void);
120-
onChangeValue: undefined | ((value: any, rawValue: string) => void);
118+
validate?: (value: any, rawValue: string) => string | undefined;
119+
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
120+
onChangeValue?: (value: any, rawValue: string) => void;
121121
}): {
122122
value?: string;
123123
defaultValue?: string;

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export {default as StackingGroup} from './stacking-group';
123123
export {default as Form} from './form';
124124
export {default as Select} from './select';
125125
export {default as TextField} from './text-field';
126+
export {default as PinField} from './pin-field';
126127
export {TextFieldBase} from './text-field-base';
127128
export {default as SearchField} from './search-field';
128129
export {default as EmailField} from './email-field';

0 commit comments

Comments
 (0)