import { tlenv, tltime, useMaybeEditor } from '@tldraw/editor' import classNames from 'classnames' import * as React from 'react' import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey' import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { TLUiIconType } from '../../icon-types' import { TldrawUiIcon } from './TldrawUiIcon' /** @public */ export interface TLUiInputProps { disabled?: boolean label?: TLUiTranslationKey | Exclude icon?: TLUiIconType | Exclude iconLeft?: TLUiIconType | Exclude iconLabel?: TLUiTranslationKey | Exclude autoFocus?: boolean autoSelect?: boolean children?: React.ReactNode defaultValue?: string placeholder?: string onComplete?(value: string): void onValueChange?(value: string): void onCancel?(value: string): void onBlur?(value: string): void onFocus?(): void className?: string /** * Usually on iOS when you focus an input, the browser will adjust the viewport to bring the input * into view. Sometimes this doesn't work properly though - for example, if the input is newly * created, iOS seems to have a hard time adjusting the viewport for it. This prop allows you to * opt-in to some extra code to manually bring the input into view when the visual viewport of the * browser changes, but we don't want to use it everywhere because generally the native behavior * looks nicer in scenarios where it's sufficient. */ shouldManuallyMaintainScrollPositionWhenFocused?: boolean value?: string 'data-testid'?: string 'aria-label'?: string } /** @public @react */ export const TldrawUiInput = React.forwardRef( function TldrawUiInput( { className, label, icon, iconLeft, iconLabel, autoSelect = false, autoFocus = false, defaultValue, placeholder, onComplete, onValueChange, onCancel, onFocus, onBlur, shouldManuallyMaintainScrollPositionWhenFocused = false, children, value, 'data-testid': dataTestId, disabled, 'aria-label': ariaLabel, }, ref ) { const editor = useMaybeEditor() const rInputRef = React.useRef(null) // combine rInputRef and ref React.useImperativeHandle(ref, () => rInputRef.current as HTMLInputElement) const msg = useTranslation() const rInitialValue = React.useRef(defaultValue ?? '') const rCurrentValue = React.useRef(defaultValue ?? '') const isComposing = React.useRef(false) const [isFocused, setIsFocused] = React.useState(false) const handleFocus = React.useCallback( (e: React.FocusEvent) => { setIsFocused(true) const elm = e.currentTarget as HTMLInputElement rCurrentValue.current = elm.value if (editor) { editor.timers.requestAnimationFrame(() => { if (autoSelect) { elm.select() } }) } else { tltime.requestAnimationFrame('anon', () => { if (autoSelect) { elm.select() } }) } onFocus?.() }, [autoSelect, editor, onFocus] ) const handleChange = React.useCallback( (e: React.ChangeEvent) => { const value = e.currentTarget.value rCurrentValue.current = value onValueChange?.(value) }, [onValueChange] ) // We use keydown capture, because we want to get the escape key event. const handleKeyDownCapture = React.useCallback( (e: React.KeyboardEvent) => { switch (e.key) { case 'Enter': { // In Chrome, if the user presses the Enter key while using the IME and calls // `e.currentTarget.blur()` in the event callback here, it will trigger an // `onChange` with a duplicated text value. if (isComposing.current) return e.currentTarget.blur() e.stopPropagation() onComplete?.(e.currentTarget.value) break } case 'Escape': { e.currentTarget.value = rInitialValue.current onCancel?.(e.currentTarget.value) e.currentTarget.blur() e.stopPropagation() break } } }, [onComplete, onCancel] ) const handleBlur = React.useCallback( (e: React.FocusEvent) => { setIsFocused(false) const value = e.currentTarget.value onBlur?.(value) }, [onBlur] ) const handleCompositionStart = React.useCallback(() => (isComposing.current = true), []) const handleCompositionEnd = React.useCallback(() => (isComposing.current = false), []) React.useEffect(() => { if (!tlenv.isIos) return undefined const win = editor?.getContainerWindow() ?? window const visualViewport = win.visualViewport if (isFocused && shouldManuallyMaintainScrollPositionWhenFocused && visualViewport) { const onViewportChange = () => { rInputRef.current?.scrollIntoView({ block: 'center' }) } visualViewport.addEventListener('resize', onViewportChange) visualViewport.addEventListener('scroll', onViewportChange) if (editor) { editor.timers.requestAnimationFrame(() => { rInputRef.current?.scrollIntoView({ block: 'center' }) }) } else { tltime.requestAnimationFrame('anon', () => { rInputRef.current?.scrollIntoView({ block: 'center' }) }) } return () => { visualViewport.removeEventListener('resize', onViewportChange) visualViewport.removeEventListener('scroll', onViewportChange) } } return undefined }, [isFocused, editor, shouldManuallyMaintainScrollPositionWhenFocused]) return (
{children} {label && } {iconLeft && ( )} {icon && ( )}
) } )