Add Chromebook compatibility fixes and optimizations
Implemented comprehensive Chromebook-specific fixes including viewport adjustments, enhanced touch and keyboard detection, improved scrolling and keyboard navigation hooks, and extensive CSS optimizations for better usability. Updated chat and dashboard interfaces for larger touch targets, better focus management, and responsive layouts. Added documentation in docs/CHROMEBOOK-FIXES.md and new hooks for Chromebook scroll and keyboard handling.
This commit is contained in:
160
hooks/use-chromebook-keyboard.tsx
Normal file
160
hooks/use-chromebook-keyboard.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* Hook for enhanced keyboard navigation on Chromebooks
|
||||
*/
|
||||
export function useChromebookKeyboard() {
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
// Enhanced keyboard shortcuts for Chromebooks
|
||||
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
|
||||
|
||||
// Chromebook-specific shortcuts
|
||||
if (metaKey || ctrlKey) {
|
||||
switch (key) {
|
||||
case 'k':
|
||||
// Focus search or command palette
|
||||
e.preventDefault();
|
||||
const searchInput = document.querySelector('input[type="search"], input[placeholder*="search" i]') as HTMLInputElement;
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
// Submit forms with Ctrl/Cmd + Enter
|
||||
e.preventDefault();
|
||||
const form = document.querySelector('form') as HTMLFormElement;
|
||||
if (form) {
|
||||
const submitButton = form.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||
if (submitButton && !submitButton.disabled) {
|
||||
submitButton.click();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
// Navigate through messages or list items
|
||||
e.preventDefault();
|
||||
const focusableElements = document.querySelectorAll(
|
||||
'button, input, textarea, [tabindex]:not([tabindex="-1"]), [role="button"], [role="tab"]'
|
||||
) as NodeListOf<HTMLElement>;
|
||||
|
||||
const currentIndex = Array.from(focusableElements).indexOf(document.activeElement as HTMLElement);
|
||||
let nextIndex;
|
||||
|
||||
if (key === 'ArrowUp') {
|
||||
nextIndex = currentIndex > 0 ? currentIndex - 1 : focusableElements.length - 1;
|
||||
} else {
|
||||
nextIndex = currentIndex < focusableElements.length - 1 ? currentIndex + 1 : 0;
|
||||
}
|
||||
|
||||
focusableElements[nextIndex]?.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Escape key handling
|
||||
if (key === 'Escape') {
|
||||
// Close modals, clear inputs, or go back
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
|
||||
if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') {
|
||||
// Clear input on Escape
|
||||
(activeElement as HTMLInputElement).value = '';
|
||||
activeElement.blur();
|
||||
} else {
|
||||
// Look for close buttons or back buttons
|
||||
const closeButton = document.querySelector('[aria-label*="close" i], [aria-label*="back" i]') as HTMLElement;
|
||||
if (closeButton) {
|
||||
closeButton.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tab navigation enhancement
|
||||
if (key === 'Tab') {
|
||||
// Ensure proper tab order for Chromebooks
|
||||
const focusableElements = document.querySelectorAll(
|
||||
'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [role="button"]:not([disabled]), [role="tab"]'
|
||||
);
|
||||
|
||||
// Add visual focus indicators
|
||||
const addFocusIndicator = (element: Element) => {
|
||||
element.classList.add('keyboard-focus');
|
||||
};
|
||||
|
||||
const removeFocusIndicator = (element: Element) => {
|
||||
element.classList.remove('keyboard-focus');
|
||||
};
|
||||
|
||||
// Handle focus events
|
||||
focusableElements.forEach(element => {
|
||||
element.addEventListener('focus', () => addFocusIndicator(element));
|
||||
element.addEventListener('blur', () => removeFocusIndicator(element));
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Add global keyboard event listener
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
|
||||
return {
|
||||
handleKeyDown
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing focus in chat interfaces
|
||||
*/
|
||||
export function useChatFocus() {
|
||||
const focusMessageInput = useCallback(() => {
|
||||
const messageInput = document.querySelector('input[aria-label*="message" i], textarea[aria-label*="message" i]') as HTMLInputElement;
|
||||
if (messageInput) {
|
||||
messageInput.focus();
|
||||
messageInput.select();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const focusNextMessage = useCallback(() => {
|
||||
const messages = document.querySelectorAll('[role="article"]');
|
||||
const currentMessage = document.activeElement?.closest('[role="article"]');
|
||||
|
||||
if (currentMessage) {
|
||||
const currentIndex = Array.from(messages).indexOf(currentMessage);
|
||||
const nextMessage = messages[currentIndex + 1] as HTMLElement;
|
||||
if (nextMessage) {
|
||||
nextMessage.focus();
|
||||
}
|
||||
} else if (messages.length > 0) {
|
||||
(messages[0] as HTMLElement).focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const focusPreviousMessage = useCallback(() => {
|
||||
const messages = document.querySelectorAll('[role="article"]');
|
||||
const currentMessage = document.activeElement?.closest('[role="article"]');
|
||||
|
||||
if (currentMessage) {
|
||||
const currentIndex = Array.from(messages).indexOf(currentMessage);
|
||||
const previousMessage = messages[currentIndex - 1] as HTMLElement;
|
||||
if (previousMessage) {
|
||||
previousMessage.focus();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
focusMessageInput,
|
||||
focusNextMessage,
|
||||
focusPreviousMessage
|
||||
};
|
||||
}
|
||||
86
hooks/use-chromebook-scroll.tsx
Normal file
86
hooks/use-chromebook-scroll.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* Hook to enhance scrolling behavior for Chromebooks and touch devices
|
||||
*/
|
||||
export function useChromebookScroll() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
// Enhanced scrolling for Chromebooks
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
// Prevent default touch behavior that might interfere with scrolling
|
||||
if (e.touches.length === 1) {
|
||||
// Single touch - allow normal scrolling
|
||||
return;
|
||||
}
|
||||
|
||||
// Multi-touch - prevent zoom gestures
|
||||
if (e.touches.length > 1) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: TouchEvent) => {
|
||||
// Allow momentum scrolling on Chromebooks
|
||||
if (e.touches.length === 1) {
|
||||
// Single touch scrolling - allow default behavior
|
||||
return;
|
||||
}
|
||||
|
||||
// Multi-touch - prevent zoom
|
||||
if (e.touches.length > 1) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
// Enhanced wheel scrolling for Chromebook trackpads
|
||||
const delta = e.deltaY;
|
||||
const container = e.currentTarget as HTMLElement;
|
||||
|
||||
// Smooth scrolling for Chromebook trackpads
|
||||
if (Math.abs(delta) > 0) {
|
||||
container.scrollBy({
|
||||
top: delta * 0.5, // Reduce scroll speed for better control
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Add event listeners
|
||||
container.addEventListener('touchstart', handleTouchStart, { passive: false });
|
||||
container.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
container.addEventListener('wheel', handleWheel, { passive: true });
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
container.removeEventListener('touchstart', handleTouchStart);
|
||||
container.removeEventListener('touchmove', handleTouchMove);
|
||||
container.removeEventListener('wheel', handleWheel);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return containerRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for smooth scrolling to bottom (useful for chat interfaces)
|
||||
*/
|
||||
export function useSmoothScrollToBottom() {
|
||||
const scrollToBottom = (container: HTMLElement) => {
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
const scrollToBottomInstant = (container: HTMLElement) => {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
};
|
||||
|
||||
return { scrollToBottom, scrollToBottomInstant };
|
||||
}
|
||||
@@ -23,18 +23,29 @@ export function useIsTouchDevice() {
|
||||
|
||||
React.useEffect(() => {
|
||||
const checkTouch = () => {
|
||||
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0
|
||||
const hasTouch = 'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
(navigator.userAgent.includes('CrOS') && 'ontouchstart' in window) ||
|
||||
window.matchMedia('(pointer: coarse)').matches ||
|
||||
!window.matchMedia('(hover: hover)').matches
|
||||
|
||||
setIsTouch(hasTouch)
|
||||
}
|
||||
|
||||
checkTouch()
|
||||
|
||||
// Listen for changes in touch capability
|
||||
const mediaQuery = window.matchMedia('(pointer: coarse)')
|
||||
const hoverQuery = window.matchMedia('(hover: hover)')
|
||||
|
||||
const handleChange = () => checkTouch()
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
hoverQuery.addEventListener('change', handleChange)
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', handleChange)
|
||||
hoverQuery.removeEventListener('change', handleChange)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return !!isTouch
|
||||
|
||||
Reference in New Issue
Block a user