feat(chat): add keyboard diagnostics
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, type RefObject } from "react";
|
||||
|
||||
import { BrowserDetector } from "@/utils/browser-detect";
|
||||
import { PlatformDetector } from "@/utils/platform-detect";
|
||||
|
||||
const CHAT_KEYBOARD_DEBUG_PARAM = "keyboard_debug";
|
||||
const CHAT_INPUT_LIFT_VAR = "--chat-input-lift";
|
||||
|
||||
interface VirtualKeyboardLike extends EventTarget {
|
||||
readonly boundingRect?: Pick<
|
||||
DOMRectReadOnly,
|
||||
"height" | "width" | "x" | "y"
|
||||
>;
|
||||
}
|
||||
|
||||
interface NavigatorWithVirtualKeyboard extends Navigator {
|
||||
readonly virtualKeyboard?: VirtualKeyboardLike;
|
||||
}
|
||||
|
||||
export interface ChatKeyboardDebugSnapshot {
|
||||
event: string;
|
||||
time: number;
|
||||
windowInnerHeight: number;
|
||||
documentClientHeight: number;
|
||||
scrollY: number;
|
||||
visualViewportSupported: boolean;
|
||||
visualHeight: number | null;
|
||||
visualWidth: number | null;
|
||||
visualOffsetTop: number | null;
|
||||
visualOffsetLeft: number | null;
|
||||
visualScale: number | null;
|
||||
virtualKeyboardSupported: boolean;
|
||||
virtualKeyboardRect: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
} | null;
|
||||
activeElement: string | null;
|
||||
activeBottom: number | null;
|
||||
activeFocused: boolean;
|
||||
inputLift: string | null;
|
||||
viewportMeta: string | null;
|
||||
platform: {
|
||||
name: string;
|
||||
osName: string;
|
||||
osVersion: string;
|
||||
deviceVendor: string;
|
||||
deviceModel: string;
|
||||
};
|
||||
browser: {
|
||||
name: string;
|
||||
version: string;
|
||||
inAppBrowserName: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__keyboardDebugCleanup?: () => void;
|
||||
__keyboardDebugSnapshot?: () => ChatKeyboardDebugSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
export interface UseChatKeyboardDiagnosticsOptions {
|
||||
containerRef: RefObject<HTMLElement | null>;
|
||||
}
|
||||
|
||||
export function useChatKeyboardDiagnostics({
|
||||
containerRef,
|
||||
}: UseChatKeyboardDiagnosticsOptions): void {
|
||||
useEffect(() => {
|
||||
if (!isChatKeyboardDiagnosticsEnabled(window.location.search)) return;
|
||||
return installChatKeyboardDiagnostics(containerRef);
|
||||
}, [containerRef]);
|
||||
}
|
||||
|
||||
export function isChatKeyboardDiagnosticsEnabled(search: string): boolean {
|
||||
return new URLSearchParams(search).get(CHAT_KEYBOARD_DEBUG_PARAM) === "1";
|
||||
}
|
||||
|
||||
export function installChatKeyboardDiagnostics(
|
||||
containerRef: RefObject<HTMLElement | null>,
|
||||
): () => void {
|
||||
window.__keyboardDebugCleanup?.();
|
||||
|
||||
const removers: Array<() => void> = [];
|
||||
let disposed = false;
|
||||
|
||||
const takeSnapshot = (event: string): ChatKeyboardDebugSnapshot => {
|
||||
const snapshot = createChatKeyboardDebugSnapshot(event, containerRef);
|
||||
window.console.info("[keyboard-debug]", snapshot);
|
||||
return snapshot;
|
||||
};
|
||||
|
||||
const listen = (
|
||||
target: EventTarget | null | undefined,
|
||||
eventName: string,
|
||||
label = eventName,
|
||||
) => {
|
||||
if (!target) return;
|
||||
const handler = () => takeSnapshot(label);
|
||||
target.addEventListener(eventName, handler);
|
||||
removers.push(() => target.removeEventListener(eventName, handler));
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
removers.forEach((remove) => remove());
|
||||
if (window.__keyboardDebugSnapshot === manualSnapshot) {
|
||||
delete window.__keyboardDebugSnapshot;
|
||||
}
|
||||
if (window.__keyboardDebugCleanup === cleanup) {
|
||||
delete window.__keyboardDebugCleanup;
|
||||
}
|
||||
window.console.info("[keyboard-debug] removed");
|
||||
};
|
||||
const manualSnapshot = () => takeSnapshot("manual");
|
||||
const virtualKeyboard = getVirtualKeyboard();
|
||||
|
||||
listen(window, "resize", "window.resize");
|
||||
listen(window, "scroll", "window.scroll");
|
||||
listen(window, "orientationchange", "orientationchange");
|
||||
listen(document, "focusin", "focusin");
|
||||
listen(document, "focusout", "focusout");
|
||||
listen(document, "visibilitychange", "visibilitychange");
|
||||
listen(window.visualViewport, "resize", "visualViewport.resize");
|
||||
listen(window.visualViewport, "scroll", "visualViewport.scroll");
|
||||
listen(virtualKeyboard, "geometrychange", "virtualKeyboard.geometrychange");
|
||||
|
||||
window.__keyboardDebugSnapshot = manualSnapshot;
|
||||
window.__keyboardDebugCleanup = cleanup;
|
||||
takeSnapshot("installed");
|
||||
|
||||
return cleanup;
|
||||
}
|
||||
|
||||
function createChatKeyboardDebugSnapshot(
|
||||
event: string,
|
||||
containerRef: RefObject<HTMLElement | null>,
|
||||
): ChatKeyboardDebugSnapshot {
|
||||
const visualViewport = window.visualViewport;
|
||||
const virtualKeyboardRect = getVirtualKeyboard()?.boundingRect ?? null;
|
||||
const activeElement = document.activeElement;
|
||||
const activeRect =
|
||||
activeElement instanceof HTMLElement
|
||||
? activeElement.getBoundingClientRect()
|
||||
: null;
|
||||
const platform = PlatformDetector.getPlatformInfo();
|
||||
const browser = BrowserDetector.getBrowserInfo();
|
||||
|
||||
return {
|
||||
event,
|
||||
time: Math.round(performance.now()),
|
||||
windowInnerHeight: window.innerHeight,
|
||||
documentClientHeight: document.documentElement.clientHeight,
|
||||
scrollY: window.scrollY,
|
||||
visualViewportSupported: visualViewport != null,
|
||||
visualHeight: visualViewport?.height ?? null,
|
||||
visualWidth: visualViewport?.width ?? null,
|
||||
visualOffsetTop: visualViewport?.offsetTop ?? null,
|
||||
visualOffsetLeft: visualViewport?.offsetLeft ?? null,
|
||||
visualScale: visualViewport?.scale ?? null,
|
||||
virtualKeyboardSupported: getVirtualKeyboard() != null,
|
||||
virtualKeyboardRect: virtualKeyboardRect
|
||||
? {
|
||||
x: virtualKeyboardRect.x,
|
||||
y: virtualKeyboardRect.y,
|
||||
width: virtualKeyboardRect.width,
|
||||
height: virtualKeyboardRect.height,
|
||||
}
|
||||
: null,
|
||||
activeElement: activeElement?.tagName ?? null,
|
||||
activeBottom: activeRect?.bottom ?? null,
|
||||
activeFocused: activeElement !== document.body,
|
||||
inputLift:
|
||||
containerRef.current?.style.getPropertyValue(CHAT_INPUT_LIFT_VAR) || null,
|
||||
viewportMeta:
|
||||
document.querySelector<HTMLMetaElement>('meta[name="viewport"]')
|
||||
?.content ?? null,
|
||||
platform: {
|
||||
name: platform.platform,
|
||||
osName: platform.osName,
|
||||
osVersion: platform.osVersion,
|
||||
deviceVendor: platform.deviceVendor,
|
||||
deviceModel: platform.deviceModel,
|
||||
},
|
||||
browser: {
|
||||
name: browser.name,
|
||||
version: browser.version,
|
||||
inAppBrowserName: browser.inAppBrowserName,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getVirtualKeyboard(): VirtualKeyboardLike | null {
|
||||
return (
|
||||
(navigator as NavigatorWithVirtualKeyboard).virtualKeyboard ?? null
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user