Skip to content

feat(code-input): добавлено поведение фокуса на первый инпут при клике на любое пустое поле [DS-12539]#1900

Draft
dHIM24 wants to merge 10 commits intomasterfrom
DS-12539
Draft

feat(code-input): добавлено поведение фокуса на первый инпут при клике на любое пустое поле [DS-12539]#1900
dHIM24 wants to merge 10 commits intomasterfrom
DS-12539

Conversation

@dHIM24
Copy link
Contributor

@dHIM24 dHIM24 commented Sep 25, 2025

BaseCodeInput
  • Добавлен проп strictFocus для включения последовательного ввода:

    • при клике на ячейку правее первой пустой — фокус остается на первой
    • фокус разрешается только на уже заполненные ячейки и первую пустую ячейку
  • Добавлено поведение автоматического фокуса на первый инпут при клике на любое пустое поле

Confirmation
  • Добавлена поддержка пропа strictFocus для использования в CodeInput

Чек лист

  • Задача сформулирована и описана в JIRA
  • В названии ветки есть айдишник задачи в JIRA (fix/DS-1234), ссылку прикреплять не надо
  • У реквеста осмысленное название feat(...) или fix(...) по conventional commits (https://www.conventionalcommits.org)
  • Код покрыт тестами и протестирован в различных браузерах
  • Добавленные пропсы добавлены в демки и описаны в документации
  • К реквесту добавлен changeset

Если есть визуальные изменения

  • Прикреплено изображение было/стало

Тест кейсы:

Невозможно переключиться с помощью мыши и клавиатуры на следующий поле ввода.
Протестировано, функционал Android WebOTP Api работает корректно в компонента Confirmation и CodeInput

  1. https://github.com/user-attachments/assets/8f2ad147-1222-4310-a52c-ffca9db7f0d0
  2. https://github.com/user-attachments/assets/392523a0-36e7-4179-92d7-53f5cb133a3c

Код из песочницы:

restrictFocus: boolean

CodeInput

<Container align = {isMobile() ? 'center' : 'left'}>
    <CodeInput fields={isMobile() ? 4 : 5} breakpoint={BREAKPOINT} strictFocus/>
</Container>

Confirmation

render(() => {
    const isMobileFrame = document.body.clientWidth < 450;
    const variants = [
        { key: 'success', content: 'Корректный код' },
        { key: 'error', content: 'Некорректный код' },
        { key: 'expired', content: 'Код устарел' },
        { key: 'fatal', content: 'Закончились попытки ввода кода, вводимый код некорректен' },
        {
            key: 'sms-requests-ended',
            content: 'Закончились попытки запроса кода, вводимый код устарел',
        },
        { key: 'temp-block-over', content: 'Сценарий, когда форма временно заблокирована' },
    ];

    const [variant, setVariant] = React.useState(variants[0]);
    const [shownSuccessScreen, setShownSuccessScreen] = React.useState(false);

    const {
        confirmationState,
        confirmationScreen,
        confirmationBlockSmsRetry,
        setConfirmationState,
        setConfirmationScreen,
        setConfirmationBlockSmsRetry,
    } = useConfirmation();

    const handleInputFinished = () => {
        setTimeout(() => {
            switch (variant.key) {
                case 'success':
                    setShownSuccessScreen(true);
                    setConfirmationState('INITIAL');
                    break;
                case 'error':
                    setConfirmationState('CODE_ERROR');
                    break;
                case 'expired':
                    setConfirmationState('CODE_EXPIRED');
                    break;
                case 'fatal':
                    setConfirmationScreen('FATAL_ERROR');
                    break;
                case 'sms-requests-ended':
                    setConfirmationState('CODE_EXPIRED_ENDED');
                    break;
                case 'temp-block-over':
                    setConfirmationScreen('TEMP_BLOCK');
                    break;
                default:
                    break;
            }
        }, 1000);
    };

    const durations = {
        'temp-block': 24 * 60 * 60 * 1000,
        'temp-block-over': 10000,
    };

    const currentTempBlockDuration = durations[variant.key] || 10000;

    const handleSmsRetryClick = () => {
        setTimeout(() => {
            if (variant.key === 'sms-requests-ended') {
                setConfirmationBlockSmsRetry(true);
            }
            setConfirmationState('INITIAL');
        }, 1000);
    };

    const handleTempBlockFinished = () => {
        if (variant.key === 'temp-block-over') {
            setConfirmationScreen('TEMP_BLOCK_OVER');
        } else {
            setConfirmationScreen('INITIAL');
            setConfirmationState('CODE_SENDING');
        }
    };

    const Component = isMobileFrame ? ConfirmationMobile : Confirmation;

    return (
        <div style={{ margin: '0 auto', width: isMobileFrame ? '100%' : 388 }}>
            <SelectDesktop
                block={true}
                options={variants}
                onChange={({ selected }) => {
                    setShownSuccessScreen(false);
                    setConfirmationState('INITIAL');
                    setConfirmationScreen('INITIAL');
                    setConfirmationBlockSmsRetry(false);
                    setVariant(selected);
                }}
                selected={variant.key}
                Option={BaseOption}
                optionsListWidth='field'
            />
            <div
                key={variant.key}
                style={{
                    margin: '16px 0 0',
                    padding: '16px',
                    boxShadow: '0 0 0 1px #eeeff1',
                    boxSizing: 'border-box',
                }}
            >
                {shownSuccessScreen ? (
                    <div style={{ display: 'flex', flexFlow: 'column nowrap', height: 266 }}>
                        <Gap size='2xl' />
                        <div
                            style={{
                                display: 'flex',
                                flexFlow: 'column nowrap',
                                alignItems: 'center',
                                flex: 1,
                            }}
                        >
                            <div style={{ textAlign: 'center' }}>
                                <SuperEllipse
                                    size={80}
                                    backgroundColor='var(--color-light-status-positive)'
                                >
                                    <CheckmarkMIcon style={{ fill: '#fff' }} />
                                </SuperEllipse>

                                <Gap size='m' />

                                <Typography.Text view='primary-medium' weight='bold'>
                                    Введён корректный код
                                </Typography.Text>
                            </div>

                            <Gap size={isMobileFrame ? '2xl' : '4xl'} />

                            <Button onClick={() => setShownSuccessScreen(false)} size='xs'>
                                Попробовать ещё раз
                            </Button>
                        </div>
                    </div>
                ) : (
                    <Component
                        screen={confirmationScreen}
                        state={confirmationState}
                        alignContent='center'
                        blockSmsRetry={confirmationBlockSmsRetry}
                        countdownDuration={10000}
                        tempBlockDuration={currentTempBlockDuration}
                        onChangeState={setConfirmationState}
                        onChangeScreen={setConfirmationScreen}
                        onInputFinished={handleInputFinished}
                        onSmsRetryClick={handleSmsRetryClick}
                        onTempBlockFinished={handleTempBlockFinished}
                        phone='+7 ··· ··· 07 24'
                        strictFocus
                    />
                )}
            </div>
        </div>
    );
});

@changeset-bot
Copy link

changeset-bot bot commented Sep 25, 2025

🦋 Changeset detected

Latest commit: 3fa4fbe

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@alfalab/core-components Minor
@alfalab/core-components-code-input Minor
@alfalab/core-components-confirmation Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coveralls
Copy link

coveralls commented Sep 25, 2025

Pull Request Test Coverage Report for Build 21025986734

Details

  • 47 of 53 (88.68%) changed or added relevant lines in 3 files are covered.
  • 1 unchanged line in 1 file lost coverage.
  • Overall coverage increased (+0.1%) to 79.971%

Changes Missing Coverage Covered Lines Changed/Added Lines %
packages/code-input/src/components/base-code-input/component.tsx 39 45 86.67%
Files with Coverage Reduction New Missed Lines %
packages/code-input/src/components/base-code-input/component.tsx 1 76.1%
Totals Coverage Status
Change from base Build 20998327799: 0.1%
Covered Lines: 9725
Relevant Lines: 11399

💛 - Coveralls

@github-actions
Copy link
Contributor

Demo build

https://core-ds.github.io/core-components/1900

@dHIM24
Copy link
Contributor Author

dHIM24 commented Sep 29, 2025

автоподстановка сломалась с текущей реализацией, обдумываю другое решение

update: fix выкачен, видео обновлено в заголовке задачи

video_2025-09-29_11-21-39.mp4

@dHIM24 dHIM24 marked this pull request as ready for review October 1, 2025 08:13
@dHIM24 dHIM24 requested a review from Oladii October 1, 2025 08:54
@dHIM24 dHIM24 requested review from Oladii and hextion October 3, 2025 09:39
@Oladii Oladii requested review from Oladii and removed request for Oladii October 30, 2025 12:54
@dHIM24 dHIM24 marked this pull request as draft October 31, 2025 07:42
@dHIM24 dHIM24 marked this pull request as ready for review November 24, 2025 21:48
@dHIM24
Copy link
Contributor Author

dHIM24 commented Nov 25, 2025

strictFocus === true, последовательный ввод

2025-11-24.21.18.40.mov

strictFocus === false

2025-11-24.21.20.28.mov

@dHIM24
Copy link
Contributor Author

dHIM24 commented Nov 25, 2025

upd: strictFocus === true - позволяем удалять введенные значения.

2025-11-25.18.48.35.mov

@fulcanellee fulcanellee self-requested a review November 28, 2025 09:44
case 'ArrowRight':
event.preventDefault();

if (restrictFocus) {
Copy link
Contributor

@fulcanellee fulcanellee Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Опционально - рассмотрел бы возможность изолировать логику связанную с новым пропом в отдельный хук.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

хорошая идея, будет читабельнее, сделаю вместе с beforeEach выше

@dHIM24 dHIM24 force-pushed the DS-12539 branch 3 times, most recently from 1b4c0ce to e4ff5f2 Compare January 15, 2026 08:56
@dHIM24
Copy link
Contributor Author

dHIM24 commented Jan 15, 2026

Договорились, что приложу артефакт со скринридера по доступности компонента.

Видео будет под тредом.

@dHIM24 dHIM24 added the paused label Feb 9, 2026
@hextion hextion removed the paused label Feb 10, 2026
@hextion hextion marked this pull request as draft February 10, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants