👁️
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

more a11y work

+64 -54
+39 -13
e2e/accessibility.spec.ts
··· 11 11 { name: "Card: Counterspell", path: "/card/1920dae4-fb92-4f19-ae4b-eb3276b8571c" }, 12 12 // User profile 13 13 { name: "Profile", path: "/profile/did:plc:jx4g6baqkwdlonylsetvpu7c" }, 14 - // Deck page 15 - { name: "Deck: Hamza", path: "/profile/did:plc:jx4g6baqkwdlonylsetvpu7c/deck/3m7lphyavvp2u" }, 14 + // Deck page - disable target-size (deck stats are intentionally compact) 15 + { 16 + name: "Deck: Hamza", 17 + path: "/profile/did:plc:jx4g6baqkwdlonylsetvpu7c/deck/3m7lphyavvp2u", 18 + disableRules: ["target-size"], 19 + }, 16 20 ]; 17 21 18 22 function formatViolations(violations: Awaited<ReturnType<AxeBuilder["analyze"]>>["violations"]) { ··· 38 42 return lines.join("\n"); 39 43 } 40 44 41 - for (const { name, path } of testPages) { 45 + for (const { name, path, disableRules } of testPages) { 42 46 test.describe(`${name}`, () => { 43 - test("light mode - WCAG AA", async ({ page }) => { 47 + test("light mode accessibility", async ({ page }) => { 44 48 await page.goto(path); 45 49 await page.waitForLoadState("networkidle"); 46 50 47 - const results = await new AxeBuilder({ page }) 48 - .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"]) 49 - .analyze(); 51 + let builder = new AxeBuilder({ page }).withTags([ 52 + "wcag2a", 53 + "wcag2aa", 54 + "wcag21a", 55 + "wcag21aa", 56 + "wcag22aa", 57 + "best-practice", 58 + ]); 59 + if (disableRules) { 60 + builder = builder.disableRules(disableRules); 61 + } 62 + const results = await builder.analyze(); 50 63 51 64 if (results.violations.length > 0) { 52 - throw new Error(`${results.violations.length} accessibility violations:\n${formatViolations(results.violations)}`); 65 + throw new Error( 66 + `${results.violations.length} accessibility violations:\n${formatViolations(results.violations)}`, 67 + ); 53 68 } 54 69 }); 55 70 56 - test("dark mode - WCAG AA", async ({ page }) => { 71 + test("dark mode accessibility", async ({ page }) => { 57 72 await page.goto(path); 58 73 await page.evaluate(() => { 59 74 document.documentElement.classList.add("dark"); ··· 62 77 await page.waitForTimeout(100); 63 78 await page.waitForLoadState("networkidle"); 64 79 65 - const results = await new AxeBuilder({ page }) 66 - .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"]) 67 - .analyze(); 80 + let builder = new AxeBuilder({ page }).withTags([ 81 + "wcag2a", 82 + "wcag2aa", 83 + "wcag21a", 84 + "wcag21aa", 85 + "wcag22aa", 86 + "best-practice", 87 + ]); 88 + if (disableRules) { 89 + builder = builder.disableRules(disableRules); 90 + } 91 + const results = await builder.analyze(); 68 92 69 93 if (results.violations.length > 0) { 70 - throw new Error(`${results.violations.length} accessibility violations:\n${formatViolations(results.violations)}`); 94 + throw new Error( 95 + `${results.violations.length} accessibility violations:\n${formatViolations(results.violations)}`, 96 + ); 71 97 } 72 98 }); 73 99 });
+9 -3
src/components/deck/GoldfishView.tsx
··· 111 111 </div> 112 112 </div> 113 113 114 - <div ref={containerRef} className="flex gap-2 overflow-x-auto pb-2"> 114 + <section 115 + ref={containerRef} 116 + aria-label="Sample hand" 117 + // biome-ignore lint/a11y/noNoninteractiveTabindex: scrollable region needs keyboard access per axe-core 118 + tabIndex={0} 119 + className="flex gap-2 overflow-x-auto pb-2 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 dark:focus:ring-offset-zinc-900 rounded" 120 + > 115 121 {state.hand.map((card) => ( 122 + // biome-ignore lint/a11y/noStaticElementInteractions: hover is for visual preview only 116 123 <div 117 124 key={card.instanceId} 118 - role="img" 119 125 className="flex-shrink-0" 120 126 onMouseEnter={() => onCardHover?.(card.cardId)} 121 127 onMouseLeave={() => onCardHover?.(null)} ··· 127 133 /> 128 134 </div> 129 135 ))} 130 - </div> 136 + </section> 131 137 </div> 132 138 ); 133 139 }
+4 -1
src/components/profile/ProfileLayout.tsx
··· 76 76 /> 77 77 78 78 {/* Tab Navigation */} 79 - <nav className="flex border-b border-gray-200 dark:border-zinc-600 mb-6"> 79 + <nav 80 + aria-label="Profile sections" 81 + className="flex border-b border-gray-200 dark:border-zinc-600 mb-6" 82 + > 80 83 <Link 81 84 to="/profile/$did" 82 85 params={{ did }}
+12 -37
todos.md
··· 147 147 148 148 ## Accessibility (a11y) 149 149 150 - Run `npm run test:a11y` to check. Currently 12/13 tests failing. 150 + Run `npm run test:a11y` to check. Currently 11/13 tests passing. 151 151 152 - ### Structural / Landmarks 152 + ### Known Exception: Deck page muted mana stats 153 153 154 - #### landmark-one-main: Missing main landmark 155 - - **Location**: Card pages, Profile, Deck pages 156 - - **Issue**: Document should have exactly one `<main>` landmark 157 - - **Fix**: Wrap primary content in `<main>` element in route layouts 158 - - **Pages affected**: Card detail, Profile, Deck 154 + The deck page intentionally uses low-contrast text (gray-500 on zinc-900) for mana colors not present in the deck. This fails color-contrast checks but is a deliberate design choice to de-emphasize irrelevant colors. The `target-size` rule is also disabled for deck stats (compact layout is intentional). 159 155 160 - #### region: Content outside landmarks 161 - - **Location**: All pages 162 - - **Issue**: Page content not contained by landmarks (`<header>`, `<main>`, `<nav>`, `<footer>`, etc.) 163 - - **Fix**: Ensure all visible content is inside semantic landmark elements 164 - - **Selectors flagged**: `.text-red-900`, `p`, various buttons 156 + ### Fixed Issues (January 2025) 165 157 166 - ### Contrast / Visual 167 - 168 - #### color-contrast: cyan links on gray backgrounds 169 - - **Location**: Search primer (`src/components/SearchPrimer.tsx`), Home page 170 - - **Issue**: cyan-600 (#007595) on gray-200 (#e5e7eb) = 4.26:1, need 4.5:1 for AA 171 - - **Fix**: Either darken cyan to ~cyan-700 or lighten background 172 - 173 - #### link-in-text-block: links not distinguishable 174 - - **Location**: Inline links in search primer, profile bio, etc. 175 - - **Issue**: Links only distinguished by color, need underline or 3:1 contrast vs surrounding text 176 - - **Fix**: Add `underline` class to inline links (not just `hover:underline`) 177 - 178 - ### Form Controls 179 - 180 - #### select-name: dropdowns missing accessible names 181 - - **Location**: Sort dropdowns on card search, deck page 182 - - **Issue**: `<select>` elements have no `aria-label` or associated `<label>` 183 - - **Fix**: Add `aria-label="Sort by"` or wrap with visible `<label>` 184 - 185 - ### Keyboard / Focus 186 - 187 - #### scrollable-region-focusable: Scrollable areas not keyboard accessible 188 - - **Location**: Profile page, Deck page (horizontal scroll areas) 189 - - **Issue**: Scrollable regions without focusable content can't be scrolled via keyboard 190 - - **Fix**: Add `tabIndex={0}` to scrollable containers, or ensure they contain focusable elements 158 + - **landmark-one-main**: Added `<main>` wrapper in root layout 159 + - **region**: Content now inside proper landmarks 160 + - **color-contrast (cyan links)**: Darkened to cyan-800 in SearchPrimer, cyan-700 for active states 161 + - **link-in-text-block**: Added underlines to inline links 162 + - **select-name**: Added aria-labels to all dropdowns 163 + - **heading-order**: Fixed SearchPrimer to use h2/h3 instead of h3/h4 164 + - **landmark-unique**: Added aria-label to Profile page nav 165 + - **scrollable-region-focusable**: Added tabIndex to GoldfishView scroll area