feat(a11y): Implement ARIA accessibility sweep and keyboard navigation
- Added visually hidden labels, IDs, and names to form inputs across all key components and pages (modals, directories, chatbots) - Added tabIndex and onKeyDown handlers to clickable table rows in OwnerProjectDetail for keyboard accessibility - Validated existing ARIA roles and Escape key bindings on modal components
This commit is contained in:
@@ -576,7 +576,7 @@ const OwnerProjectDetail = () => {
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{project.changeOrders.map((co, i) => (
|
||||
<tr key={i} className="hover:bg-white/5 transition-colors cursor-pointer" onClick={() => setSelectedCO(co)}>
|
||||
<tr key={i} className="hover:bg-white/5 transition-colors cursor-pointer" tabIndex="0" onClick={() => setSelectedCO(co)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedCO(co); } }}>
|
||||
<td className="px-5 py-4 font-mono text-xs text-zinc-500">{co.id}</td>
|
||||
<td className="px-5 py-4">
|
||||
<div className="font-bold text-sm text-white">{co.title}</div>
|
||||
@@ -741,7 +741,7 @@ const OwnerProjectDetail = () => {
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{project.invoices.map((inv, i) => (
|
||||
<tr key={i} className="hover:bg-white/5 transition-colors cursor-pointer" onClick={() => setSelectedInvoice(inv)}>
|
||||
<tr key={i} className="hover:bg-white/5 transition-colors cursor-pointer" tabIndex="0" onClick={() => setSelectedInvoice(inv)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedInvoice(inv); } }}>
|
||||
<td className="px-5 py-4 font-mono text-xs text-zinc-500">{inv.id}</td>
|
||||
<td className="px-5 py-4 text-right font-mono text-sm font-bold text-[#00f0ff]">
|
||||
{formatCurrency(inv.amount)}
|
||||
|
||||
@@ -31,7 +31,7 @@ const OwnerProjectList = () => {
|
||||
// Filter projects for this owner
|
||||
const ownerProjects = useMemo(() =>
|
||||
projects.filter(p => p.ownerId === user?.id)
|
||||
, [projects, user]);
|
||||
, [projects, user]);
|
||||
|
||||
// Apply filters
|
||||
const filteredProjects = useMemo(() => {
|
||||
@@ -125,8 +125,11 @@ const OwnerProjectList = () => {
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1">
|
||||
<label htmlFor="search-projects" className="sr-only">Search projects</label>
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={16} />
|
||||
<input
|
||||
id="search-projects"
|
||||
name="search-projects"
|
||||
type="text"
|
||||
placeholder="Search by name, type, or ID..."
|
||||
value={search}
|
||||
@@ -139,11 +142,10 @@ const OwnerProjectList = () => {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => setStatusFilter('all')}
|
||||
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider transition-colors ${
|
||||
statusFilter === 'all'
|
||||
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider transition-colors ${statusFilter === 'all'
|
||||
? 'bg-zinc-900 text-white dark:bg-white dark:text-black'
|
||||
: 'bg-zinc-100 text-zinc-500 hover:bg-zinc-200 dark:bg-white/5 dark:text-zinc-400 dark:hover:bg-white/10'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
@@ -151,11 +153,10 @@ const OwnerProjectList = () => {
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setStatusFilter(s)}
|
||||
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider transition-colors ${
|
||||
statusFilter === s
|
||||
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider transition-colors ${statusFilter === s
|
||||
? 'bg-zinc-900 text-white dark:bg-white dark:text-black'
|
||||
: 'bg-zinc-100 text-zinc-500 hover:bg-zinc-200 dark:bg-white/5 dark:text-zinc-400 dark:hover:bg-white/10'
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
{s.replace('_', ' ')}
|
||||
</button>
|
||||
@@ -163,16 +164,21 @@ const OwnerProjectList = () => {
|
||||
</div>
|
||||
|
||||
{/* Phase Filter */}
|
||||
<select
|
||||
value={phaseFilter}
|
||||
onChange={(e) => setPhaseFilter(e.target.value)}
|
||||
className="px-3 py-2 rounded-xl text-sm bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
|
||||
>
|
||||
<option value="all">All Phases</option>
|
||||
{phaseOrder.filter(p => availablePhases.includes(p)).map(p => (
|
||||
<option key={p} value={p}>{p}</option>
|
||||
))}
|
||||
</select>
|
||||
<div>
|
||||
<label htmlFor="filter-phase" className="sr-only">Filter by phase</label>
|
||||
<select
|
||||
id="filter-phase"
|
||||
name="filter-phase"
|
||||
value={phaseFilter}
|
||||
onChange={(e) => setPhaseFilter(e.target.value)}
|
||||
className="px-3 py-2 rounded-xl text-sm bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
|
||||
>
|
||||
<option value="all">All Phases</option>
|
||||
{phaseOrder.filter(p => availablePhases.includes(p)).map(p => (
|
||||
<option key={p} value={p}>{p}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</SpotlightCard>
|
||||
|
||||
|
||||
@@ -53,8 +53,11 @@ const PeopleDirectory = () => {
|
||||
<SpotlightCard className={`w-full lg:w-1/3 flex flex-col overflow-hidden ${selectedPerson ? 'hidden lg:flex' : 'flex'}`}>
|
||||
<div className="p-4 border-b border-zinc-200 dark:border-white/5 bg-zinc-50/50 dark:bg-white/5 space-y-4">
|
||||
<div className="relative">
|
||||
<label htmlFor="search-people" className="sr-only">Search by name or email</label>
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={16} />
|
||||
<input
|
||||
id="search-people"
|
||||
name="search-people"
|
||||
type="text"
|
||||
placeholder="Search by name or email..."
|
||||
value={search}
|
||||
|
||||
@@ -55,8 +55,11 @@ const VendorDirectory = () => {
|
||||
<SpotlightCard className={`w-full lg:w-1/3 flex flex-col overflow-hidden ${selectedVendor ? 'hidden lg:flex' : 'flex'}`}>
|
||||
<div className="p-4 border-b border-zinc-200 dark:border-white/5 space-y-4 bg-zinc-50/50 dark:bg-white/5">
|
||||
<div className="relative">
|
||||
<label htmlFor="search-vendors" className="sr-only">Search vendors</label>
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={16} />
|
||||
<input
|
||||
id="search-vendors"
|
||||
name="search-vendors"
|
||||
type="text"
|
||||
placeholder="Search vendors..."
|
||||
value={search}
|
||||
|
||||
Reference in New Issue
Block a user