From 632fad37afbca2bf4b08535758e092fab7e3e2d3 Mon Sep 17 00:00:00 2001 From: Andrey Morozov Date: Mon, 24 Jun 2024 15:24:41 +0300 Subject: [PATCH 1/2] feat(PinInput): implement component API --- src/components/PinInput/PinInput.tsx | 34 ++++++++++++++++--- src/components/PinInput/README.md | 6 ++++ .../PinInput/__tests__/PinInput.test.tsx | 33 ++++++++++++++++-- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/components/PinInput/PinInput.tsx b/src/components/PinInput/PinInput.tsx index 3fe0fee872..c3386c9744 100644 --- a/src/components/PinInput/PinInput.tsx +++ b/src/components/PinInput/PinInput.tsx @@ -8,7 +8,7 @@ import type {TextInputProps, TextInputSize} from '../controls'; import {TextInput} from '../controls'; import {OuterAdditionalContent} from '../controls/common/OuterAdditionalContent/OuterAdditionalContent'; import {useDirection} from '../theme'; -import type {DOMProps, QAProps} from '../types'; +import type {AriaLabelingProps, DOMProps, QAProps} from '../types'; import {block} from '../utils/cn'; import './PinInput.scss'; @@ -16,7 +16,12 @@ import './PinInput.scss'; export type PinInputSize = TextInputSize; export type PinInputType = 'numeric' | 'alphanumeric'; -export interface PinInputProps extends DOMProps, QAProps { +export interface PinInputApi { + focus: () => void; + blur: () => void; +} + +export interface PinInputProps extends DOMProps, AriaLabelingProps, QAProps { value?: string[]; defaultValue?: string[]; onUpdate?: (value: string[]) => void; @@ -34,9 +39,7 @@ export interface PinInputProps extends DOMProps, QAProps { note?: TextInputProps['note']; validationState?: TextInputProps['validationState']; errorMessage?: TextInputProps['errorMessage']; - 'aria-label'?: string; - 'aria-labelledby'?: string; - 'aria-describedby'?: string; + apiRef?: React.RefObject; } const b = block('pin-input'); @@ -70,6 +73,7 @@ export const PinInput = React.forwardRef((props, note, validationState, errorMessage, + apiRef, className, style, qa, @@ -215,6 +219,7 @@ export const PinInput = React.forwardRef((props, const handleFocus = (index: number) => { setFocusedIndex(index); + setActiveIndex(index); }; const handleBlur = () => { @@ -229,6 +234,24 @@ export const PinInput = React.forwardRef((props, // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + React.useImperativeHandle( + apiRef, + () => ({ + focus: () => { + refs.current[activeIndex]?.focus(); + }, + blur: () => { + if ( + document.activeElement && + document.activeElement === refs.current[activeIndex] + ) { + refs.current[activeIndex]?.blur(); + } + }, + }), + [activeIndex], + ); + return (
@@ -253,6 +276,7 @@ export const PinInput = React.forwardRef((props, 'aria-label': props['aria-label'], 'aria-labelledby': props['aria-labelledby'], 'aria-describedby': ariaDescribedBy, + 'aria-details': props['aria-details'], 'aria-invalid': validationState === 'invalid' ? true : undefined, }} controlRef={handleRef.bind(null, i)} diff --git a/src/components/PinInput/README.md b/src/components/PinInput/README.md index 01e68242f6..868926746a 100644 --- a/src/components/PinInput/README.md +++ b/src/components/PinInput/README.md @@ -157,10 +157,16 @@ LANDING_BLOCK--> If you want the browser to suggest "one time codes" from the outer context (e.g. SMS) set the `otp` prop. +## API + +- `focus(): void` - Set focus to the current active input. +- `blur(): void` - Remove focus from the current active input. + ## Properties | Name | Description | Type | Default | | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :---------: | +| apiRef | Ref to the [API](#api) | `React.RefObject` | | | aria-describedby | HTML `aria-describedby` attribute | `string` | | | aria-label | HTML `aria-label` attribute | `string` | | | aria-labelledby | HTML `aria-labelledby` attribute | `string` | | diff --git a/src/components/PinInput/__tests__/PinInput.test.tsx b/src/components/PinInput/__tests__/PinInput.test.tsx index c0cc5ebcb7..1661b4b92e 100644 --- a/src/components/PinInput/__tests__/PinInput.test.tsx +++ b/src/components/PinInput/__tests__/PinInput.test.tsx @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'; import {act, fireEvent, render, screen} from '../../../../test-utils/utils'; import {PinInput} from '../PinInput'; +import type {PinInputApi} from '../PinInput'; describe('PinInput', () => { let inputs: HTMLElement[]; @@ -158,8 +159,6 @@ describe('PinInput', () => { expect(inputs[0]).toHaveValue('x'); }); - xtest('replace current input value on change', () => {}); - test('typing via keyboard', async () => { const user = userEvent.setup(); const onUpdate = jest.fn(); @@ -271,4 +270,34 @@ describe('PinInput', () => { await user.keyboard('{ArrowUp}'); expect(inputs[0]).toHaveFocus(); }); + + describe('API', () => { + test('focus', async () => { + const user = userEvent.setup(); + const apiRef: React.RefObject = {current: null}; + renderComponent(); + + await user.click(inputs[1]); + expect(inputs[1]).toHaveFocus(); + await user.tab(); + expect(inputs[1]).not.toHaveFocus(); + act(() => { + apiRef.current?.focus(); + }); + expect(inputs[1]).toHaveFocus(); + }); + + test('blur', async () => { + const user = userEvent.setup(); + const apiRef: React.RefObject = {current: null}; + renderComponent(); + + await user.click(inputs[1]); + expect(inputs[1]).toHaveFocus(); + act(() => { + apiRef.current?.blur(); + }); + expect(inputs[1]).not.toHaveFocus(); + }); + }); }); From 6bd17f257e21704698e9f559556c5af13c747185 Mon Sep 17 00:00:00 2001 From: Andrey Morozov Date: Tue, 25 Jun 2024 21:44:51 +0300 Subject: [PATCH 2/2] chore: remove blur method --- src/components/PinInput/PinInput.tsx | 9 --------- src/components/PinInput/README.md | 1 - src/components/PinInput/__tests__/PinInput.test.tsx | 13 ------------- 3 files changed, 23 deletions(-) diff --git a/src/components/PinInput/PinInput.tsx b/src/components/PinInput/PinInput.tsx index c3386c9744..a73ca16578 100644 --- a/src/components/PinInput/PinInput.tsx +++ b/src/components/PinInput/PinInput.tsx @@ -18,7 +18,6 @@ export type PinInputType = 'numeric' | 'alphanumeric'; export interface PinInputApi { focus: () => void; - blur: () => void; } export interface PinInputProps extends DOMProps, AriaLabelingProps, QAProps { @@ -240,14 +239,6 @@ export const PinInput = React.forwardRef((props, focus: () => { refs.current[activeIndex]?.focus(); }, - blur: () => { - if ( - document.activeElement && - document.activeElement === refs.current[activeIndex] - ) { - refs.current[activeIndex]?.blur(); - } - }, }), [activeIndex], ); diff --git a/src/components/PinInput/README.md b/src/components/PinInput/README.md index 868926746a..5f4876929b 100644 --- a/src/components/PinInput/README.md +++ b/src/components/PinInput/README.md @@ -160,7 +160,6 @@ If you want the browser to suggest "one time codes" from the outer context (e.g. ## API - `focus(): void` - Set focus to the current active input. -- `blur(): void` - Remove focus from the current active input. ## Properties diff --git a/src/components/PinInput/__tests__/PinInput.test.tsx b/src/components/PinInput/__tests__/PinInput.test.tsx index 1661b4b92e..6e92c2a56a 100644 --- a/src/components/PinInput/__tests__/PinInput.test.tsx +++ b/src/components/PinInput/__tests__/PinInput.test.tsx @@ -286,18 +286,5 @@ describe('PinInput', () => { }); expect(inputs[1]).toHaveFocus(); }); - - test('blur', async () => { - const user = userEvent.setup(); - const apiRef: React.RefObject = {current: null}; - renderComponent(); - - await user.click(inputs[1]); - expect(inputs[1]).toHaveFocus(); - act(() => { - apiRef.current?.blur(); - }); - expect(inputs[1]).not.toHaveFocus(); - }); }); });