import type { MaybeRef, Ref } from 'vue' import { toValue, watchEffect } from 'vue' import { getActiveElement } from '@/shared' import { DRAWER_CSS_VARS } from '../utils' // Thresholds and timings ported from Base UI `DrawerVirtualKeyboardProvider.tsx`. /** Viewport shrinkage below this is browser chrome moving, not the keyboard. */ const KEYBOARD_RESIZE_THRESHOLD = 60 /** Gap kept between the focused field and the edges of the visible band. */ const KEYBOARD_VISIBILITY_MARGIN = 16 const KEYBOARD_REALIGN_INTERVAL = 150 const KEYBOARD_REALIGN_MAX_PASSES = 4 /** Frames the alignment waits for its scroll destination to stop moving. */ const KEYBOARD_SETTLE_FRAME_LIMIT = 60 /** Travel that turns a tap into a scroll/swipe. */ const INPUT_TAP_MOVE_THRESHOLD = 10 const INPUT_TAP_HIT_SLOP = 16 const KEYBOARD_INPUT_TYPES = new Set([ 'email', 'number', 'password', 'search', 'tel', 'text', 'url', ]) const INTERACTIVE_SELECTOR = 'button,a[href],select,[role="button"],[tabindex]:not([tabindex="-1"]),' + 'input:not([type="hidden"]):not([disabled]),textarea:not([disabled]),' + '[contenteditable]:not([contenteditable="false"])' /** * The lift point landed on another interactive element. Distinct from `null` so * the caller does not fall back to the touchstart target and steal that tap. */ const KEYBOARD_TAP_BLOCKED = Symbol('KeyboardTapBlocked') export interface KeyboardViewport { /** Top of the visible band, in client coordinates. */ top: number /** Bottom of the visible band, in client coordinates. */ bottom: number /** The browser is still resizing the layout viewport to the visual viewport. */ resizing: boolean } interface KeyboardTouchTarget { focusTarget: HTMLElement clickTarget: HTMLElement } function isKeyboardInputElement(element: HTMLElement): boolean { if (element.isContentEditable) return true const win = element.ownerDocument.defaultView ?? window if ( element instanceof win.HTMLTextAreaElement || (element instanceof win.HTMLInputElement && KEYBOARD_INPUT_TYPES.has(element.type)) ) { // A disabled control can neither focus nor open the keyboard. return !element.matches(':disabled') } return false } /** Nodes inside a `contenteditable` are not focusable; the editing host is. */ function getContentEditableHost(element: HTMLElement): HTMLElement { let host = element while (host.parentElement?.isContentEditable) host = host.parentElement return host } /** * Normalizes a target to the element the keyboard is attached to: the field * itself, its `contenteditable` host, or the control a `