"use client"; import React, { useEffect, useState, useCallback, useRef } from "react"; interface KeyboardState { isVisible: boolean; height: number; duration: number; easing: string; } interface ViewportInfo { width: number; height: number; visualHeight: number; keyboardHeight: number; } interface MobileKeyboardHandlerProps { children: React.ReactNode; onKeyboardToggle?: (isVisible: boolean, height: number) => void; adjustmentStrategy?: "resize" | "scroll" | "shrink"; className?: string; enableSafeArea?: boolean; minVisibleHeight?: number; } // Hook for detecting keyboard visibility and viewport changes export function useKeyboardDetection(): KeyboardState & ViewportInfo { const [keyboardState, setKeyboardState] = useState({ isVisible: false, height: 0, duration: 300, easing: "ease-out", }); const [viewportInfo, setViewportInfo] = useState({ width: 0, height: 0, visualHeight: 0, keyboardHeight: 0, }); useEffect(() => { if (typeof window === "undefined") return; let initialHeight = window.innerHeight; let initialWidth = window.innerWidth; const updateViewport = () => { const currentHeight = window.innerHeight; const currentWidth = window.innerWidth; // Use Visual Viewport API if available let visualViewportHeight = currentHeight; if ("visualViewport" in window && window.visualViewport) { visualViewportHeight = window.visualViewport.height; } const keyboardHeight = Math.max(0, initialHeight - visualViewportHeight); const keyboardVisible = keyboardHeight > 150; // Threshold for keyboard detection setKeyboardState((prev) => ({ ...prev, isVisible: keyboardVisible, height: keyboardHeight, })); setViewportInfo({ width: currentWidth, height: currentHeight, visualHeight: visualViewportHeight, keyboardHeight, }); }; // Handle orientation changes const handleOrientationChange = () => { // Wait for orientation change to complete setTimeout(() => { initialHeight = window.innerHeight; initialWidth = window.innerWidth; updateViewport(); }, 500); }; // Multiple event listeners for comprehensive coverage const events = ["resize", "orientationchange"]; events.forEach((event) => { window.addEventListener(event, updateViewport); }); // Special handling for orientation change window.addEventListener("orientationchange", handleOrientationChange); // Visual Viewport API listeners if ("visualViewport" in window && window.visualViewport) { window.visualViewport.addEventListener("resize", updateViewport); window.visualViewport.addEventListener("scroll", updateViewport); } // iOS specific handling if (/iPhone|iPad|iPod/.test(navigator.userAgent)) { // iOS specific events document.addEventListener("focusin", updateViewport); document.addEventListener("focusout", updateViewport); } // Initial update updateViewport(); return () => { events.forEach((event) => { window.removeEventListener(event, updateViewport); }); window.removeEventListener("orientationchange", handleOrientationChange); if ("visualViewport" in window && window.visualViewport) { window.visualViewport.removeEventListener("resize", updateViewport); window.visualViewport.removeEventListener("scroll", updateViewport); } if (/iPhone|iPad|iPod/.test(navigator.userAgent)) { document.removeEventListener("focusin", updateViewport); document.removeEventListener("focusout", updateViewport); } }; }, []); return { ...keyboardState, ...viewportInfo }; } // Hook for managing input focus and keyboard behavior export function useMobileInputFocus() { const scrollToInput = useCallback((inputElement: HTMLElement) => { if (!inputElement) return; // Prevent default zoom behavior on iOS const originalFontSize = inputElement.style.fontSize; const originalViewport = document.querySelector('meta[name="viewport"]'); // Temporarily set font size to prevent zoom inputElement.style.fontSize = "16px"; // Scroll input into view setTimeout(() => { inputElement.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest", }); // Focus the input inputElement.focus(); // Restore original font size if (originalFontSize) { inputElement.style.fontSize = originalFontSize; } }, 100); }, []); const preventZoom = useCallback((inputElement: HTMLInputElement) => { // Prevent zoom on input focus (iOS) const currentFontSize = window.getComputedStyle(inputElement).fontSize; const fontSize = parseFloat(currentFontSize); if (fontSize < 16) { inputElement.style.fontSize = "16px"; inputElement.style.transform = `scale(${fontSize / 16})`; inputElement.style.transformOrigin = "left center"; } }, []); const handleInputFocus = useCallback( (event: React.FocusEvent) => { const input = event.target; preventZoom(input); scrollToInput(input); }, [preventZoom, scrollToInput] ); return { scrollToInput, preventZoom, handleInputFocus }; } // Component for handling keyboard adjustments export function MobileKeyboardHandler({ children, onKeyboardToggle, adjustmentStrategy = "resize", className = "", enableSafeArea = true, minVisibleHeight = 200, }: MobileKeyboardHandlerProps) { const keyboard = useKeyboardDetection(); const containerRef = useRef(null); // Notify parent of keyboard changes useEffect(() => { onKeyboardToggle?.(keyboard.isVisible, keyboard.height); }, [keyboard.isVisible, keyboard.height, onKeyboardToggle]); // Apply adjustments based on strategy const getContainerStyle = useCallback((): React.CSSProperties => { const baseStyle: React.CSSProperties = { position: "relative", transition: `all ${keyboard.duration}ms ${keyboard.easing}`, width: "100%", }; if (!keyboard.isVisible) { return { ...baseStyle, height: "100vh", maxHeight: "100vh", }; } switch (adjustmentStrategy) { case "resize": return { ...baseStyle, height: `${keyboard.visualHeight}px`, maxHeight: `${keyboard.visualHeight}px`, overflow: "hidden", }; case "scroll": return { ...baseStyle, height: "100vh", paddingBottom: `${keyboard.keyboardHeight}px`, overflowY: "auto", }; case "shrink": const availableHeight = Math.max( keyboard.visualHeight, minVisibleHeight ); return { ...baseStyle, height: `${availableHeight}px`, maxHeight: `${availableHeight}px`, transform: keyboard.isVisible ? `translateY(-${Math.max( 0, keyboard.keyboardHeight - (keyboard.height - availableHeight) )}px)` : "translateY(0)", }; default: return baseStyle; } }, [ keyboard.isVisible, keyboard.visualHeight, keyboard.keyboardHeight, keyboard.height, keyboard.duration, keyboard.easing, adjustmentStrategy, minVisibleHeight, ]); // Apply safe area styles for devices with notches const getSafeAreaStyle = useCallback((): React.CSSProperties => { if (!enableSafeArea) return {}; return { paddingTop: "env(safe-area-inset-top)", paddingLeft: "env(safe-area-inset-left)", paddingRight: "env(safe-area-inset-right)", paddingBottom: keyboard.isVisible ? "0" : "env(safe-area-inset-bottom)", }; }, [enableSafeArea, keyboard.isVisible]); return (
{children}
); } // Enhanced input component with keyboard handling interface KeyboardAwareInputProps { value: string; onChange: (value: string) => void; onSubmit?: () => void; placeholder?: string; disabled?: boolean; className?: string; autoFocus?: boolean; keepVisible?: boolean; } export function KeyboardAwareInput({ value, onChange, onSubmit, placeholder, disabled = false, className = "", autoFocus = false, keepVisible = true, }: KeyboardAwareInputProps) { const inputRef = useRef(null); const { handleInputFocus } = useMobileInputFocus(); const keyboard = useKeyboardDetection(); // Keep input visible when keyboard appears useEffect(() => { if (keepVisible && keyboard.isVisible && inputRef.current) { const input = inputRef.current; const inputRect = input.getBoundingClientRect(); const viewportHeight = keyboard.visualHeight; // Check if input is hidden by keyboard if (inputRect.bottom > viewportHeight) { input.scrollIntoView({ behavior: "smooth", block: "end", }); } } }, [keyboard.isVisible, keyboard.visualHeight, keepVisible]); const handleKeyPress = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter" && onSubmit) { e.preventDefault(); onSubmit(); } }, [onSubmit] ); return ( onChange(e.target.value)} onKeyPress={handleKeyPress} onFocus={handleInputFocus} placeholder={placeholder} disabled={disabled} autoFocus={autoFocus} className={className} style={{ fontSize: "16px", // Prevent zoom on iOS minHeight: "48px", padding: "12px 16px", borderRadius: "24px", border: "2px solid #e1e5e9", outline: "none", width: "100%", backgroundColor: "#f8f9fa", transition: "border-color 0.2s ease", touchAction: "manipulation", WebkitAppearance: "none", // Remove iOS styling }} /> ); } // Hook for back button handling (Android/Browser) export function useBackButtonHandler(onBackPress?: () => void) { useEffect(() => { if (typeof window === "undefined" || !onBackPress) return; // Handle browser back button const handlePopState = (event: PopStateEvent) => { event.preventDefault(); onBackPress(); }; // Push a state to handle back button window.history.pushState({ modal: true }, "", ""); window.addEventListener("popstate", handlePopState); return () => { window.removeEventListener("popstate", handlePopState); // Clean up history state if still on the same state if (window.history.state?.modal) { window.history.back(); } }; }, [onBackPress]); } // Hook for preventing body scroll when modal is open export function useScrollLock(isLocked: boolean) { useEffect(() => { if (typeof document === "undefined") return; const body = document.body; const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth; if (isLocked) { const originalStyle = window.getComputedStyle(body); const originalTop = body.style.top; const originalPosition = body.style.position; const originalOverflow = body.style.overflow; const originalPaddingRight = body.style.paddingRight; // Lock scroll body.style.overflow = "hidden"; body.style.paddingRight = `${scrollBarWidth}px`; // iOS specific fixes if (/iPhone|iPad|iPod/.test(navigator.userAgent)) { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; body.style.position = "fixed"; body.style.top = `-${scrollTop}px`; body.style.left = "0"; body.style.right = "0"; } return () => { // Restore scroll body.style.overflow = originalOverflow; body.style.paddingRight = originalPaddingRight; if (/iPhone|iPad|iPod/.test(navigator.userAgent)) { body.style.position = originalPosition; body.style.top = originalTop; body.style.left = ""; body.style.right = ""; // Restore scroll position const top = parseInt(originalTop || "0", 10) * -1; window.scrollTo(0, top); } }; } }, [isLocked]); }