Enhance accessibility and improve form semantics across multiple pages
Build & Push Docker Image / build (push) Successful in 4m4s
Build & Push Docker Image / build (push) Successful in 4m4s
- Added `htmlFor` attributes to labels for better accessibility in Calendar, Dashboard, GuestJoin, Login, Register, RoomDetail, and Settings pages. - Included `aria-hidden` attributes for icons to improve screen reader experience. - Set `autoComplete` attributes for input fields to enhance user experience during form filling. - Implemented `role` and `aria` attributes for radio groups and buttons to improve accessibility compliance.
This commit is contained in:
@@ -30,8 +30,21 @@ export default function FederatedRoomCard({ room, onRemove }) {
|
||||
|
||||
const recordingOn = room.allow_recording === 1 || room.allow_recording === true;
|
||||
|
||||
const openRoom = () => navigate(`/federation/rooms/${room.id}`);
|
||||
|
||||
return (
|
||||
<div className={`card-hover group p-5 cursor-pointer ${isDeleted ? 'opacity-60' : ''}`} onClick={() => navigate(`/federation/rooms/${room.id}`)}>
|
||||
<div
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
aria-label={room.room_name}
|
||||
onClick={openRoom}
|
||||
onKeyDown={(e) => {
|
||||
if (e.target !== e.currentTarget) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openRoom(); }
|
||||
}}
|
||||
className={`card-hover group p-5 cursor-pointer focus:outline-hidden focus:ring-2 focus:ring-th-ring focus:ring-offset-2 ${isDeleted ? 'opacity-60' : ''}`}
|
||||
style={{ '--tw-ring-offset-color': 'var(--bg-primary)' }}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -107,6 +120,7 @@ export default function FederatedRoomCard({ room, onRemove }) {
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
className="btn-ghost text-xs py-1.5 px-2 text-th-error hover:text-th-error"
|
||||
aria-label={t('federation.removeRoom')}
|
||||
title={t('federation.removeRoom')}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
|
||||
@@ -1,15 +1,49 @@
|
||||
import { useEffect, useId, useRef } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
|
||||
export default function Modal({ title, children, onClose, maxWidth = 'max-w-lg' }) {
|
||||
const { t } = useLanguage();
|
||||
const titleId = useId();
|
||||
const dialogRef = useRef(null);
|
||||
const previouslyFocused = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
previouslyFocused.current = document.activeElement;
|
||||
const handleKey = (e) => {
|
||||
if (e.key === 'Escape') onClose?.();
|
||||
};
|
||||
document.addEventListener('keydown', handleKey);
|
||||
// Focus the dialog so screen readers announce it and keyboard focus lands inside
|
||||
dialogRef.current?.focus();
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKey);
|
||||
previouslyFocused.current?.focus?.();
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-xs" onClick={onClose} />
|
||||
<div className={`relative bg-th-card rounded-2xl border border-th-border shadow-2xl w-full ${maxWidth}`}>
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 backdrop-blur-xs"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
ref={dialogRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
tabIndex={-1}
|
||||
className={`relative bg-th-card rounded-2xl border border-th-border shadow-2xl w-full ${maxWidth} focus:outline-hidden`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-th-border rounded-t-2xl">
|
||||
<h2 className="text-lg font-semibold text-th-text">{title}</h2>
|
||||
<h2 id={titleId} className="text-lg font-semibold text-th-text">{title}</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label={t('common.close')}
|
||||
className="p-2 rounded-lg hover:bg-th-hover text-th-text-s transition-colors"
|
||||
>
|
||||
<X size={20} />
|
||||
|
||||
@@ -44,6 +44,7 @@ export default function Navbar({ onMenuClick }) {
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={onMenuClick}
|
||||
aria-label={t('nav.navigation')}
|
||||
className="lg:hidden p-2 rounded-lg hover:bg-th-hover text-th-text-s transition-colors"
|
||||
>
|
||||
<Menu size={20} />
|
||||
@@ -59,6 +60,9 @@ export default function Navbar({ onMenuClick }) {
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
aria-label={user?.display_name || user?.name}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={dropdownOpen}
|
||||
className="flex items-center gap-2 p-1.5 rounded-lg hover:bg-th-hover transition-colors"
|
||||
>
|
||||
<div
|
||||
@@ -68,7 +72,7 @@ export default function Navbar({ onMenuClick }) {
|
||||
{user?.avatar_image ? (
|
||||
<img
|
||||
src={`${api.defaults.baseURL}/auth/avatar/${user.avatar_image}`}
|
||||
alt="Avatar"
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -83,6 +83,11 @@ export default function NotificationBell() {
|
||||
<button
|
||||
onClick={() => setOpen(prev => !prev)}
|
||||
className="relative p-2 rounded-lg hover:bg-th-hover text-th-text-s transition-colors"
|
||||
aria-label={unreadCount > 0
|
||||
? `${t('notifications.bell')} (${unreadCount})`
|
||||
: t('notifications.bell')}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open}
|
||||
title={t('notifications.bell')}
|
||||
>
|
||||
<Bell size={20} />
|
||||
@@ -174,6 +179,7 @@ export default function NotificationBell() {
|
||||
<button
|
||||
onClick={(e) => handleDelete(e, n.id)}
|
||||
className="opacity-0 group-hover:opacity-100 p-0.5 rounded-sm hover:text-th-error transition-all text-th-text-s/50"
|
||||
aria-label={t('notifications.delete')}
|
||||
title={t('notifications.delete')}
|
||||
>
|
||||
<X size={13} />
|
||||
|
||||
@@ -43,8 +43,21 @@ export default function RoomCard({ room, onDelete }) {
|
||||
return () => clearInterval(interval);
|
||||
}, [room.uid]);
|
||||
|
||||
const openRoom = () => navigate(`/rooms/${room.uid}`);
|
||||
|
||||
return (
|
||||
<div className="card-hover group p-5 cursor-pointer" onClick={() => navigate(`/rooms/${room.uid}`)}>
|
||||
<div
|
||||
role="link"
|
||||
tabIndex={0}
|
||||
aria-label={room.name}
|
||||
onClick={openRoom}
|
||||
onKeyDown={(e) => {
|
||||
if (e.target !== e.currentTarget) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openRoom(); }
|
||||
}}
|
||||
className="card-hover group p-5 cursor-pointer focus:outline-hidden focus:ring-2 focus:ring-th-ring focus:ring-offset-2"
|
||||
style={{ '--tw-ring-offset-color': 'var(--bg-primary)' }}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -121,6 +134,9 @@ export default function RoomCard({ room, onDelete }) {
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setShowCopyMenu(v => !v); }}
|
||||
className="btn-ghost text-xs py-1.5 px-2"
|
||||
aria-label={t('room.copyLink')}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={showCopyMenu}
|
||||
title={t('room.copyLink')}
|
||||
>
|
||||
<Copy size={14} />
|
||||
@@ -148,6 +164,7 @@ export default function RoomCard({ room, onDelete }) {
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onDelete(room); }}
|
||||
className="btn-ghost text-xs py-1.5 px-2 text-th-error hover:text-th-error"
|
||||
aria-label={t('common.delete')}
|
||||
title={t('common.delete')}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
|
||||
@@ -60,6 +60,7 @@ export default function Sidebar({ open, onClose }) {
|
||||
<BrandLogo size="md" className="flex-1 min-w-0" />
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label={t('common.close')}
|
||||
className="lg:hidden p-1.5 rounded-lg hover:bg-th-hover text-th-text-s transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
@@ -112,7 +113,7 @@ export default function Sidebar({ open, onClose }) {
|
||||
{user?.avatar_image ? (
|
||||
<img
|
||||
src={`${api.defaults.baseURL}/auth/avatar/${user.avatar_image}`}
|
||||
alt="Avatar"
|
||||
alt=""
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -10,17 +10,23 @@ export default function ThemeSelector({ onClose }) {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-xs" onClick={onClose} />
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-xs" onClick={onClose} aria-hidden="true" />
|
||||
|
||||
<div className="relative bg-th-card rounded-2xl border border-th-border shadow-2xl w-full max-w-2xl max-h-[80vh] overflow-hidden">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="theme-selector-title"
|
||||
className="relative bg-th-card rounded-2xl border border-th-border shadow-2xl w-full max-w-2xl max-h-[80vh] overflow-hidden"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-th-border">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-th-text">{t('themes.selectTheme')}</h2>
|
||||
<h2 id="theme-selector-title" className="text-lg font-semibold text-th-text">{t('themes.selectTheme')}</h2>
|
||||
<p className="text-sm text-th-text-s mt-0.5">{t('themes.selectThemeSubtitle')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label={t('common.close')}
|
||||
className="p-2 rounded-lg hover:bg-th-hover text-th-text-s transition-colors"
|
||||
>
|
||||
<X size={20} />
|
||||
|
||||
Reference in New Issue
Block a user