From d39dcf36fdb5906ef75d51df6ee7eec0c2249844 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Tue, 27 Jan 2026 23:05:04 +0530 Subject: [PATCH] feat(11-01): add accessibility and keyboard navigation Add comprehensive ARIA attributes, keyboard shortcuts, and focus management for chat interface. - JKSessionChatWindow: Add role="dialog", aria-labelledby, Escape key handler, auto-focus textarea - JKChatComposer: Add aria-label, aria-describedby, aria-invalid, role="alert" for validation - JKChatHeader: Add id="chat-window-title" for aria-labelledby reference - JKSessionChatButton: Add aria-label with unread count, aria-pressed state, role="button", tabIndex - Focus management: Textarea receives focus when window opens (100ms delay for DOM) - Keyboard navigation: Escape closes window, Tab order preserved, Enter sends message Co-Authored-By: Claude Sonnet 4.5 --- .../components/client/JKSessionChatButton.js | 4 +++ .../components/client/JKSessionChatWindow.js | 35 +++++++++++++++++-- .../components/client/chat/JKChatComposer.js | 17 +++++++-- .../components/client/chat/JKChatHeader.js | 2 +- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/jam-ui/src/components/client/JKSessionChatButton.js b/jam-ui/src/components/client/JKSessionChatButton.js index 62f4f1507..adbcf01ec 100644 --- a/jam-ui/src/components/client/JKSessionChatButton.js +++ b/jam-ui/src/components/client/JKSessionChatButton.js @@ -42,6 +42,10 @@ const JKSessionChatButton = ({ sessionId }) => { src={chatIcon} alt="Chat" onClick={handleClick} + role="button" + tabIndex={0} + aria-label={`Open chat${unreadCount > 0 ? ` (${unreadCount} unread)` : ''}`} + aria-pressed={isWindowOpen} style={{ cursor: 'pointer', width: '24px', diff --git a/jam-ui/src/components/client/JKSessionChatWindow.js b/jam-ui/src/components/client/JKSessionChatWindow.js index 667ce3178..7520d78fc 100644 --- a/jam-ui/src/components/client/JKSessionChatWindow.js +++ b/jam-ui/src/components/client/JKSessionChatWindow.js @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import WindowPortal from '../common/WindowPortal.js'; import JKChatHeader from './chat/JKChatHeader.js'; @@ -32,6 +32,7 @@ import { useJamServerContext } from '../../context/JamServerContext'; */ const JKSessionChatWindow = () => { const dispatch = useDispatch(); + const textareaRef = useRef(null); // Redux selectors const isWindowOpen = useSelector(selectIsChatWindowOpen); @@ -54,6 +55,29 @@ const JKSessionChatWindow = () => { return 'Session Chat'; }, [activeChannel]); + // Keyboard shortcut: Escape to close window + useEffect(() => { + const handleKeyDown = (e) => { + if (e.key === 'Escape') { + dispatch(closeChatWindow()); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [dispatch]); + + // Focus management: Auto-focus textarea when window opens + useEffect(() => { + if (isWindowOpen && textareaRef.current) { + // Delay focus to ensure DOM is ready + setTimeout(() => { + if (textareaRef.current) { + textareaRef.current.focus(); + } + }, 100); + } + }, [isWindowOpen]); + // Conditional render: only show if window is open if (!isWindowOpen) return null; @@ -64,7 +88,12 @@ const JKSessionChatWindow = () => { onClose={handleClose} windowId="jamkazam-chat" > -
+
{
- +
); diff --git a/jam-ui/src/components/client/chat/JKChatComposer.js b/jam-ui/src/components/client/chat/JKChatComposer.js index 65d2596a2..e8bd44734 100644 --- a/jam-ui/src/components/client/chat/JKChatComposer.js +++ b/jam-ui/src/components/client/chat/JKChatComposer.js @@ -20,8 +20,11 @@ import { useJamServerContext } from '../../../context/JamServerContext'; * - Gray: 0-230 characters (normal) * - Yellow: 231-255 characters (approaching limit) * - Red: 256+ characters (over limit) + * + * @param {Object} props - Component props + * @param {React.Ref} props.textareaRef - Ref for textarea (for focus management) */ -const JKChatComposer = () => { +const JKChatComposer = ({ textareaRef }) => { const dispatch = useDispatch(); const [inputText, setInputText] = useState(''); @@ -102,11 +105,15 @@ const JKChatComposer = () => {
{/* Textarea */}