/
kaws
/
ui-kit-ce
Обзор
Документация
Войти
/
kaws
/
ui-kit-ce
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
packages/input-password/src/InputPassword.tsx
205 строк
6 KB
Урядышев Павел Александрович
chore(input-password): исправление поведения фокуса в Safari
28 авг 2025, 19:40
28 авг 2025, 19:40
7aa0b94
Код
Авторство
О чём код?
'use client' import * as React from 'react' import { Input, InputProps } from '@v-uik/input' import { useToggle, useButtonReset, useMergedRefs } from '@v-uik/hooks' import { HideIcon, ShowIcon } from './Icons' import { clsx, createUseStyles } from '@v-uik/theme' import { ButtonProps } from '@v-uik/button' import { ComponentPropsWithAttributes } from '@v-uik/common' const useStyles = createUseStyles((theme) => ({ icon: { cursor: 'pointer', display: 'flex', '&:focus-visible': { boxShadow: `0 0 0 ${theme.comp.input.widthShadowFocus}px ${theme.comp.input.colorShadowFocus}`, }, }, password: { fontFamily: theme.comp.inputPassword.typographyFontFamilyHidden, fontWeight: theme.comp.inputPassword.typographyFontWeightHidden, fontSize: theme.comp.inputPassword.typographyFontSizeHidden, lineHeight: theme.comp.inputPassword.typographyLineHeightHidden, letterSpacing: theme.comp.inputPassword.typographyLetterSpacingHidden, }, })) type Visibility = boolean type IconPosition = 'start' | 'end' export interface InputPasswordProps<TCanClear extends boolean | unknown = false> extends Omit<InputProps<TCanClear>, 'suffix' | 'prefix'> { /** * Скрыть/показать текст в input'e. * * @inner * * @default false */ visible?: Visibility /** * Обработчик нажатия по иконке видимости. * * @returns */ onVisibilityChange?: (value: Visibility) => void /** * Местоположение иконки: слева или справа. * * @inner * * @default "end" */ iconPosition?: IconPosition /** * Иконка, отображаема при скрытом инпуте. * * @inner */ hideIcon?: React.ReactNode /** * Иконка, отображаема при НЕ скрытом инпуте. * * @inner */ showIcon?: React.ReactNode /** * Пропсы для кнопки с иконкой. * * @inner */ buttonIconProps?: ComponentPropsWithAttributes< Omit<ButtonProps, 'className' | 'onMouseDown' | 'onClick'> > //TODO: 2.0 ButtonProps оставлены для обратной совместимости, должны быть HTMLProps Button } const _InputPassword = React.forwardRef( <TCanClear extends boolean = false>( { visible = false, onVisibilityChange, iconPosition: iconPositionProp = 'end', hideIcon: hideIconProp, showIcon: showIconProp, buttonIconProps = {}, ...rest }: InputPasswordProps<TCanClear>, ref: React.ForwardedRef<HTMLDivElement> ) => { const isFocused = React.useRef(false) const classesList = useStyles() const iconPosition = iconPositionProp const showIcon = showIconProp || ShowIcon const hideIcon = hideIconProp || HideIcon const buttonResetClasses = useButtonReset() const inputPasswordRef = React.useRef<HTMLInputElement>(null) const selectionRef = React.useRef({ start: 0, end: 0, }) const mergedInputRef = useMergedRefs([ inputPasswordRef, rest.inputProps?.ref ?? null, ]) const [toggled, toggle] = useToggle(visible) const toggleVisibility = () => { toggle() onVisibilityChange?.(toggled) } const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => { e.preventDefault() const inputElement = inputPasswordRef.current if (inputElement) { selectionRef.current = { start: inputElement.selectionStart ?? 0, end: inputElement.selectionEnd ?? 0, } } } const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => { isFocused.current = true rest.inputProps?.onFocus?.(event) } const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => { isFocused.current = false rest.inputProps?.onBlur?.(event) } React.useEffect(() => { const inputElement = inputPasswordRef.current const { start, end } = selectionRef.current let animationFrame: number // Небольшой костыль с проверкой фокуса, так как setSelectionRange в Safari вешает // состояние focus на поле. if (inputElement && isFocused.current) { animationFrame = requestAnimationFrame(() => { inputElement.setSelectionRange(start, end) }) } return () => cancelAnimationFrame(animationFrame) }, [toggled]) /* Приходится оборачивать в спан, тк нужно навесить курсор и клик Тип ReactNode включает в себя еще и строки и числа, а на них Не получится навесить ни стили, ни события */ const Icon = ( // onMouseDown исправляет потерю фокуса с input <button type="button" className={clsx(buttonResetClasses.resetButton, classesList.icon)} onClick={toggleVisibility} onMouseDown={handleMouseDown} {...buttonIconProps} > {toggled ? showIcon : hideIcon} </button> ) const classes = { ...rest.classes, input: clsx(rest.classes?.input, { [classesList.password]: !toggled, }), } return ( <Input {...rest} ref={ref} suffix={iconPosition === 'end' && Icon} prefix={iconPosition === 'start' && Icon} classes={classes} inputProps={{ ...rest.inputProps, onFocus: handleFocus, onBlur: handleBlur, type: toggled ? 'text' : 'password', ref: mergedInputRef, }} /> ) } ) export const InputPassword = _InputPassword as < TCanClear extends boolean | unknown = false >( props: InputPasswordProps<TCanClear> & { /** * */ ref?: React.ForwardedRef<HTMLDivElement> } ) => React.ReactElement