All files / web/src/hooks useDeviceCapabilities.ts

0% Statements 0/173
0% Branches 0/1
0% Functions 0/1
0% Lines 0/173

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174                                                                                                                                                                                                                                                                                                                                                           
'use client'

/**
 * Device Capabilities Hooks
 *
 * Detect device input capabilities for adaptive UI behavior.
 * These hooks help distinguish between:
 * - Touch-only devices (phones, tablets without mouse)
 * - Pointer devices (desktops, laptops with mouse/trackpad)
 * - Hybrid devices (laptops with touchscreen, tablets with attached mouse)
 *
 * @see useMediaQuery for viewport-based breakpoints (useIsMobile, useIsDesktop, etc.)
 */

import { useEffect, useState } from 'react'

/**
 * Hook to detect if the device is primarily touch-based (mobile/tablet).
 * Returns true only for devices where touch is the primary input method.
 *
 * Use cases:
 * - Showing virtual keyboards
 * - Adjusting touch target sizes
 * - Showing mobile-specific UI
 *
 * Note: Returns false during SSR (before hydration)
 */
export function useIsTouchDevice(): boolean {
  const [isTouchDevice, setIsTouchDevice] = useState(false)

  useEffect(() => {
    const checkTouchDevice = () => {
      // Check if device has touch capability
      const hasTouchCapability =
        'ontouchstart' in window ||
        navigator.maxTouchPoints > 0 ||
        // @ts-expect-error - msMaxTouchPoints is IE/Edge specific
        navigator.msMaxTouchPoints > 0

      // Check if the device has no fine pointer (mouse)
      // This helps distinguish touch-only devices from laptops with touchscreens
      const hasNoFinePointer = !window.matchMedia('(pointer: fine)').matches

      // Also check for coarse pointer (finger/touch)
      const hasCoarsePointer = window.matchMedia('(pointer: coarse)').matches

      setIsTouchDevice(hasTouchCapability && (hasNoFinePointer || hasCoarsePointer))
    }

    checkTouchDevice()

    // Re-check on resize (in case device mode changes, e.g., responsive testing)
    window.addEventListener('resize', checkTouchDevice)
    return () => window.removeEventListener('resize', checkTouchDevice)
  }, [])

  return isTouchDevice
}

/**
 * Hook to detect if any pointing device on this device is "fine" (mouse-like).
 * Returns true for:
 * - Desktops/laptops with mouse
 * - Tablets with attached mouse/trackpad
 * - Laptops with touchscreen (primary may be touch, but mouse is available)
 *
 * Use cases:
 * - Showing hover-based UI hints (hot/cold feedback)
 * - Enabling mouse-specific interactions
 *
 * Note: Returns false during SSR (before hydration)
 */
export function useHasAnyFinePointer(): boolean {
  const [hasAnyFinePointer, setHasAnyFinePointer] = useState(false)

  useEffect(() => {
    const checkFinePointer = () => {
      // any-pointer: fine matches if ANY available pointing device is fine
      // This is broader than pointer: fine which only checks the primary device
      setHasAnyFinePointer(window.matchMedia('(any-pointer: fine)').matches)
    }

    checkFinePointer()

    // Re-check on resize (in case device mode changes)
    window.addEventListener('resize', checkFinePointer)
    return () => window.removeEventListener('resize', checkFinePointer)
  }, [])

  return hasAnyFinePointer
}

/**
 * Hook to detect if device has a physical keyboard available.
 * Uses a combination of:
 * 1. Actual keyboard input detection (most reliable)
 * 2. Heuristic fallback (pointer type, user agent, viewport)
 *
 * Returns:
 * - null: Still detecting (initial state)
 * - true: Physical keyboard detected or likely available
 * - false: No physical keyboard, show on-screen keyboard
 *
 * Use cases:
 * - Showing/hiding on-screen number pads
 * - Choosing between keyboard vs touch input modes
 */
export function useHasPhysicalKeyboard(): boolean | null {
  const [hasKeyboard, setHasKeyboard] = useState<boolean | null>(null)

  useEffect(() => {
    let keyboardDetected = false
    let detectionTimeout: NodeJS.Timeout | null = null

    const handleKeyPress = (e: KeyboardEvent) => {
      // Any number key press confirms physical keyboard
      if (/^[0-9]$/.test(e.key)) {
        keyboardDetected = true
        setHasKeyboard(true)
        cleanup()
      }
    }

    const detectFromHeuristics = () => {
      // Method 1: Check if device supports keyboard via media queries
      const hasKeyboardSupport =
        window.matchMedia('(pointer: fine)').matches && window.matchMedia('(hover: hover)').matches

      // Method 2: Check if device is likely touch-only
      const isTouchDevice =
        'ontouchstart' in window ||
        navigator.maxTouchPoints > 0 ||
        /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)

      // Method 3: Check viewport characteristics for mobile devices
      const isMobileViewport = window.innerWidth <= 768

      // Combined heuristic: likely has keyboard if:
      // - Desktop-like pointer AND not mobile user agent AND not mobile viewport
      const likelyHasKeyboard = hasKeyboardSupport && !isTouchDevice && !isMobileViewport

      // Alternative: assume no keyboard if touch device + mobile viewport + no precise pointer
      const likelyNoKeyboard = isTouchDevice && isMobileViewport && !hasKeyboardSupport

      if (!keyboardDetected) {
        // If clearly no keyboard, set false; if clearly has keyboard, set true; otherwise uncertain -> true (allow keyboard input)
        setHasKeyboard(likelyNoKeyboard ? false : likelyHasKeyboard || !isTouchDevice)
      }
    }

    const cleanup = () => {
      document.removeEventListener('keypress', handleKeyPress)
      if (detectionTimeout) clearTimeout(detectionTimeout)
    }

    // Listen for keyboard input
    document.addEventListener('keypress', handleKeyPress)

    // Fallback to heuristic after 2 seconds
    detectionTimeout = setTimeout(() => {
      if (!keyboardDetected) {
        detectFromHeuristics()
      }
    }, 2000)

    // Initial heuristic (will be overridden by actual keypress)
    setTimeout(detectFromHeuristics, 100)

    return cleanup
  }, [])

  return hasKeyboard
}