Skip to content

Commit

Permalink
feat(PinInput): implement component API (#1674)
Browse files Browse the repository at this point in the history
  • Loading branch information
amje authored Jun 26, 2024
1 parent b2a19a6 commit 72cf90f
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 7 deletions.
25 changes: 20 additions & 5 deletions src/components/PinInput/PinInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ 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';

export type PinInputSize = TextInputSize;
export type PinInputType = 'numeric' | 'alphanumeric';

export interface PinInputProps extends DOMProps, QAProps {
export interface PinInputApi {
focus: () => void;
}

export interface PinInputProps extends DOMProps, AriaLabelingProps, QAProps {
value?: string[];
defaultValue?: string[];
onUpdate?: (value: string[]) => void;
Expand All @@ -34,9 +38,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<PinInputApi>;
}

const b = block('pin-input');
Expand Down Expand Up @@ -70,6 +72,7 @@ export const PinInput = React.forwardRef<HTMLDivElement, PinInputProps>((props,
note,
validationState,
errorMessage,
apiRef,
className,
style,
qa,
Expand Down Expand Up @@ -215,6 +218,7 @@ export const PinInput = React.forwardRef<HTMLDivElement, PinInputProps>((props,

const handleFocus = (index: number) => {
setFocusedIndex(index);
setActiveIndex(index);
};

const handleBlur = () => {
Expand All @@ -229,6 +233,16 @@ export const PinInput = React.forwardRef<HTMLDivElement, PinInputProps>((props,
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

React.useImperativeHandle(
apiRef,
() => ({
focus: () => {
refs.current[activeIndex]?.focus();
},
}),
[activeIndex],
);

return (
<div ref={ref} className={b({size}, className)} style={style} data-qa={qa}>
<div className={b('items')}>
Expand All @@ -253,6 +267,7 @@ export const PinInput = React.forwardRef<HTMLDivElement, PinInputProps>((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)}
Expand Down
5 changes: 5 additions & 0 deletions src/components/PinInput/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,15 @@ 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.

## 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` | |
Expand Down
20 changes: 18 additions & 2 deletions src/components/PinInput/__tests__/PinInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -271,4 +270,21 @@ describe('PinInput', () => {
await user.keyboard('{ArrowUp}');
expect(inputs[0]).toHaveFocus();
});

describe('API', () => {
test('focus', async () => {
const user = userEvent.setup();
const apiRef: React.RefObject<PinInputApi> = {current: null};
renderComponent(<PinInput apiRef={apiRef} defaultValue={['0', '1', '2', '3']} />);

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();
});
});
});

0 comments on commit 72cf90f

Please sign in to comment.