Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108) - #214

Merged
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2
Jun 12, 2026
Merged

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108)#214
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 12, 2026

Copy link
Copy Markdown
Member

Wave 2 — Phase 2: accessibility (WCAG AA)

Two scoped, single-issue commits. Each verified by a fresh-context scope check; builds green (tsc + eslint + vitest 37/37). Not for merge yet — awaiting spot-check of the contrast list.

#107 — design-token contrast (WCAG 1.4.3)

Darken four shared tokens/controls that measured below 4.5:1 (ratios computed with the WCAG relative-luminance formula):

Token / controlBeforeBefore ratioAfterAfter ratio
--text-muted (~224 uses)#8a83723.4–3.8:1#6f68575.0–5.5:1
.btn--primary bg (white label)--accent#8a9a5b3.06:1--brand-forest#1B6C426.37:1
.chip--accent text--accent on --accent-soft2.80:1--sap-600#3a6a2c5.84:1
ModelToggle "Fast" label#3B82F62.87:1#1D4ED85.23:1
ModelToggle "Smart" label#8A63D23.30:1#6D28D95.36:1

Also re-verified the #112 state cues: error ! = 4.70:1 (text); rarity status dot ≥3:1 for all five tiers (non-text). Pre-existing/out-of-scope finding flagged: the rarity text label (common/uncommon/legendary) is 3.2–4.2:1.

#108 — focus + a11y labels (2.4.7 / 4.1.2 / 2.4.3 / 1.1.1)

  • Removed inline outline:"none" on 13 inputs/textareas → global :focus-visible ring restored.
  • aria-labels on the ChatPanel textarea, ManageCourses search, Calendar checkboxes.
  • SignInModal: focus-in + Tab-trap + restore (Dialog pattern in place).
  • role="img"+label on FunctionPlot / MermaidBlock; KnowledgeGraph2D gets a label + visually-hidden node list; landing logo <div onClick><button>.

Summary by CodeRabbit

  • Accessibility Improvements

    • Restored visible focus indicators on form inputs for keyboard navigation.
    • Added descriptive labels for screen reader users across interactive elements.
    • Enhanced keyboard navigation with improved focus management in modals.
    • Added accessibility descriptions for charts and diagrams.
  • Style Updates

    • Refined color values and visual styling for buttons and UI components.

Darken four contrast failures that measured below the 4.5:1 AA minimum
for normal text:
- --text-muted #8a8372 (~224 uses) -> #6f6857: now 5.0-5.5:1 on
white / --bg / --bg-inset (was 3.4-3.8:1).
- .btn--primary background --accent #8a9a5b (white label 3.06:1) ->
--brand-forest #1B6C42 (white label 6.37:1), tying the primary
action to the established brand green.
- .chip--accent text --accent on --accent-soft (2.80:1) -> --sap-600
(5.84:1).
- ModelToggle active labels: Fast #3B82F6 (2.87:1) -> #1D4ED8 (5.23:1),
Smart #8A63D2 (3.30:1) -> #6D28D9 (5.36:1), on their soft pills.
Ratios computed with the WCAG relative-luminance formula. The new #112
state cues were verified too: the error "!" is 4.70:1 (--err on
--err-soft) and the rarity status dot clears the 3:1 non-text bar for
all five tiers.
#108)
Focus visibility (WCAG 2.4.7): remove the inline outline:"none" on the
13 inputs/textareas (ChatPanel, Social, notetaker x6, Learn x2,
ParsedCardsTable x2, page.tsx) so each falls back to the existing global
:focus-visible accent ring instead of having no focus indicator.
Accessible names (4.1.2 / 3.3.2): aria-label on the ChatPanel tutor
textarea ("Message"), the ManageCoursesModal search input, and the
Calendar select-all + per-row checkboxes.
Modal focus (2.4.3): SignInModal now moves focus into the panel on open,
traps Tab within it, and restores focus to the trigger on close. Applied
the Dialog.tsx pattern in place (rather than reparenting into Dialog) to
preserve SignInModal's custom panel and close animation.
Non-text content (1.1.1): role="img" + a source-derived aria-label on
FunctionPlot and MermaidBlock; KnowledgeGraph2D gains role="img"/label
plus a visually-hidden node <ul> (mirroring KnowledgeGraph3D) so the
pointer-only graph is keyboard/AT navigable; the landing logo
<div onClick> becomes a <button> with its icon marked decorative.
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend1a34b4fJun 12 2026, 02:30 PM

@coderabbitai

coderabbitaiBot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR improves application accessibility and visual consistency across the frontend. It restores browser focus outlines on form inputs, adds descriptive labels and semantic roles to interactive elements and visualizations, implements keyboard focus management in the sign-in modal, and updates design system tokens for better contrast compliance and visual harmony.

Changes

Accessibility and Design System Refinement

Layer / File(s)Summary
Focus outline restoration for form inputs
frontend/src/app/(shell)/notetaker/page.tsx, frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/flashcards/ParsedCardsTable.tsx, frontend/src/components/screens/Learn.tsx, frontend/src/components/screens/Social.tsx
Removes outline: "none" from multiple input and textarea elements across the application, restoring browser default focus indicators while preserving borders, backgrounds, and colors.
Semantic and accessible input controls
frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/ManageCoursesModal.tsx, frontend/src/components/screens/Calendar.tsx
Adds aria-label attributes to unlabeled inputs and checkboxes; converts navbar brand from div to button with proper labeling; improves checkbox accessibility in the assignments table.
Visual accessibility for complex components
frontend/src/components/FunctionPlot.tsx, frontend/src/components/MermaidBlock.tsx, frontend/src/components/KnowledgeGraph2D.tsx
Marks data visualization components (function plots, Mermaid diagrams, knowledge graphs) with role="img" and normalized aria-label attributes. Adds screen-reader-only list of graph nodes and reusable SR_ONLY style constant.
Modal keyboard and focus management
frontend/src/components/SignInModal.tsx
Implements keyboard accessibility by adding focus trap logic that contains Tab/Shift+Tab navigation within the modal, closes on Escape, and manages focus restoration during open/close lifecycle.
Design system token and color updates
frontend/src/app/globals.css, frontend/src/components/AchievementUnlockToast.tsx, frontend/src/components/ModelToggle.tsx
Updates CSS custom properties and component colors: --text-muted darkened for WCAG AA compliance, .chip--accent text color changed to --sap-600, .btn--primary background switched to --brand-forest with brightness hover effect, badge and option colors aligned with theme system.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit hops through focus rings so bright,
Accessible labels shine with WCAG might,
Modals trap focus, buttons emerge from divs,
Design tokens harmonize—accessibility thrives!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and specifically references the two main focuses of the PR—accessibility improvements for contrast (#107) and focus/labels (#108)—accurately summarizing the primary changes.
Description check✅ PassedThe description comprehensively covers changes made, related issues (#107, #108, #112), testing status (builds green), and detailed technical notes including WCAG ratios and implementation details for reviewers.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch a11y/wave2-phase2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/KnowledgeGraph2D.tsx`:
- Around line 648-655: The SR_ONLY hidden node list in KnowledgeGraph2D.tsx
currently renders focusable <button> elements inside nodes.map when onNodeClick
is provided (symbols: SR_ONLY, nodes.map, onNodeClick), which can receive
keyboard focus while visually hidden; change the hidden list to be
non-interactive by replacing the focusable controls with plain, non-focusable
elements (e.g., render a <span> or similar static element for each node) or
remove onClick handlers from the SR_ONLY list so no element inside the list can
receive focus, and if you must keep interactive behavior move the clickable
<button> out of the SR_ONLY container or explicitly set tabIndex={-1} and
aria-hidden to prevent focus but prefer the non-interactive approach.
In `@frontend/src/components/SignInModal.tsx`:
- Around line 79-93: The onKey handler for the SignInModal’s focus trap
(function onKey, using panelRef) only checks equality with first/last elements
and fails when document.activeElement is outside the focusable list (e.g., the
dialog container with tabIndex=-1); update onKey so that after building
focusable (from panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a5b2643-fbc9-4186-9857-602f32e7c140

📥 Commits

Reviewing files that changed from the base of the PR and between 50bb633 and 1a34b4f.

📒 Files selected for processing (15)
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/page.tsx
  • frontend/src/components/AchievementUnlockToast.tsx
  • frontend/src/components/ChatPanel.tsx
  • frontend/src/components/FunctionPlot.tsx
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/ManageCoursesModal.tsx
  • frontend/src/components/MermaidBlock.tsx
  • frontend/src/components/ModelToggle.tsx
  • frontend/src/components/SignInModal.tsx
  • frontend/src/components/flashcards/ParsedCardsTable.tsx
  • frontend/src/components/screens/Calendar.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Social.tsx
💤 Files with no reviewable changes (2)
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/app/(shell)/notetaker/page.tsx

Comment on lines +648 to +655
<ul style={SR_ONLY} aria-label="Knowledge graph nodes">
{nodes.map((n) =>
onNodeClick ? (
<li key={n.id}>
<button type="button" onClick={() => onNodeClick(n)}>
{n.name}
</button>
</li>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid focusable controls inside the visually-hidden node list.

At Line 650, the SR-only list renders hidden <button> elements. Those controls can receive keyboard focus while remaining visually clipped, which breaks visible focus expectations and can strand keyboard users. Keep this list non-interactive, or add a focus style that makes focused items visible.

Suggested fix
- <ul style={SR_ONLY} aria-label="Knowledge graph nodes">- {nodes.map((n) =>- onNodeClick ? (- <li key={n.id}>- <button type="button" onClick={() => onNodeClick(n)}>- {n.name}- </button>- </li>- ) : (- <li key={n.id}>{n.name}</li>- ),- )}- </ul>+ <ul style={SR_ONLY} aria-label="Knowledge graph nodes">+ {nodes.map((n) => (+ <li key={n.id}>{n.name}</li>+ ))}+ </ul>
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>
onNodeClick ? (
<likey={n.id}>
<buttontype="button"onClick={()=>onNodeClick(n)}>
{n.name}
</button>
</li>
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>(
<likey={n.id}>{n.name}</li>
))}
</ul>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/KnowledgeGraph2D.tsx` around lines 648 - 655, The
SR_ONLY hidden node list in KnowledgeGraph2D.tsx currently renders focusable
<button> elements inside nodes.map when onNodeClick is provided (symbols:
SR_ONLY, nodes.map, onNodeClick), which can receive keyboard focus while
visually hidden; change the hidden list to be non-interactive by replacing the
focusable controls with plain, non-focusable elements (e.g., render a <span> or
similar static element for each node) or remove onClick handlers from the
SR_ONLY list so no element inside the list can receive focus, and if you must
keep interactive behavior move the clickable <button> out of the SR_ONLY
container or explicitly set tabIndex={-1} and aria-hidden to prevent focus but
prefer the non-interactive approach.

Comment on lines +79 to +93
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") { close(); return; }
if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Harden Tab trap when focus is not on boundary elements.

On Line 91–Line 92, wrapping only handles active === first/last. If focus lands on the dialog container (tabIndex={-1}) or otherwise outside focusable, Shift+Tab can escape the modal. Force-focus first/last whenever active is outside the trapped set.

Suggested patch
 if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
- if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }- else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }+ const inTrap = !!active && focusable.includes(active);+ if (!inTrap) {+ e.preventDefault();+ (e.shiftKey ? last : first).focus();+ } else if (e.shiftKey && active === first) {+ e.preventDefault(); last.focus();+ } else if (!e.shiftKey && active === last) {+ e.preventDefault(); first.focus();+ }
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
if(e.shiftKey&&active===first){e.preventDefault();last.focus();}
elseif(!e.shiftKey&&active===last){e.preventDefault();first.focus();}
}
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
constinTrap=!!active&&focusable.includes(active);
if(!inTrap){
e.preventDefault();
(e.shiftKey ? last : first).focus();
}elseif(e.shiftKey&&active===first){
e.preventDefault();last.focus();
}elseif(!e.shiftKey&&active===last){
e.preventDefault();first.focus();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/SignInModal.tsx` around lines 79 - 93, The onKey
handler for the SignInModal’s focus trap (function onKey, using panelRef) only
checks equality with first/last elements and fails when document.activeElement
is outside the focusable list (e.g., the dialog container with tabIndex=-1);
update onKey so that after building focusable (from
panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit cb1265e into mainJun 12, 2026
5 of 6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the a11y/wave2-phase2 branch June 12, 2026 14:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108) - #214

Merged
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2
Jun 12, 2026
Merged

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108)#214
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 12, 2026

Copy link
Copy Markdown
Member

Wave 2 — Phase 2: accessibility (WCAG AA)

Two scoped, single-issue commits. Each verified by a fresh-context scope check; builds green (tsc + eslint + vitest 37/37). Not for merge yet — awaiting spot-check of the contrast list.

#107 — design-token contrast (WCAG 1.4.3)

Darken four shared tokens/controls that measured below 4.5:1 (ratios computed with the WCAG relative-luminance formula):

Token / controlBeforeBefore ratioAfterAfter ratio
--text-muted (~224 uses)#8a83723.4–3.8:1#6f68575.0–5.5:1
.btn--primary bg (white label)--accent#8a9a5b3.06:1--brand-forest#1B6C426.37:1
.chip--accent text--accent on --accent-soft2.80:1--sap-600#3a6a2c5.84:1
ModelToggle "Fast" label#3B82F62.87:1#1D4ED85.23:1
ModelToggle "Smart" label#8A63D23.30:1#6D28D95.36:1

Also re-verified the #112 state cues: error ! = 4.70:1 (text); rarity status dot ≥3:1 for all five tiers (non-text). Pre-existing/out-of-scope finding flagged: the rarity text label (common/uncommon/legendary) is 3.2–4.2:1.

#108 — focus + a11y labels (2.4.7 / 4.1.2 / 2.4.3 / 1.1.1)

  • Removed inline outline:"none" on 13 inputs/textareas → global :focus-visible ring restored.
  • aria-labels on the ChatPanel textarea, ManageCourses search, Calendar checkboxes.
  • SignInModal: focus-in + Tab-trap + restore (Dialog pattern in place).
  • role="img"+label on FunctionPlot / MermaidBlock; KnowledgeGraph2D gets a label + visually-hidden node list; landing logo <div onClick><button>.

Summary by CodeRabbit

  • Accessibility Improvements

    • Restored visible focus indicators on form inputs for keyboard navigation.
    • Added descriptive labels for screen reader users across interactive elements.
    • Enhanced keyboard navigation with improved focus management in modals.
    • Added accessibility descriptions for charts and diagrams.
  • Style Updates

    • Refined color values and visual styling for buttons and UI components.

Darken four contrast failures that measured below the 4.5:1 AA minimum
for normal text:
- --text-muted #8a8372 (~224 uses) -> #6f6857: now 5.0-5.5:1 on
white / --bg / --bg-inset (was 3.4-3.8:1).
- .btn--primary background --accent #8a9a5b (white label 3.06:1) ->
--brand-forest #1B6C42 (white label 6.37:1), tying the primary
action to the established brand green.
- .chip--accent text --accent on --accent-soft (2.80:1) -> --sap-600
(5.84:1).
- ModelToggle active labels: Fast #3B82F6 (2.87:1) -> #1D4ED8 (5.23:1),
Smart #8A63D2 (3.30:1) -> #6D28D9 (5.36:1), on their soft pills.
Ratios computed with the WCAG relative-luminance formula. The new #112
state cues were verified too: the error "!" is 4.70:1 (--err on
--err-soft) and the rarity status dot clears the 3:1 non-text bar for
all five tiers.
#108)
Focus visibility (WCAG 2.4.7): remove the inline outline:"none" on the
13 inputs/textareas (ChatPanel, Social, notetaker x6, Learn x2,
ParsedCardsTable x2, page.tsx) so each falls back to the existing global
:focus-visible accent ring instead of having no focus indicator.
Accessible names (4.1.2 / 3.3.2): aria-label on the ChatPanel tutor
textarea ("Message"), the ManageCoursesModal search input, and the
Calendar select-all + per-row checkboxes.
Modal focus (2.4.3): SignInModal now moves focus into the panel on open,
traps Tab within it, and restores focus to the trigger on close. Applied
the Dialog.tsx pattern in place (rather than reparenting into Dialog) to
preserve SignInModal's custom panel and close animation.
Non-text content (1.1.1): role="img" + a source-derived aria-label on
FunctionPlot and MermaidBlock; KnowledgeGraph2D gains role="img"/label
plus a visually-hidden node <ul> (mirroring KnowledgeGraph3D) so the
pointer-only graph is keyboard/AT navigable; the landing logo
<div onClick> becomes a <button> with its icon marked decorative.
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend1a34b4fJun 12 2026, 02:30 PM

@coderabbitai

coderabbitaiBot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR improves application accessibility and visual consistency across the frontend. It restores browser focus outlines on form inputs, adds descriptive labels and semantic roles to interactive elements and visualizations, implements keyboard focus management in the sign-in modal, and updates design system tokens for better contrast compliance and visual harmony.

Changes

Accessibility and Design System Refinement

Layer / File(s)Summary
Focus outline restoration for form inputs
frontend/src/app/(shell)/notetaker/page.tsx, frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/flashcards/ParsedCardsTable.tsx, frontend/src/components/screens/Learn.tsx, frontend/src/components/screens/Social.tsx
Removes outline: "none" from multiple input and textarea elements across the application, restoring browser default focus indicators while preserving borders, backgrounds, and colors.
Semantic and accessible input controls
frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/ManageCoursesModal.tsx, frontend/src/components/screens/Calendar.tsx
Adds aria-label attributes to unlabeled inputs and checkboxes; converts navbar brand from div to button with proper labeling; improves checkbox accessibility in the assignments table.
Visual accessibility for complex components
frontend/src/components/FunctionPlot.tsx, frontend/src/components/MermaidBlock.tsx, frontend/src/components/KnowledgeGraph2D.tsx
Marks data visualization components (function plots, Mermaid diagrams, knowledge graphs) with role="img" and normalized aria-label attributes. Adds screen-reader-only list of graph nodes and reusable SR_ONLY style constant.
Modal keyboard and focus management
frontend/src/components/SignInModal.tsx
Implements keyboard accessibility by adding focus trap logic that contains Tab/Shift+Tab navigation within the modal, closes on Escape, and manages focus restoration during open/close lifecycle.
Design system token and color updates
frontend/src/app/globals.css, frontend/src/components/AchievementUnlockToast.tsx, frontend/src/components/ModelToggle.tsx
Updates CSS custom properties and component colors: --text-muted darkened for WCAG AA compliance, .chip--accent text color changed to --sap-600, .btn--primary background switched to --brand-forest with brightness hover effect, badge and option colors aligned with theme system.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit hops through focus rings so bright,
Accessible labels shine with WCAG might,
Modals trap focus, buttons emerge from divs,
Design tokens harmonize—accessibility thrives!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and specifically references the two main focuses of the PR—accessibility improvements for contrast (#107) and focus/labels (#108)—accurately summarizing the primary changes.
Description check✅ PassedThe description comprehensively covers changes made, related issues (#107, #108, #112), testing status (builds green), and detailed technical notes including WCAG ratios and implementation details for reviewers.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch a11y/wave2-phase2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/KnowledgeGraph2D.tsx`:
- Around line 648-655: The SR_ONLY hidden node list in KnowledgeGraph2D.tsx
currently renders focusable <button> elements inside nodes.map when onNodeClick
is provided (symbols: SR_ONLY, nodes.map, onNodeClick), which can receive
keyboard focus while visually hidden; change the hidden list to be
non-interactive by replacing the focusable controls with plain, non-focusable
elements (e.g., render a <span> or similar static element for each node) or
remove onClick handlers from the SR_ONLY list so no element inside the list can
receive focus, and if you must keep interactive behavior move the clickable
<button> out of the SR_ONLY container or explicitly set tabIndex={-1} and
aria-hidden to prevent focus but prefer the non-interactive approach.
In `@frontend/src/components/SignInModal.tsx`:
- Around line 79-93: The onKey handler for the SignInModal’s focus trap
(function onKey, using panelRef) only checks equality with first/last elements
and fails when document.activeElement is outside the focusable list (e.g., the
dialog container with tabIndex=-1); update onKey so that after building
focusable (from panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a5b2643-fbc9-4186-9857-602f32e7c140

📥 Commits

Reviewing files that changed from the base of the PR and between 50bb633 and 1a34b4f.

📒 Files selected for processing (15)
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/page.tsx
  • frontend/src/components/AchievementUnlockToast.tsx
  • frontend/src/components/ChatPanel.tsx
  • frontend/src/components/FunctionPlot.tsx
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/ManageCoursesModal.tsx
  • frontend/src/components/MermaidBlock.tsx
  • frontend/src/components/ModelToggle.tsx
  • frontend/src/components/SignInModal.tsx
  • frontend/src/components/flashcards/ParsedCardsTable.tsx
  • frontend/src/components/screens/Calendar.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Social.tsx
💤 Files with no reviewable changes (2)
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/app/(shell)/notetaker/page.tsx

Comment on lines +648 to +655
<ul style={SR_ONLY} aria-label="Knowledge graph nodes">
{nodes.map((n) =>
onNodeClick ? (
<li key={n.id}>
<button type="button" onClick={() => onNodeClick(n)}>
{n.name}
</button>
</li>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid focusable controls inside the visually-hidden node list.

At Line 650, the SR-only list renders hidden <button> elements. Those controls can receive keyboard focus while remaining visually clipped, which breaks visible focus expectations and can strand keyboard users. Keep this list non-interactive, or add a focus style that makes focused items visible.

Suggested fix
- <ul style={SR_ONLY} aria-label="Knowledge graph nodes">- {nodes.map((n) =>- onNodeClick ? (- <li key={n.id}>- <button type="button" onClick={() => onNodeClick(n)}>- {n.name}- </button>- </li>- ) : (- <li key={n.id}>{n.name}</li>- ),- )}- </ul>+ <ul style={SR_ONLY} aria-label="Knowledge graph nodes">+ {nodes.map((n) => (+ <li key={n.id}>{n.name}</li>+ ))}+ </ul>
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>
onNodeClick ? (
<likey={n.id}>
<buttontype="button"onClick={()=>onNodeClick(n)}>
{n.name}
</button>
</li>
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>(
<likey={n.id}>{n.name}</li>
))}
</ul>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/KnowledgeGraph2D.tsx` around lines 648 - 655, The
SR_ONLY hidden node list in KnowledgeGraph2D.tsx currently renders focusable
<button> elements inside nodes.map when onNodeClick is provided (symbols:
SR_ONLY, nodes.map, onNodeClick), which can receive keyboard focus while
visually hidden; change the hidden list to be non-interactive by replacing the
focusable controls with plain, non-focusable elements (e.g., render a <span> or
similar static element for each node) or remove onClick handlers from the
SR_ONLY list so no element inside the list can receive focus, and if you must
keep interactive behavior move the clickable <button> out of the SR_ONLY
container or explicitly set tabIndex={-1} and aria-hidden to prevent focus but
prefer the non-interactive approach.

Comment on lines +79 to +93
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") { close(); return; }
if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Harden Tab trap when focus is not on boundary elements.

On Line 91–Line 92, wrapping only handles active === first/last. If focus lands on the dialog container (tabIndex={-1}) or otherwise outside focusable, Shift+Tab can escape the modal. Force-focus first/last whenever active is outside the trapped set.

Suggested patch
 if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
- if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }- else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }+ const inTrap = !!active && focusable.includes(active);+ if (!inTrap) {+ e.preventDefault();+ (e.shiftKey ? last : first).focus();+ } else if (e.shiftKey && active === first) {+ e.preventDefault(); last.focus();+ } else if (!e.shiftKey && active === last) {+ e.preventDefault(); first.focus();+ }
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
if(e.shiftKey&&active===first){e.preventDefault();last.focus();}
elseif(!e.shiftKey&&active===last){e.preventDefault();first.focus();}
}
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
constinTrap=!!active&&focusable.includes(active);
if(!inTrap){
e.preventDefault();
(e.shiftKey ? last : first).focus();
}elseif(e.shiftKey&&active===first){
e.preventDefault();last.focus();
}elseif(!e.shiftKey&&active===last){
e.preventDefault();first.focus();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/SignInModal.tsx` around lines 79 - 93, The onKey
handler for the SignInModal’s focus trap (function onKey, using panelRef) only
checks equality with first/last elements and fails when document.activeElement
is outside the focusable list (e.g., the dialog container with tabIndex=-1);
update onKey so that after building focusable (from
panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit cb1265e into mainJun 12, 2026
5 of 6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the a11y/wave2-phase2 branch June 12, 2026 14:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108) - #214

Merged
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2
Jun 12, 2026
Merged

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108)#214
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 12, 2026

Copy link
Copy Markdown
Member

Wave 2 — Phase 2: accessibility (WCAG AA)

Two scoped, single-issue commits. Each verified by a fresh-context scope check; builds green (tsc + eslint + vitest 37/37). Not for merge yet — awaiting spot-check of the contrast list.

#107 — design-token contrast (WCAG 1.4.3)

Darken four shared tokens/controls that measured below 4.5:1 (ratios computed with the WCAG relative-luminance formula):

Token / controlBeforeBefore ratioAfterAfter ratio
--text-muted (~224 uses)#8a83723.4–3.8:1#6f68575.0–5.5:1
.btn--primary bg (white label)--accent#8a9a5b3.06:1--brand-forest#1B6C426.37:1
.chip--accent text--accent on --accent-soft2.80:1--sap-600#3a6a2c5.84:1
ModelToggle "Fast" label#3B82F62.87:1#1D4ED85.23:1
ModelToggle "Smart" label#8A63D23.30:1#6D28D95.36:1

Also re-verified the #112 state cues: error ! = 4.70:1 (text); rarity status dot ≥3:1 for all five tiers (non-text). Pre-existing/out-of-scope finding flagged: the rarity text label (common/uncommon/legendary) is 3.2–4.2:1.

#108 — focus + a11y labels (2.4.7 / 4.1.2 / 2.4.3 / 1.1.1)

  • Removed inline outline:"none" on 13 inputs/textareas → global :focus-visible ring restored.
  • aria-labels on the ChatPanel textarea, ManageCourses search, Calendar checkboxes.
  • SignInModal: focus-in + Tab-trap + restore (Dialog pattern in place).
  • role="img"+label on FunctionPlot / MermaidBlock; KnowledgeGraph2D gets a label + visually-hidden node list; landing logo <div onClick><button>.

Summary by CodeRabbit

  • Accessibility Improvements

    • Restored visible focus indicators on form inputs for keyboard navigation.
    • Added descriptive labels for screen reader users across interactive elements.
    • Enhanced keyboard navigation with improved focus management in modals.
    • Added accessibility descriptions for charts and diagrams.
  • Style Updates

    • Refined color values and visual styling for buttons and UI components.

Darken four contrast failures that measured below the 4.5:1 AA minimum
for normal text:
- --text-muted #8a8372 (~224 uses) -> #6f6857: now 5.0-5.5:1 on
white / --bg / --bg-inset (was 3.4-3.8:1).
- .btn--primary background --accent #8a9a5b (white label 3.06:1) ->
--brand-forest #1B6C42 (white label 6.37:1), tying the primary
action to the established brand green.
- .chip--accent text --accent on --accent-soft (2.80:1) -> --sap-600
(5.84:1).
- ModelToggle active labels: Fast #3B82F6 (2.87:1) -> #1D4ED8 (5.23:1),
Smart #8A63D2 (3.30:1) -> #6D28D9 (5.36:1), on their soft pills.
Ratios computed with the WCAG relative-luminance formula. The new #112
state cues were verified too: the error "!" is 4.70:1 (--err on
--err-soft) and the rarity status dot clears the 3:1 non-text bar for
all five tiers.
#108)
Focus visibility (WCAG 2.4.7): remove the inline outline:"none" on the
13 inputs/textareas (ChatPanel, Social, notetaker x6, Learn x2,
ParsedCardsTable x2, page.tsx) so each falls back to the existing global
:focus-visible accent ring instead of having no focus indicator.
Accessible names (4.1.2 / 3.3.2): aria-label on the ChatPanel tutor
textarea ("Message"), the ManageCoursesModal search input, and the
Calendar select-all + per-row checkboxes.
Modal focus (2.4.3): SignInModal now moves focus into the panel on open,
traps Tab within it, and restores focus to the trigger on close. Applied
the Dialog.tsx pattern in place (rather than reparenting into Dialog) to
preserve SignInModal's custom panel and close animation.
Non-text content (1.1.1): role="img" + a source-derived aria-label on
FunctionPlot and MermaidBlock; KnowledgeGraph2D gains role="img"/label
plus a visually-hidden node <ul> (mirroring KnowledgeGraph3D) so the
pointer-only graph is keyboard/AT navigable; the landing logo
<div onClick> becomes a <button> with its icon marked decorative.
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend1a34b4fJun 12 2026, 02:30 PM

@coderabbitai

coderabbitaiBot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR improves application accessibility and visual consistency across the frontend. It restores browser focus outlines on form inputs, adds descriptive labels and semantic roles to interactive elements and visualizations, implements keyboard focus management in the sign-in modal, and updates design system tokens for better contrast compliance and visual harmony.

Changes

Accessibility and Design System Refinement

Layer / File(s)Summary
Focus outline restoration for form inputs
frontend/src/app/(shell)/notetaker/page.tsx, frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/flashcards/ParsedCardsTable.tsx, frontend/src/components/screens/Learn.tsx, frontend/src/components/screens/Social.tsx
Removes outline: "none" from multiple input and textarea elements across the application, restoring browser default focus indicators while preserving borders, backgrounds, and colors.
Semantic and accessible input controls
frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/ManageCoursesModal.tsx, frontend/src/components/screens/Calendar.tsx
Adds aria-label attributes to unlabeled inputs and checkboxes; converts navbar brand from div to button with proper labeling; improves checkbox accessibility in the assignments table.
Visual accessibility for complex components
frontend/src/components/FunctionPlot.tsx, frontend/src/components/MermaidBlock.tsx, frontend/src/components/KnowledgeGraph2D.tsx
Marks data visualization components (function plots, Mermaid diagrams, knowledge graphs) with role="img" and normalized aria-label attributes. Adds screen-reader-only list of graph nodes and reusable SR_ONLY style constant.
Modal keyboard and focus management
frontend/src/components/SignInModal.tsx
Implements keyboard accessibility by adding focus trap logic that contains Tab/Shift+Tab navigation within the modal, closes on Escape, and manages focus restoration during open/close lifecycle.
Design system token and color updates
frontend/src/app/globals.css, frontend/src/components/AchievementUnlockToast.tsx, frontend/src/components/ModelToggle.tsx
Updates CSS custom properties and component colors: --text-muted darkened for WCAG AA compliance, .chip--accent text color changed to --sap-600, .btn--primary background switched to --brand-forest with brightness hover effect, badge and option colors aligned with theme system.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit hops through focus rings so bright,
Accessible labels shine with WCAG might,
Modals trap focus, buttons emerge from divs,
Design tokens harmonize—accessibility thrives!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and specifically references the two main focuses of the PR—accessibility improvements for contrast (#107) and focus/labels (#108)—accurately summarizing the primary changes.
Description check✅ PassedThe description comprehensively covers changes made, related issues (#107, #108, #112), testing status (builds green), and detailed technical notes including WCAG ratios and implementation details for reviewers.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch a11y/wave2-phase2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/KnowledgeGraph2D.tsx`:
- Around line 648-655: The SR_ONLY hidden node list in KnowledgeGraph2D.tsx
currently renders focusable <button> elements inside nodes.map when onNodeClick
is provided (symbols: SR_ONLY, nodes.map, onNodeClick), which can receive
keyboard focus while visually hidden; change the hidden list to be
non-interactive by replacing the focusable controls with plain, non-focusable
elements (e.g., render a <span> or similar static element for each node) or
remove onClick handlers from the SR_ONLY list so no element inside the list can
receive focus, and if you must keep interactive behavior move the clickable
<button> out of the SR_ONLY container or explicitly set tabIndex={-1} and
aria-hidden to prevent focus but prefer the non-interactive approach.
In `@frontend/src/components/SignInModal.tsx`:
- Around line 79-93: The onKey handler for the SignInModal’s focus trap
(function onKey, using panelRef) only checks equality with first/last elements
and fails when document.activeElement is outside the focusable list (e.g., the
dialog container with tabIndex=-1); update onKey so that after building
focusable (from panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a5b2643-fbc9-4186-9857-602f32e7c140

📥 Commits

Reviewing files that changed from the base of the PR and between 50bb633 and 1a34b4f.

📒 Files selected for processing (15)
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/page.tsx
  • frontend/src/components/AchievementUnlockToast.tsx
  • frontend/src/components/ChatPanel.tsx
  • frontend/src/components/FunctionPlot.tsx
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/ManageCoursesModal.tsx
  • frontend/src/components/MermaidBlock.tsx
  • frontend/src/components/ModelToggle.tsx
  • frontend/src/components/SignInModal.tsx
  • frontend/src/components/flashcards/ParsedCardsTable.tsx
  • frontend/src/components/screens/Calendar.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Social.tsx
💤 Files with no reviewable changes (2)
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/app/(shell)/notetaker/page.tsx

Comment on lines +648 to +655
<ul style={SR_ONLY} aria-label="Knowledge graph nodes">
{nodes.map((n) =>
onNodeClick ? (
<li key={n.id}>
<button type="button" onClick={() => onNodeClick(n)}>
{n.name}
</button>
</li>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid focusable controls inside the visually-hidden node list.

At Line 650, the SR-only list renders hidden <button> elements. Those controls can receive keyboard focus while remaining visually clipped, which breaks visible focus expectations and can strand keyboard users. Keep this list non-interactive, or add a focus style that makes focused items visible.

Suggested fix
- <ul style={SR_ONLY} aria-label="Knowledge graph nodes">- {nodes.map((n) =>- onNodeClick ? (- <li key={n.id}>- <button type="button" onClick={() => onNodeClick(n)}>- {n.name}- </button>- </li>- ) : (- <li key={n.id}>{n.name}</li>- ),- )}- </ul>+ <ul style={SR_ONLY} aria-label="Knowledge graph nodes">+ {nodes.map((n) => (+ <li key={n.id}>{n.name}</li>+ ))}+ </ul>
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>
onNodeClick ? (
<likey={n.id}>
<buttontype="button"onClick={()=>onNodeClick(n)}>
{n.name}
</button>
</li>
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>(
<likey={n.id}>{n.name}</li>
))}
</ul>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/KnowledgeGraph2D.tsx` around lines 648 - 655, The
SR_ONLY hidden node list in KnowledgeGraph2D.tsx currently renders focusable
<button> elements inside nodes.map when onNodeClick is provided (symbols:
SR_ONLY, nodes.map, onNodeClick), which can receive keyboard focus while
visually hidden; change the hidden list to be non-interactive by replacing the
focusable controls with plain, non-focusable elements (e.g., render a <span> or
similar static element for each node) or remove onClick handlers from the
SR_ONLY list so no element inside the list can receive focus, and if you must
keep interactive behavior move the clickable <button> out of the SR_ONLY
container or explicitly set tabIndex={-1} and aria-hidden to prevent focus but
prefer the non-interactive approach.

Comment on lines +79 to +93
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") { close(); return; }
if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Harden Tab trap when focus is not on boundary elements.

On Line 91–Line 92, wrapping only handles active === first/last. If focus lands on the dialog container (tabIndex={-1}) or otherwise outside focusable, Shift+Tab can escape the modal. Force-focus first/last whenever active is outside the trapped set.

Suggested patch
 if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
- if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }- else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }+ const inTrap = !!active && focusable.includes(active);+ if (!inTrap) {+ e.preventDefault();+ (e.shiftKey ? last : first).focus();+ } else if (e.shiftKey && active === first) {+ e.preventDefault(); last.focus();+ } else if (!e.shiftKey && active === last) {+ e.preventDefault(); first.focus();+ }
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
if(e.shiftKey&&active===first){e.preventDefault();last.focus();}
elseif(!e.shiftKey&&active===last){e.preventDefault();first.focus();}
}
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
constinTrap=!!active&&focusable.includes(active);
if(!inTrap){
e.preventDefault();
(e.shiftKey ? last : first).focus();
}elseif(e.shiftKey&&active===first){
e.preventDefault();last.focus();
}elseif(!e.shiftKey&&active===last){
e.preventDefault();first.focus();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/SignInModal.tsx` around lines 79 - 93, The onKey
handler for the SignInModal’s focus trap (function onKey, using panelRef) only
checks equality with first/last elements and fails when document.activeElement
is outside the focusable list (e.g., the dialog container with tabIndex=-1);
update onKey so that after building focusable (from
panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit cb1265e into mainJun 12, 2026
5 of 6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the a11y/wave2-phase2 branch June 12, 2026 14:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108) - #214

Merged
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2
Jun 12, 2026
Merged

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108)#214
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 12, 2026

Copy link
Copy Markdown
Member

Wave 2 — Phase 2: accessibility (WCAG AA)

Two scoped, single-issue commits. Each verified by a fresh-context scope check; builds green (tsc + eslint + vitest 37/37). Not for merge yet — awaiting spot-check of the contrast list.

#107 — design-token contrast (WCAG 1.4.3)

Darken four shared tokens/controls that measured below 4.5:1 (ratios computed with the WCAG relative-luminance formula):

Token / controlBeforeBefore ratioAfterAfter ratio
--text-muted (~224 uses)#8a83723.4–3.8:1#6f68575.0–5.5:1
.btn--primary bg (white label)--accent#8a9a5b3.06:1--brand-forest#1B6C426.37:1
.chip--accent text--accent on --accent-soft2.80:1--sap-600#3a6a2c5.84:1
ModelToggle "Fast" label#3B82F62.87:1#1D4ED85.23:1
ModelToggle "Smart" label#8A63D23.30:1#6D28D95.36:1

Also re-verified the #112 state cues: error ! = 4.70:1 (text); rarity status dot ≥3:1 for all five tiers (non-text). Pre-existing/out-of-scope finding flagged: the rarity text label (common/uncommon/legendary) is 3.2–4.2:1.

#108 — focus + a11y labels (2.4.7 / 4.1.2 / 2.4.3 / 1.1.1)

  • Removed inline outline:"none" on 13 inputs/textareas → global :focus-visible ring restored.
  • aria-labels on the ChatPanel textarea, ManageCourses search, Calendar checkboxes.
  • SignInModal: focus-in + Tab-trap + restore (Dialog pattern in place).
  • role="img"+label on FunctionPlot / MermaidBlock; KnowledgeGraph2D gets a label + visually-hidden node list; landing logo <div onClick><button>.

Summary by CodeRabbit

  • Accessibility Improvements

    • Restored visible focus indicators on form inputs for keyboard navigation.
    • Added descriptive labels for screen reader users across interactive elements.
    • Enhanced keyboard navigation with improved focus management in modals.
    • Added accessibility descriptions for charts and diagrams.
  • Style Updates

    • Refined color values and visual styling for buttons and UI components.

Darken four contrast failures that measured below the 4.5:1 AA minimum
for normal text:
- --text-muted #8a8372 (~224 uses) -> #6f6857: now 5.0-5.5:1 on
white / --bg / --bg-inset (was 3.4-3.8:1).
- .btn--primary background --accent #8a9a5b (white label 3.06:1) ->
--brand-forest #1B6C42 (white label 6.37:1), tying the primary
action to the established brand green.
- .chip--accent text --accent on --accent-soft (2.80:1) -> --sap-600
(5.84:1).
- ModelToggle active labels: Fast #3B82F6 (2.87:1) -> #1D4ED8 (5.23:1),
Smart #8A63D2 (3.30:1) -> #6D28D9 (5.36:1), on their soft pills.
Ratios computed with the WCAG relative-luminance formula. The new #112
state cues were verified too: the error "!" is 4.70:1 (--err on
--err-soft) and the rarity status dot clears the 3:1 non-text bar for
all five tiers.
#108)
Focus visibility (WCAG 2.4.7): remove the inline outline:"none" on the
13 inputs/textareas (ChatPanel, Social, notetaker x6, Learn x2,
ParsedCardsTable x2, page.tsx) so each falls back to the existing global
:focus-visible accent ring instead of having no focus indicator.
Accessible names (4.1.2 / 3.3.2): aria-label on the ChatPanel tutor
textarea ("Message"), the ManageCoursesModal search input, and the
Calendar select-all + per-row checkboxes.
Modal focus (2.4.3): SignInModal now moves focus into the panel on open,
traps Tab within it, and restores focus to the trigger on close. Applied
the Dialog.tsx pattern in place (rather than reparenting into Dialog) to
preserve SignInModal's custom panel and close animation.
Non-text content (1.1.1): role="img" + a source-derived aria-label on
FunctionPlot and MermaidBlock; KnowledgeGraph2D gains role="img"/label
plus a visually-hidden node <ul> (mirroring KnowledgeGraph3D) so the
pointer-only graph is keyboard/AT navigable; the landing logo
<div onClick> becomes a <button> with its icon marked decorative.
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend1a34b4fJun 12 2026, 02:30 PM

@coderabbitai

coderabbitaiBot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR improves application accessibility and visual consistency across the frontend. It restores browser focus outlines on form inputs, adds descriptive labels and semantic roles to interactive elements and visualizations, implements keyboard focus management in the sign-in modal, and updates design system tokens for better contrast compliance and visual harmony.

Changes

Accessibility and Design System Refinement

Layer / File(s)Summary
Focus outline restoration for form inputs
frontend/src/app/(shell)/notetaker/page.tsx, frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/flashcards/ParsedCardsTable.tsx, frontend/src/components/screens/Learn.tsx, frontend/src/components/screens/Social.tsx
Removes outline: "none" from multiple input and textarea elements across the application, restoring browser default focus indicators while preserving borders, backgrounds, and colors.
Semantic and accessible input controls
frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/ManageCoursesModal.tsx, frontend/src/components/screens/Calendar.tsx
Adds aria-label attributes to unlabeled inputs and checkboxes; converts navbar brand from div to button with proper labeling; improves checkbox accessibility in the assignments table.
Visual accessibility for complex components
frontend/src/components/FunctionPlot.tsx, frontend/src/components/MermaidBlock.tsx, frontend/src/components/KnowledgeGraph2D.tsx
Marks data visualization components (function plots, Mermaid diagrams, knowledge graphs) with role="img" and normalized aria-label attributes. Adds screen-reader-only list of graph nodes and reusable SR_ONLY style constant.
Modal keyboard and focus management
frontend/src/components/SignInModal.tsx
Implements keyboard accessibility by adding focus trap logic that contains Tab/Shift+Tab navigation within the modal, closes on Escape, and manages focus restoration during open/close lifecycle.
Design system token and color updates
frontend/src/app/globals.css, frontend/src/components/AchievementUnlockToast.tsx, frontend/src/components/ModelToggle.tsx
Updates CSS custom properties and component colors: --text-muted darkened for WCAG AA compliance, .chip--accent text color changed to --sap-600, .btn--primary background switched to --brand-forest with brightness hover effect, badge and option colors aligned with theme system.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit hops through focus rings so bright,
Accessible labels shine with WCAG might,
Modals trap focus, buttons emerge from divs,
Design tokens harmonize—accessibility thrives!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and specifically references the two main focuses of the PR—accessibility improvements for contrast (#107) and focus/labels (#108)—accurately summarizing the primary changes.
Description check✅ PassedThe description comprehensively covers changes made, related issues (#107, #108, #112), testing status (builds green), and detailed technical notes including WCAG ratios and implementation details for reviewers.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch a11y/wave2-phase2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/KnowledgeGraph2D.tsx`:
- Around line 648-655: The SR_ONLY hidden node list in KnowledgeGraph2D.tsx
currently renders focusable <button> elements inside nodes.map when onNodeClick
is provided (symbols: SR_ONLY, nodes.map, onNodeClick), which can receive
keyboard focus while visually hidden; change the hidden list to be
non-interactive by replacing the focusable controls with plain, non-focusable
elements (e.g., render a <span> or similar static element for each node) or
remove onClick handlers from the SR_ONLY list so no element inside the list can
receive focus, and if you must keep interactive behavior move the clickable
<button> out of the SR_ONLY container or explicitly set tabIndex={-1} and
aria-hidden to prevent focus but prefer the non-interactive approach.
In `@frontend/src/components/SignInModal.tsx`:
- Around line 79-93: The onKey handler for the SignInModal’s focus trap
(function onKey, using panelRef) only checks equality with first/last elements
and fails when document.activeElement is outside the focusable list (e.g., the
dialog container with tabIndex=-1); update onKey so that after building
focusable (from panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a5b2643-fbc9-4186-9857-602f32e7c140

📥 Commits

Reviewing files that changed from the base of the PR and between 50bb633 and 1a34b4f.

📒 Files selected for processing (15)
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/page.tsx
  • frontend/src/components/AchievementUnlockToast.tsx
  • frontend/src/components/ChatPanel.tsx
  • frontend/src/components/FunctionPlot.tsx
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/ManageCoursesModal.tsx
  • frontend/src/components/MermaidBlock.tsx
  • frontend/src/components/ModelToggle.tsx
  • frontend/src/components/SignInModal.tsx
  • frontend/src/components/flashcards/ParsedCardsTable.tsx
  • frontend/src/components/screens/Calendar.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Social.tsx
💤 Files with no reviewable changes (2)
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/app/(shell)/notetaker/page.tsx

Comment on lines +648 to +655
<ul style={SR_ONLY} aria-label="Knowledge graph nodes">
{nodes.map((n) =>
onNodeClick ? (
<li key={n.id}>
<button type="button" onClick={() => onNodeClick(n)}>
{n.name}
</button>
</li>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid focusable controls inside the visually-hidden node list.

At Line 650, the SR-only list renders hidden <button> elements. Those controls can receive keyboard focus while remaining visually clipped, which breaks visible focus expectations and can strand keyboard users. Keep this list non-interactive, or add a focus style that makes focused items visible.

Suggested fix
- <ul style={SR_ONLY} aria-label="Knowledge graph nodes">- {nodes.map((n) =>- onNodeClick ? (- <li key={n.id}>- <button type="button" onClick={() => onNodeClick(n)}>- {n.name}- </button>- </li>- ) : (- <li key={n.id}>{n.name}</li>- ),- )}- </ul>+ <ul style={SR_ONLY} aria-label="Knowledge graph nodes">+ {nodes.map((n) => (+ <li key={n.id}>{n.name}</li>+ ))}+ </ul>
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>
onNodeClick ? (
<likey={n.id}>
<buttontype="button"onClick={()=>onNodeClick(n)}>
{n.name}
</button>
</li>
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>(
<likey={n.id}>{n.name}</li>
))}
</ul>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/KnowledgeGraph2D.tsx` around lines 648 - 655, The
SR_ONLY hidden node list in KnowledgeGraph2D.tsx currently renders focusable
<button> elements inside nodes.map when onNodeClick is provided (symbols:
SR_ONLY, nodes.map, onNodeClick), which can receive keyboard focus while
visually hidden; change the hidden list to be non-interactive by replacing the
focusable controls with plain, non-focusable elements (e.g., render a <span> or
similar static element for each node) or remove onClick handlers from the
SR_ONLY list so no element inside the list can receive focus, and if you must
keep interactive behavior move the clickable <button> out of the SR_ONLY
container or explicitly set tabIndex={-1} and aria-hidden to prevent focus but
prefer the non-interactive approach.

Comment on lines +79 to +93
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") { close(); return; }
if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Harden Tab trap when focus is not on boundary elements.

On Line 91–Line 92, wrapping only handles active === first/last. If focus lands on the dialog container (tabIndex={-1}) or otherwise outside focusable, Shift+Tab can escape the modal. Force-focus first/last whenever active is outside the trapped set.

Suggested patch
 if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
- if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }- else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }+ const inTrap = !!active && focusable.includes(active);+ if (!inTrap) {+ e.preventDefault();+ (e.shiftKey ? last : first).focus();+ } else if (e.shiftKey && active === first) {+ e.preventDefault(); last.focus();+ } else if (!e.shiftKey && active === last) {+ e.preventDefault(); first.focus();+ }
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
if(e.shiftKey&&active===first){e.preventDefault();last.focus();}
elseif(!e.shiftKey&&active===last){e.preventDefault();first.focus();}
}
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
constinTrap=!!active&&focusable.includes(active);
if(!inTrap){
e.preventDefault();
(e.shiftKey ? last : first).focus();
}elseif(e.shiftKey&&active===first){
e.preventDefault();last.focus();
}elseif(!e.shiftKey&&active===last){
e.preventDefault();first.focus();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/SignInModal.tsx` around lines 79 - 93, The onKey
handler for the SignInModal’s focus trap (function onKey, using panelRef) only
checks equality with first/last elements and fails when document.activeElement
is outside the focusable list (e.g., the dialog container with tabIndex=-1);
update onKey so that after building focusable (from
panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit cb1265e into mainJun 12, 2026
5 of 6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the a11y/wave2-phase2 branch June 12, 2026 14:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108) - #214

Merged
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2
Jun 12, 2026
Merged

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108)#214
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 12, 2026

Copy link
Copy Markdown
Member

Wave 2 — Phase 2: accessibility (WCAG AA)

Two scoped, single-issue commits. Each verified by a fresh-context scope check; builds green (tsc + eslint + vitest 37/37). Not for merge yet — awaiting spot-check of the contrast list.

#107 — design-token contrast (WCAG 1.4.3)

Darken four shared tokens/controls that measured below 4.5:1 (ratios computed with the WCAG relative-luminance formula):

Token / controlBeforeBefore ratioAfterAfter ratio
--text-muted (~224 uses)#8a83723.4–3.8:1#6f68575.0–5.5:1
.btn--primary bg (white label)--accent#8a9a5b3.06:1--brand-forest#1B6C426.37:1
.chip--accent text--accent on --accent-soft2.80:1--sap-600#3a6a2c5.84:1
ModelToggle "Fast" label#3B82F62.87:1#1D4ED85.23:1
ModelToggle "Smart" label#8A63D23.30:1#6D28D95.36:1

Also re-verified the #112 state cues: error ! = 4.70:1 (text); rarity status dot ≥3:1 for all five tiers (non-text). Pre-existing/out-of-scope finding flagged: the rarity text label (common/uncommon/legendary) is 3.2–4.2:1.

#108 — focus + a11y labels (2.4.7 / 4.1.2 / 2.4.3 / 1.1.1)

  • Removed inline outline:"none" on 13 inputs/textareas → global :focus-visible ring restored.
  • aria-labels on the ChatPanel textarea, ManageCourses search, Calendar checkboxes.
  • SignInModal: focus-in + Tab-trap + restore (Dialog pattern in place).
  • role="img"+label on FunctionPlot / MermaidBlock; KnowledgeGraph2D gets a label + visually-hidden node list; landing logo <div onClick><button>.

Summary by CodeRabbit

  • Accessibility Improvements

    • Restored visible focus indicators on form inputs for keyboard navigation.
    • Added descriptive labels for screen reader users across interactive elements.
    • Enhanced keyboard navigation with improved focus management in modals.
    • Added accessibility descriptions for charts and diagrams.
  • Style Updates

    • Refined color values and visual styling for buttons and UI components.

Darken four contrast failures that measured below the 4.5:1 AA minimum
for normal text:
- --text-muted #8a8372 (~224 uses) -> #6f6857: now 5.0-5.5:1 on
white / --bg / --bg-inset (was 3.4-3.8:1).
- .btn--primary background --accent #8a9a5b (white label 3.06:1) ->
--brand-forest #1B6C42 (white label 6.37:1), tying the primary
action to the established brand green.
- .chip--accent text --accent on --accent-soft (2.80:1) -> --sap-600
(5.84:1).
- ModelToggle active labels: Fast #3B82F6 (2.87:1) -> #1D4ED8 (5.23:1),
Smart #8A63D2 (3.30:1) -> #6D28D9 (5.36:1), on their soft pills.
Ratios computed with the WCAG relative-luminance formula. The new #112
state cues were verified too: the error "!" is 4.70:1 (--err on
--err-soft) and the rarity status dot clears the 3:1 non-text bar for
all five tiers.
#108)
Focus visibility (WCAG 2.4.7): remove the inline outline:"none" on the
13 inputs/textareas (ChatPanel, Social, notetaker x6, Learn x2,
ParsedCardsTable x2, page.tsx) so each falls back to the existing global
:focus-visible accent ring instead of having no focus indicator.
Accessible names (4.1.2 / 3.3.2): aria-label on the ChatPanel tutor
textarea ("Message"), the ManageCoursesModal search input, and the
Calendar select-all + per-row checkboxes.
Modal focus (2.4.3): SignInModal now moves focus into the panel on open,
traps Tab within it, and restores focus to the trigger on close. Applied
the Dialog.tsx pattern in place (rather than reparenting into Dialog) to
preserve SignInModal's custom panel and close animation.
Non-text content (1.1.1): role="img" + a source-derived aria-label on
FunctionPlot and MermaidBlock; KnowledgeGraph2D gains role="img"/label
plus a visually-hidden node <ul> (mirroring KnowledgeGraph3D) so the
pointer-only graph is keyboard/AT navigable; the landing logo
<div onClick> becomes a <button> with its icon marked decorative.
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend1a34b4fJun 12 2026, 02:30 PM

@coderabbitai

coderabbitaiBot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR improves application accessibility and visual consistency across the frontend. It restores browser focus outlines on form inputs, adds descriptive labels and semantic roles to interactive elements and visualizations, implements keyboard focus management in the sign-in modal, and updates design system tokens for better contrast compliance and visual harmony.

Changes

Accessibility and Design System Refinement

Layer / File(s)Summary
Focus outline restoration for form inputs
frontend/src/app/(shell)/notetaker/page.tsx, frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/flashcards/ParsedCardsTable.tsx, frontend/src/components/screens/Learn.tsx, frontend/src/components/screens/Social.tsx
Removes outline: "none" from multiple input and textarea elements across the application, restoring browser default focus indicators while preserving borders, backgrounds, and colors.
Semantic and accessible input controls
frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/ManageCoursesModal.tsx, frontend/src/components/screens/Calendar.tsx
Adds aria-label attributes to unlabeled inputs and checkboxes; converts navbar brand from div to button with proper labeling; improves checkbox accessibility in the assignments table.
Visual accessibility for complex components
frontend/src/components/FunctionPlot.tsx, frontend/src/components/MermaidBlock.tsx, frontend/src/components/KnowledgeGraph2D.tsx
Marks data visualization components (function plots, Mermaid diagrams, knowledge graphs) with role="img" and normalized aria-label attributes. Adds screen-reader-only list of graph nodes and reusable SR_ONLY style constant.
Modal keyboard and focus management
frontend/src/components/SignInModal.tsx
Implements keyboard accessibility by adding focus trap logic that contains Tab/Shift+Tab navigation within the modal, closes on Escape, and manages focus restoration during open/close lifecycle.
Design system token and color updates
frontend/src/app/globals.css, frontend/src/components/AchievementUnlockToast.tsx, frontend/src/components/ModelToggle.tsx
Updates CSS custom properties and component colors: --text-muted darkened for WCAG AA compliance, .chip--accent text color changed to --sap-600, .btn--primary background switched to --brand-forest with brightness hover effect, badge and option colors aligned with theme system.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit hops through focus rings so bright,
Accessible labels shine with WCAG might,
Modals trap focus, buttons emerge from divs,
Design tokens harmonize—accessibility thrives!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and specifically references the two main focuses of the PR—accessibility improvements for contrast (#107) and focus/labels (#108)—accurately summarizing the primary changes.
Description check✅ PassedThe description comprehensively covers changes made, related issues (#107, #108, #112), testing status (builds green), and detailed technical notes including WCAG ratios and implementation details for reviewers.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch a11y/wave2-phase2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/KnowledgeGraph2D.tsx`:
- Around line 648-655: The SR_ONLY hidden node list in KnowledgeGraph2D.tsx
currently renders focusable <button> elements inside nodes.map when onNodeClick
is provided (symbols: SR_ONLY, nodes.map, onNodeClick), which can receive
keyboard focus while visually hidden; change the hidden list to be
non-interactive by replacing the focusable controls with plain, non-focusable
elements (e.g., render a <span> or similar static element for each node) or
remove onClick handlers from the SR_ONLY list so no element inside the list can
receive focus, and if you must keep interactive behavior move the clickable
<button> out of the SR_ONLY container or explicitly set tabIndex={-1} and
aria-hidden to prevent focus but prefer the non-interactive approach.
In `@frontend/src/components/SignInModal.tsx`:
- Around line 79-93: The onKey handler for the SignInModal’s focus trap
(function onKey, using panelRef) only checks equality with first/last elements
and fails when document.activeElement is outside the focusable list (e.g., the
dialog container with tabIndex=-1); update onKey so that after building
focusable (from panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a5b2643-fbc9-4186-9857-602f32e7c140

📥 Commits

Reviewing files that changed from the base of the PR and between 50bb633 and 1a34b4f.

📒 Files selected for processing (15)
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/page.tsx
  • frontend/src/components/AchievementUnlockToast.tsx
  • frontend/src/components/ChatPanel.tsx
  • frontend/src/components/FunctionPlot.tsx
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/ManageCoursesModal.tsx
  • frontend/src/components/MermaidBlock.tsx
  • frontend/src/components/ModelToggle.tsx
  • frontend/src/components/SignInModal.tsx
  • frontend/src/components/flashcards/ParsedCardsTable.tsx
  • frontend/src/components/screens/Calendar.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Social.tsx
💤 Files with no reviewable changes (2)
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/app/(shell)/notetaker/page.tsx

Comment on lines +648 to +655
<ul style={SR_ONLY} aria-label="Knowledge graph nodes">
{nodes.map((n) =>
onNodeClick ? (
<li key={n.id}>
<button type="button" onClick={() => onNodeClick(n)}>
{n.name}
</button>
</li>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid focusable controls inside the visually-hidden node list.

At Line 650, the SR-only list renders hidden <button> elements. Those controls can receive keyboard focus while remaining visually clipped, which breaks visible focus expectations and can strand keyboard users. Keep this list non-interactive, or add a focus style that makes focused items visible.

Suggested fix
- <ul style={SR_ONLY} aria-label="Knowledge graph nodes">- {nodes.map((n) =>- onNodeClick ? (- <li key={n.id}>- <button type="button" onClick={() => onNodeClick(n)}>- {n.name}- </button>- </li>- ) : (- <li key={n.id}>{n.name}</li>- ),- )}- </ul>+ <ul style={SR_ONLY} aria-label="Knowledge graph nodes">+ {nodes.map((n) => (+ <li key={n.id}>{n.name}</li>+ ))}+ </ul>
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>
onNodeClick ? (
<likey={n.id}>
<buttontype="button"onClick={()=>onNodeClick(n)}>
{n.name}
</button>
</li>
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>(
<likey={n.id}>{n.name}</li>
))}
</ul>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/KnowledgeGraph2D.tsx` around lines 648 - 655, The
SR_ONLY hidden node list in KnowledgeGraph2D.tsx currently renders focusable
<button> elements inside nodes.map when onNodeClick is provided (symbols:
SR_ONLY, nodes.map, onNodeClick), which can receive keyboard focus while
visually hidden; change the hidden list to be non-interactive by replacing the
focusable controls with plain, non-focusable elements (e.g., render a <span> or
similar static element for each node) or remove onClick handlers from the
SR_ONLY list so no element inside the list can receive focus, and if you must
keep interactive behavior move the clickable <button> out of the SR_ONLY
container or explicitly set tabIndex={-1} and aria-hidden to prevent focus but
prefer the non-interactive approach.

Comment on lines +79 to +93
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") { close(); return; }
if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Harden Tab trap when focus is not on boundary elements.

On Line 91–Line 92, wrapping only handles active === first/last. If focus lands on the dialog container (tabIndex={-1}) or otherwise outside focusable, Shift+Tab can escape the modal. Force-focus first/last whenever active is outside the trapped set.

Suggested patch
 if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
- if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }- else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }+ const inTrap = !!active && focusable.includes(active);+ if (!inTrap) {+ e.preventDefault();+ (e.shiftKey ? last : first).focus();+ } else if (e.shiftKey && active === first) {+ e.preventDefault(); last.focus();+ } else if (!e.shiftKey && active === last) {+ e.preventDefault(); first.focus();+ }
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
if(e.shiftKey&&active===first){e.preventDefault();last.focus();}
elseif(!e.shiftKey&&active===last){e.preventDefault();first.focus();}
}
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
constinTrap=!!active&&focusable.includes(active);
if(!inTrap){
e.preventDefault();
(e.shiftKey ? last : first).focus();
}elseif(e.shiftKey&&active===first){
e.preventDefault();last.focus();
}elseif(!e.shiftKey&&active===last){
e.preventDefault();first.focus();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/SignInModal.tsx` around lines 79 - 93, The onKey
handler for the SignInModal’s focus trap (function onKey, using panelRef) only
checks equality with first/last elements and fails when document.activeElement
is outside the focusable list (e.g., the dialog container with tabIndex=-1);
update onKey so that after building focusable (from
panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit cb1265e into mainJun 12, 2026
5 of 6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the a11y/wave2-phase2 branch June 12, 2026 14:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108) - #214

Merged
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2
Jun 12, 2026
Merged

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108)#214
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 12, 2026

Copy link
Copy Markdown
Member

Wave 2 — Phase 2: accessibility (WCAG AA)

Two scoped, single-issue commits. Each verified by a fresh-context scope check; builds green (tsc + eslint + vitest 37/37). Not for merge yet — awaiting spot-check of the contrast list.

#107 — design-token contrast (WCAG 1.4.3)

Darken four shared tokens/controls that measured below 4.5:1 (ratios computed with the WCAG relative-luminance formula):

Token / controlBeforeBefore ratioAfterAfter ratio
--text-muted (~224 uses)#8a83723.4–3.8:1#6f68575.0–5.5:1
.btn--primary bg (white label)--accent#8a9a5b3.06:1--brand-forest#1B6C426.37:1
.chip--accent text--accent on --accent-soft2.80:1--sap-600#3a6a2c5.84:1
ModelToggle "Fast" label#3B82F62.87:1#1D4ED85.23:1
ModelToggle "Smart" label#8A63D23.30:1#6D28D95.36:1

Also re-verified the #112 state cues: error ! = 4.70:1 (text); rarity status dot ≥3:1 for all five tiers (non-text). Pre-existing/out-of-scope finding flagged: the rarity text label (common/uncommon/legendary) is 3.2–4.2:1.

#108 — focus + a11y labels (2.4.7 / 4.1.2 / 2.4.3 / 1.1.1)

  • Removed inline outline:"none" on 13 inputs/textareas → global :focus-visible ring restored.
  • aria-labels on the ChatPanel textarea, ManageCourses search, Calendar checkboxes.
  • SignInModal: focus-in + Tab-trap + restore (Dialog pattern in place).
  • role="img"+label on FunctionPlot / MermaidBlock; KnowledgeGraph2D gets a label + visually-hidden node list; landing logo <div onClick><button>.

Summary by CodeRabbit

  • Accessibility Improvements

    • Restored visible focus indicators on form inputs for keyboard navigation.
    • Added descriptive labels for screen reader users across interactive elements.
    • Enhanced keyboard navigation with improved focus management in modals.
    • Added accessibility descriptions for charts and diagrams.
  • Style Updates

    • Refined color values and visual styling for buttons and UI components.

Darken four contrast failures that measured below the 4.5:1 AA minimum
for normal text:
- --text-muted #8a8372 (~224 uses) -> #6f6857: now 5.0-5.5:1 on
white / --bg / --bg-inset (was 3.4-3.8:1).
- .btn--primary background --accent #8a9a5b (white label 3.06:1) ->
--brand-forest #1B6C42 (white label 6.37:1), tying the primary
action to the established brand green.
- .chip--accent text --accent on --accent-soft (2.80:1) -> --sap-600
(5.84:1).
- ModelToggle active labels: Fast #3B82F6 (2.87:1) -> #1D4ED8 (5.23:1),
Smart #8A63D2 (3.30:1) -> #6D28D9 (5.36:1), on their soft pills.
Ratios computed with the WCAG relative-luminance formula. The new #112
state cues were verified too: the error "!" is 4.70:1 (--err on
--err-soft) and the rarity status dot clears the 3:1 non-text bar for
all five tiers.
#108)
Focus visibility (WCAG 2.4.7): remove the inline outline:"none" on the
13 inputs/textareas (ChatPanel, Social, notetaker x6, Learn x2,
ParsedCardsTable x2, page.tsx) so each falls back to the existing global
:focus-visible accent ring instead of having no focus indicator.
Accessible names (4.1.2 / 3.3.2): aria-label on the ChatPanel tutor
textarea ("Message"), the ManageCoursesModal search input, and the
Calendar select-all + per-row checkboxes.
Modal focus (2.4.3): SignInModal now moves focus into the panel on open,
traps Tab within it, and restores focus to the trigger on close. Applied
the Dialog.tsx pattern in place (rather than reparenting into Dialog) to
preserve SignInModal's custom panel and close animation.
Non-text content (1.1.1): role="img" + a source-derived aria-label on
FunctionPlot and MermaidBlock; KnowledgeGraph2D gains role="img"/label
plus a visually-hidden node <ul> (mirroring KnowledgeGraph3D) so the
pointer-only graph is keyboard/AT navigable; the landing logo
<div onClick> becomes a <button> with its icon marked decorative.
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend1a34b4fJun 12 2026, 02:30 PM

@coderabbitai

coderabbitaiBot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR improves application accessibility and visual consistency across the frontend. It restores browser focus outlines on form inputs, adds descriptive labels and semantic roles to interactive elements and visualizations, implements keyboard focus management in the sign-in modal, and updates design system tokens for better contrast compliance and visual harmony.

Changes

Accessibility and Design System Refinement

Layer / File(s)Summary
Focus outline restoration for form inputs
frontend/src/app/(shell)/notetaker/page.tsx, frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/flashcards/ParsedCardsTable.tsx, frontend/src/components/screens/Learn.tsx, frontend/src/components/screens/Social.tsx
Removes outline: "none" from multiple input and textarea elements across the application, restoring browser default focus indicators while preserving borders, backgrounds, and colors.
Semantic and accessible input controls
frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/ManageCoursesModal.tsx, frontend/src/components/screens/Calendar.tsx
Adds aria-label attributes to unlabeled inputs and checkboxes; converts navbar brand from div to button with proper labeling; improves checkbox accessibility in the assignments table.
Visual accessibility for complex components
frontend/src/components/FunctionPlot.tsx, frontend/src/components/MermaidBlock.tsx, frontend/src/components/KnowledgeGraph2D.tsx
Marks data visualization components (function plots, Mermaid diagrams, knowledge graphs) with role="img" and normalized aria-label attributes. Adds screen-reader-only list of graph nodes and reusable SR_ONLY style constant.
Modal keyboard and focus management
frontend/src/components/SignInModal.tsx
Implements keyboard accessibility by adding focus trap logic that contains Tab/Shift+Tab navigation within the modal, closes on Escape, and manages focus restoration during open/close lifecycle.
Design system token and color updates
frontend/src/app/globals.css, frontend/src/components/AchievementUnlockToast.tsx, frontend/src/components/ModelToggle.tsx
Updates CSS custom properties and component colors: --text-muted darkened for WCAG AA compliance, .chip--accent text color changed to --sap-600, .btn--primary background switched to --brand-forest with brightness hover effect, badge and option colors aligned with theme system.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit hops through focus rings so bright,
Accessible labels shine with WCAG might,
Modals trap focus, buttons emerge from divs,
Design tokens harmonize—accessibility thrives!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and specifically references the two main focuses of the PR—accessibility improvements for contrast (#107) and focus/labels (#108)—accurately summarizing the primary changes.
Description check✅ PassedThe description comprehensively covers changes made, related issues (#107, #108, #112), testing status (builds green), and detailed technical notes including WCAG ratios and implementation details for reviewers.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch a11y/wave2-phase2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/KnowledgeGraph2D.tsx`:
- Around line 648-655: The SR_ONLY hidden node list in KnowledgeGraph2D.tsx
currently renders focusable <button> elements inside nodes.map when onNodeClick
is provided (symbols: SR_ONLY, nodes.map, onNodeClick), which can receive
keyboard focus while visually hidden; change the hidden list to be
non-interactive by replacing the focusable controls with plain, non-focusable
elements (e.g., render a <span> or similar static element for each node) or
remove onClick handlers from the SR_ONLY list so no element inside the list can
receive focus, and if you must keep interactive behavior move the clickable
<button> out of the SR_ONLY container or explicitly set tabIndex={-1} and
aria-hidden to prevent focus but prefer the non-interactive approach.
In `@frontend/src/components/SignInModal.tsx`:
- Around line 79-93: The onKey handler for the SignInModal’s focus trap
(function onKey, using panelRef) only checks equality with first/last elements
and fails when document.activeElement is outside the focusable list (e.g., the
dialog container with tabIndex=-1); update onKey so that after building
focusable (from panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a5b2643-fbc9-4186-9857-602f32e7c140

📥 Commits

Reviewing files that changed from the base of the PR and between 50bb633 and 1a34b4f.

📒 Files selected for processing (15)
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/page.tsx
  • frontend/src/components/AchievementUnlockToast.tsx
  • frontend/src/components/ChatPanel.tsx
  • frontend/src/components/FunctionPlot.tsx
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/ManageCoursesModal.tsx
  • frontend/src/components/MermaidBlock.tsx
  • frontend/src/components/ModelToggle.tsx
  • frontend/src/components/SignInModal.tsx
  • frontend/src/components/flashcards/ParsedCardsTable.tsx
  • frontend/src/components/screens/Calendar.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Social.tsx
💤 Files with no reviewable changes (2)
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/app/(shell)/notetaker/page.tsx

Comment on lines +648 to +655
<ul style={SR_ONLY} aria-label="Knowledge graph nodes">
{nodes.map((n) =>
onNodeClick ? (
<li key={n.id}>
<button type="button" onClick={() => onNodeClick(n)}>
{n.name}
</button>
</li>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid focusable controls inside the visually-hidden node list.

At Line 650, the SR-only list renders hidden <button> elements. Those controls can receive keyboard focus while remaining visually clipped, which breaks visible focus expectations and can strand keyboard users. Keep this list non-interactive, or add a focus style that makes focused items visible.

Suggested fix
- <ul style={SR_ONLY} aria-label="Knowledge graph nodes">- {nodes.map((n) =>- onNodeClick ? (- <li key={n.id}>- <button type="button" onClick={() => onNodeClick(n)}>- {n.name}- </button>- </li>- ) : (- <li key={n.id}>{n.name}</li>- ),- )}- </ul>+ <ul style={SR_ONLY} aria-label="Knowledge graph nodes">+ {nodes.map((n) => (+ <li key={n.id}>{n.name}</li>+ ))}+ </ul>
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>
onNodeClick ? (
<likey={n.id}>
<buttontype="button"onClick={()=>onNodeClick(n)}>
{n.name}
</button>
</li>
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>(
<likey={n.id}>{n.name}</li>
))}
</ul>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/KnowledgeGraph2D.tsx` around lines 648 - 655, The
SR_ONLY hidden node list in KnowledgeGraph2D.tsx currently renders focusable
<button> elements inside nodes.map when onNodeClick is provided (symbols:
SR_ONLY, nodes.map, onNodeClick), which can receive keyboard focus while
visually hidden; change the hidden list to be non-interactive by replacing the
focusable controls with plain, non-focusable elements (e.g., render a <span> or
similar static element for each node) or remove onClick handlers from the
SR_ONLY list so no element inside the list can receive focus, and if you must
keep interactive behavior move the clickable <button> out of the SR_ONLY
container or explicitly set tabIndex={-1} and aria-hidden to prevent focus but
prefer the non-interactive approach.

Comment on lines +79 to +93
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") { close(); return; }
if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Harden Tab trap when focus is not on boundary elements.

On Line 91–Line 92, wrapping only handles active === first/last. If focus lands on the dialog container (tabIndex={-1}) or otherwise outside focusable, Shift+Tab can escape the modal. Force-focus first/last whenever active is outside the trapped set.

Suggested patch
 if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
- if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }- else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }+ const inTrap = !!active && focusable.includes(active);+ if (!inTrap) {+ e.preventDefault();+ (e.shiftKey ? last : first).focus();+ } else if (e.shiftKey && active === first) {+ e.preventDefault(); last.focus();+ } else if (!e.shiftKey && active === last) {+ e.preventDefault(); first.focus();+ }
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
if(e.shiftKey&&active===first){e.preventDefault();last.focus();}
elseif(!e.shiftKey&&active===last){e.preventDefault();first.focus();}
}
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
constinTrap=!!active&&focusable.includes(active);
if(!inTrap){
e.preventDefault();
(e.shiftKey ? last : first).focus();
}elseif(e.shiftKey&&active===first){
e.preventDefault();last.focus();
}elseif(!e.shiftKey&&active===last){
e.preventDefault();first.focus();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/SignInModal.tsx` around lines 79 - 93, The onKey
handler for the SignInModal’s focus trap (function onKey, using panelRef) only
checks equality with first/last elements and fails when document.activeElement
is outside the focusable list (e.g., the dialog container with tabIndex=-1);
update onKey so that after building focusable (from
panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit cb1265e into mainJun 12, 2026
5 of 6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the a11y/wave2-phase2 branch June 12, 2026 14:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108) - #214

Merged
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2
Jun 12, 2026
Merged

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108)#214
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 12, 2026

Copy link
Copy Markdown
Member

Wave 2 — Phase 2: accessibility (WCAG AA)

Two scoped, single-issue commits. Each verified by a fresh-context scope check; builds green (tsc + eslint + vitest 37/37). Not for merge yet — awaiting spot-check of the contrast list.

#107 — design-token contrast (WCAG 1.4.3)

Darken four shared tokens/controls that measured below 4.5:1 (ratios computed with the WCAG relative-luminance formula):

Token / controlBeforeBefore ratioAfterAfter ratio
--text-muted (~224 uses)#8a83723.4–3.8:1#6f68575.0–5.5:1
.btn--primary bg (white label)--accent#8a9a5b3.06:1--brand-forest#1B6C426.37:1
.chip--accent text--accent on --accent-soft2.80:1--sap-600#3a6a2c5.84:1
ModelToggle "Fast" label#3B82F62.87:1#1D4ED85.23:1
ModelToggle "Smart" label#8A63D23.30:1#6D28D95.36:1

Also re-verified the #112 state cues: error ! = 4.70:1 (text); rarity status dot ≥3:1 for all five tiers (non-text). Pre-existing/out-of-scope finding flagged: the rarity text label (common/uncommon/legendary) is 3.2–4.2:1.

#108 — focus + a11y labels (2.4.7 / 4.1.2 / 2.4.3 / 1.1.1)

  • Removed inline outline:"none" on 13 inputs/textareas → global :focus-visible ring restored.
  • aria-labels on the ChatPanel textarea, ManageCourses search, Calendar checkboxes.
  • SignInModal: focus-in + Tab-trap + restore (Dialog pattern in place).
  • role="img"+label on FunctionPlot / MermaidBlock; KnowledgeGraph2D gets a label + visually-hidden node list; landing logo <div onClick><button>.

Summary by CodeRabbit

  • Accessibility Improvements

    • Restored visible focus indicators on form inputs for keyboard navigation.
    • Added descriptive labels for screen reader users across interactive elements.
    • Enhanced keyboard navigation with improved focus management in modals.
    • Added accessibility descriptions for charts and diagrams.
  • Style Updates

    • Refined color values and visual styling for buttons and UI components.

Darken four contrast failures that measured below the 4.5:1 AA minimum
for normal text:
- --text-muted #8a8372 (~224 uses) -> #6f6857: now 5.0-5.5:1 on
white / --bg / --bg-inset (was 3.4-3.8:1).
- .btn--primary background --accent #8a9a5b (white label 3.06:1) ->
--brand-forest #1B6C42 (white label 6.37:1), tying the primary
action to the established brand green.
- .chip--accent text --accent on --accent-soft (2.80:1) -> --sap-600
(5.84:1).
- ModelToggle active labels: Fast #3B82F6 (2.87:1) -> #1D4ED8 (5.23:1),
Smart #8A63D2 (3.30:1) -> #6D28D9 (5.36:1), on their soft pills.
Ratios computed with the WCAG relative-luminance formula. The new #112
state cues were verified too: the error "!" is 4.70:1 (--err on
--err-soft) and the rarity status dot clears the 3:1 non-text bar for
all five tiers.
#108)
Focus visibility (WCAG 2.4.7): remove the inline outline:"none" on the
13 inputs/textareas (ChatPanel, Social, notetaker x6, Learn x2,
ParsedCardsTable x2, page.tsx) so each falls back to the existing global
:focus-visible accent ring instead of having no focus indicator.
Accessible names (4.1.2 / 3.3.2): aria-label on the ChatPanel tutor
textarea ("Message"), the ManageCoursesModal search input, and the
Calendar select-all + per-row checkboxes.
Modal focus (2.4.3): SignInModal now moves focus into the panel on open,
traps Tab within it, and restores focus to the trigger on close. Applied
the Dialog.tsx pattern in place (rather than reparenting into Dialog) to
preserve SignInModal's custom panel and close animation.
Non-text content (1.1.1): role="img" + a source-derived aria-label on
FunctionPlot and MermaidBlock; KnowledgeGraph2D gains role="img"/label
plus a visually-hidden node <ul> (mirroring KnowledgeGraph3D) so the
pointer-only graph is keyboard/AT navigable; the landing logo
<div onClick> becomes a <button> with its icon marked decorative.
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend1a34b4fJun 12 2026, 02:30 PM

@coderabbitai

coderabbitaiBot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR improves application accessibility and visual consistency across the frontend. It restores browser focus outlines on form inputs, adds descriptive labels and semantic roles to interactive elements and visualizations, implements keyboard focus management in the sign-in modal, and updates design system tokens for better contrast compliance and visual harmony.

Changes

Accessibility and Design System Refinement

Layer / File(s)Summary
Focus outline restoration for form inputs
frontend/src/app/(shell)/notetaker/page.tsx, frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/flashcards/ParsedCardsTable.tsx, frontend/src/components/screens/Learn.tsx, frontend/src/components/screens/Social.tsx
Removes outline: "none" from multiple input and textarea elements across the application, restoring browser default focus indicators while preserving borders, backgrounds, and colors.
Semantic and accessible input controls
frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/ManageCoursesModal.tsx, frontend/src/components/screens/Calendar.tsx
Adds aria-label attributes to unlabeled inputs and checkboxes; converts navbar brand from div to button with proper labeling; improves checkbox accessibility in the assignments table.
Visual accessibility for complex components
frontend/src/components/FunctionPlot.tsx, frontend/src/components/MermaidBlock.tsx, frontend/src/components/KnowledgeGraph2D.tsx
Marks data visualization components (function plots, Mermaid diagrams, knowledge graphs) with role="img" and normalized aria-label attributes. Adds screen-reader-only list of graph nodes and reusable SR_ONLY style constant.
Modal keyboard and focus management
frontend/src/components/SignInModal.tsx
Implements keyboard accessibility by adding focus trap logic that contains Tab/Shift+Tab navigation within the modal, closes on Escape, and manages focus restoration during open/close lifecycle.
Design system token and color updates
frontend/src/app/globals.css, frontend/src/components/AchievementUnlockToast.tsx, frontend/src/components/ModelToggle.tsx
Updates CSS custom properties and component colors: --text-muted darkened for WCAG AA compliance, .chip--accent text color changed to --sap-600, .btn--primary background switched to --brand-forest with brightness hover effect, badge and option colors aligned with theme system.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit hops through focus rings so bright,
Accessible labels shine with WCAG might,
Modals trap focus, buttons emerge from divs,
Design tokens harmonize—accessibility thrives!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and specifically references the two main focuses of the PR—accessibility improvements for contrast (#107) and focus/labels (#108)—accurately summarizing the primary changes.
Description check✅ PassedThe description comprehensively covers changes made, related issues (#107, #108, #112), testing status (builds green), and detailed technical notes including WCAG ratios and implementation details for reviewers.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch a11y/wave2-phase2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/KnowledgeGraph2D.tsx`:
- Around line 648-655: The SR_ONLY hidden node list in KnowledgeGraph2D.tsx
currently renders focusable <button> elements inside nodes.map when onNodeClick
is provided (symbols: SR_ONLY, nodes.map, onNodeClick), which can receive
keyboard focus while visually hidden; change the hidden list to be
non-interactive by replacing the focusable controls with plain, non-focusable
elements (e.g., render a <span> or similar static element for each node) or
remove onClick handlers from the SR_ONLY list so no element inside the list can
receive focus, and if you must keep interactive behavior move the clickable
<button> out of the SR_ONLY container or explicitly set tabIndex={-1} and
aria-hidden to prevent focus but prefer the non-interactive approach.
In `@frontend/src/components/SignInModal.tsx`:
- Around line 79-93: The onKey handler for the SignInModal’s focus trap
(function onKey, using panelRef) only checks equality with first/last elements
and fails when document.activeElement is outside the focusable list (e.g., the
dialog container with tabIndex=-1); update onKey so that after building
focusable (from panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a5b2643-fbc9-4186-9857-602f32e7c140

📥 Commits

Reviewing files that changed from the base of the PR and between 50bb633 and 1a34b4f.

📒 Files selected for processing (15)
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/page.tsx
  • frontend/src/components/AchievementUnlockToast.tsx
  • frontend/src/components/ChatPanel.tsx
  • frontend/src/components/FunctionPlot.tsx
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/ManageCoursesModal.tsx
  • frontend/src/components/MermaidBlock.tsx
  • frontend/src/components/ModelToggle.tsx
  • frontend/src/components/SignInModal.tsx
  • frontend/src/components/flashcards/ParsedCardsTable.tsx
  • frontend/src/components/screens/Calendar.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Social.tsx
💤 Files with no reviewable changes (2)
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/app/(shell)/notetaker/page.tsx

Comment on lines +648 to +655
<ul style={SR_ONLY} aria-label="Knowledge graph nodes">
{nodes.map((n) =>
onNodeClick ? (
<li key={n.id}>
<button type="button" onClick={() => onNodeClick(n)}>
{n.name}
</button>
</li>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid focusable controls inside the visually-hidden node list.

At Line 650, the SR-only list renders hidden <button> elements. Those controls can receive keyboard focus while remaining visually clipped, which breaks visible focus expectations and can strand keyboard users. Keep this list non-interactive, or add a focus style that makes focused items visible.

Suggested fix
- <ul style={SR_ONLY} aria-label="Knowledge graph nodes">- {nodes.map((n) =>- onNodeClick ? (- <li key={n.id}>- <button type="button" onClick={() => onNodeClick(n)}>- {n.name}- </button>- </li>- ) : (- <li key={n.id}>{n.name}</li>- ),- )}- </ul>+ <ul style={SR_ONLY} aria-label="Knowledge graph nodes">+ {nodes.map((n) => (+ <li key={n.id}>{n.name}</li>+ ))}+ </ul>
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>
onNodeClick ? (
<likey={n.id}>
<buttontype="button"onClick={()=>onNodeClick(n)}>
{n.name}
</button>
</li>
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>(
<likey={n.id}>{n.name}</li>
))}
</ul>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/KnowledgeGraph2D.tsx` around lines 648 - 655, The
SR_ONLY hidden node list in KnowledgeGraph2D.tsx currently renders focusable
<button> elements inside nodes.map when onNodeClick is provided (symbols:
SR_ONLY, nodes.map, onNodeClick), which can receive keyboard focus while
visually hidden; change the hidden list to be non-interactive by replacing the
focusable controls with plain, non-focusable elements (e.g., render a <span> or
similar static element for each node) or remove onClick handlers from the
SR_ONLY list so no element inside the list can receive focus, and if you must
keep interactive behavior move the clickable <button> out of the SR_ONLY
container or explicitly set tabIndex={-1} and aria-hidden to prevent focus but
prefer the non-interactive approach.

Comment on lines +79 to +93
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") { close(); return; }
if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Harden Tab trap when focus is not on boundary elements.

On Line 91–Line 92, wrapping only handles active === first/last. If focus lands on the dialog container (tabIndex={-1}) or otherwise outside focusable, Shift+Tab can escape the modal. Force-focus first/last whenever active is outside the trapped set.

Suggested patch
 if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
- if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }- else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }+ const inTrap = !!active && focusable.includes(active);+ if (!inTrap) {+ e.preventDefault();+ (e.shiftKey ? last : first).focus();+ } else if (e.shiftKey && active === first) {+ e.preventDefault(); last.focus();+ } else if (!e.shiftKey && active === last) {+ e.preventDefault(); first.focus();+ }
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
if(e.shiftKey&&active===first){e.preventDefault();last.focus();}
elseif(!e.shiftKey&&active===last){e.preventDefault();first.focus();}
}
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
constinTrap=!!active&&focusable.includes(active);
if(!inTrap){
e.preventDefault();
(e.shiftKey ? last : first).focus();
}elseif(e.shiftKey&&active===first){
e.preventDefault();last.focus();
}elseif(!e.shiftKey&&active===last){
e.preventDefault();first.focus();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/SignInModal.tsx` around lines 79 - 93, The onKey
handler for the SignInModal’s focus trap (function onKey, using panelRef) only
checks equality with first/last elements and fails when document.activeElement
is outside the focusable list (e.g., the dialog container with tabIndex=-1);
update onKey so that after building focusable (from
panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit cb1265e into mainJun 12, 2026
5 of 6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the a11y/wave2-phase2 branch June 12, 2026 14:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108) - #214

Merged
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2
Jun 12, 2026
Merged

Wave 2 Phase 2 — accessibility: contrast (#107) + focus/labels (#108)#214
Jose-Gael-Cruz-Lopez merged 3 commits into
mainfrom
a11y/wave2-phase2

Conversation

@Jose-Gael-Cruz-Lopez

@Jose-Gael-Cruz-LopezJose-Gael-Cruz-Lopez commented Jun 12, 2026

Copy link
Copy Markdown
Member

Wave 2 — Phase 2: accessibility (WCAG AA)

Two scoped, single-issue commits. Each verified by a fresh-context scope check; builds green (tsc + eslint + vitest 37/37). Not for merge yet — awaiting spot-check of the contrast list.

#107 — design-token contrast (WCAG 1.4.3)

Darken four shared tokens/controls that measured below 4.5:1 (ratios computed with the WCAG relative-luminance formula):

Token / controlBeforeBefore ratioAfterAfter ratio
--text-muted (~224 uses)#8a83723.4–3.8:1#6f68575.0–5.5:1
.btn--primary bg (white label)--accent#8a9a5b3.06:1--brand-forest#1B6C426.37:1
.chip--accent text--accent on --accent-soft2.80:1--sap-600#3a6a2c5.84:1
ModelToggle "Fast" label#3B82F62.87:1#1D4ED85.23:1
ModelToggle "Smart" label#8A63D23.30:1#6D28D95.36:1

Also re-verified the #112 state cues: error ! = 4.70:1 (text); rarity status dot ≥3:1 for all five tiers (non-text). Pre-existing/out-of-scope finding flagged: the rarity text label (common/uncommon/legendary) is 3.2–4.2:1.

#108 — focus + a11y labels (2.4.7 / 4.1.2 / 2.4.3 / 1.1.1)

  • Removed inline outline:"none" on 13 inputs/textareas → global :focus-visible ring restored.
  • aria-labels on the ChatPanel textarea, ManageCourses search, Calendar checkboxes.
  • SignInModal: focus-in + Tab-trap + restore (Dialog pattern in place).
  • role="img"+label on FunctionPlot / MermaidBlock; KnowledgeGraph2D gets a label + visually-hidden node list; landing logo <div onClick><button>.

Summary by CodeRabbit

  • Accessibility Improvements

    • Restored visible focus indicators on form inputs for keyboard navigation.
    • Added descriptive labels for screen reader users across interactive elements.
    • Enhanced keyboard navigation with improved focus management in modals.
    • Added accessibility descriptions for charts and diagrams.
  • Style Updates

    • Refined color values and visual styling for buttons and UI components.

Darken four contrast failures that measured below the 4.5:1 AA minimum
for normal text:
- --text-muted #8a8372 (~224 uses) -> #6f6857: now 5.0-5.5:1 on
white / --bg / --bg-inset (was 3.4-3.8:1).
- .btn--primary background --accent #8a9a5b (white label 3.06:1) ->
--brand-forest #1B6C42 (white label 6.37:1), tying the primary
action to the established brand green.
- .chip--accent text --accent on --accent-soft (2.80:1) -> --sap-600
(5.84:1).
- ModelToggle active labels: Fast #3B82F6 (2.87:1) -> #1D4ED8 (5.23:1),
Smart #8A63D2 (3.30:1) -> #6D28D9 (5.36:1), on their soft pills.
Ratios computed with the WCAG relative-luminance formula. The new #112
state cues were verified too: the error "!" is 4.70:1 (--err on
--err-soft) and the rarity status dot clears the 3:1 non-text bar for
all five tiers.
#108)
Focus visibility (WCAG 2.4.7): remove the inline outline:"none" on the
13 inputs/textareas (ChatPanel, Social, notetaker x6, Learn x2,
ParsedCardsTable x2, page.tsx) so each falls back to the existing global
:focus-visible accent ring instead of having no focus indicator.
Accessible names (4.1.2 / 3.3.2): aria-label on the ChatPanel tutor
textarea ("Message"), the ManageCoursesModal search input, and the
Calendar select-all + per-row checkboxes.
Modal focus (2.4.3): SignInModal now moves focus into the panel on open,
traps Tab within it, and restores focus to the trigger on close. Applied
the Dialog.tsx pattern in place (rather than reparenting into Dialog) to
preserve SignInModal's custom panel and close animation.
Non-text content (1.1.1): role="img" + a source-derived aria-label on
FunctionPlot and MermaidBlock; KnowledgeGraph2D gains role="img"/label
plus a visually-hidden node <ul> (mirroring KnowledgeGraph3D) so the
pointer-only graph is keyboard/AT navigable; the landing logo
<div onClick> becomes a <button> with its icon marked decorative.
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitUpdated (UTC)
❌ Deployment failed
View logs
frontend1a34b4fJun 12 2026, 02:30 PM

@coderabbitai

coderabbitaiBot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR improves application accessibility and visual consistency across the frontend. It restores browser focus outlines on form inputs, adds descriptive labels and semantic roles to interactive elements and visualizations, implements keyboard focus management in the sign-in modal, and updates design system tokens for better contrast compliance and visual harmony.

Changes

Accessibility and Design System Refinement

Layer / File(s)Summary
Focus outline restoration for form inputs
frontend/src/app/(shell)/notetaker/page.tsx, frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/flashcards/ParsedCardsTable.tsx, frontend/src/components/screens/Learn.tsx, frontend/src/components/screens/Social.tsx
Removes outline: "none" from multiple input and textarea elements across the application, restoring browser default focus indicators while preserving borders, backgrounds, and colors.
Semantic and accessible input controls
frontend/src/app/page.tsx, frontend/src/components/ChatPanel.tsx, frontend/src/components/ManageCoursesModal.tsx, frontend/src/components/screens/Calendar.tsx
Adds aria-label attributes to unlabeled inputs and checkboxes; converts navbar brand from div to button with proper labeling; improves checkbox accessibility in the assignments table.
Visual accessibility for complex components
frontend/src/components/FunctionPlot.tsx, frontend/src/components/MermaidBlock.tsx, frontend/src/components/KnowledgeGraph2D.tsx
Marks data visualization components (function plots, Mermaid diagrams, knowledge graphs) with role="img" and normalized aria-label attributes. Adds screen-reader-only list of graph nodes and reusable SR_ONLY style constant.
Modal keyboard and focus management
frontend/src/components/SignInModal.tsx
Implements keyboard accessibility by adding focus trap logic that contains Tab/Shift+Tab navigation within the modal, closes on Escape, and manages focus restoration during open/close lifecycle.
Design system token and color updates
frontend/src/app/globals.css, frontend/src/components/AchievementUnlockToast.tsx, frontend/src/components/ModelToggle.tsx
Updates CSS custom properties and component colors: --text-muted darkened for WCAG AA compliance, .chip--accent text color changed to --sap-600, .btn--primary background switched to --brand-forest with brightness hover effect, badge and option colors aligned with theme system.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A rabbit hops through focus rings so bright,
Accessible labels shine with WCAG might,
Modals trap focus, buttons emerge from divs,
Design tokens harmonize—accessibility thrives!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and specifically references the two main focuses of the PR—accessibility improvements for contrast (#107) and focus/labels (#108)—accurately summarizing the primary changes.
Description check✅ PassedThe description comprehensively covers changes made, related issues (#107, #108, #112), testing status (builds green), and detailed technical notes including WCAG ratios and implementation details for reviewers.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch a11y/wave2-phase2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/KnowledgeGraph2D.tsx`:
- Around line 648-655: The SR_ONLY hidden node list in KnowledgeGraph2D.tsx
currently renders focusable <button> elements inside nodes.map when onNodeClick
is provided (symbols: SR_ONLY, nodes.map, onNodeClick), which can receive
keyboard focus while visually hidden; change the hidden list to be
non-interactive by replacing the focusable controls with plain, non-focusable
elements (e.g., render a <span> or similar static element for each node) or
remove onClick handlers from the SR_ONLY list so no element inside the list can
receive focus, and if you must keep interactive behavior move the clickable
<button> out of the SR_ONLY container or explicitly set tabIndex={-1} and
aria-hidden to prevent focus but prefer the non-interactive approach.
In `@frontend/src/components/SignInModal.tsx`:
- Around line 79-93: The onKey handler for the SignInModal’s focus trap
(function onKey, using panelRef) only checks equality with first/last elements
and fails when document.activeElement is outside the focusable list (e.g., the
dialog container with tabIndex=-1); update onKey so that after building
focusable (from panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a5b2643-fbc9-4186-9857-602f32e7c140

📥 Commits

Reviewing files that changed from the base of the PR and between 50bb633 and 1a34b4f.

📒 Files selected for processing (15)
  • frontend/src/app/(shell)/notetaker/page.tsx
  • frontend/src/app/globals.css
  • frontend/src/app/page.tsx
  • frontend/src/components/AchievementUnlockToast.tsx
  • frontend/src/components/ChatPanel.tsx
  • frontend/src/components/FunctionPlot.tsx
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/ManageCoursesModal.tsx
  • frontend/src/components/MermaidBlock.tsx
  • frontend/src/components/ModelToggle.tsx
  • frontend/src/components/SignInModal.tsx
  • frontend/src/components/flashcards/ParsedCardsTable.tsx
  • frontend/src/components/screens/Calendar.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/components/screens/Social.tsx
💤 Files with no reviewable changes (2)
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/app/(shell)/notetaker/page.tsx

Comment on lines +648 to +655
<ul style={SR_ONLY} aria-label="Knowledge graph nodes">
{nodes.map((n) =>
onNodeClick ? (
<li key={n.id}>
<button type="button" onClick={() => onNodeClick(n)}>
{n.name}
</button>
</li>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid focusable controls inside the visually-hidden node list.

At Line 650, the SR-only list renders hidden <button> elements. Those controls can receive keyboard focus while remaining visually clipped, which breaks visible focus expectations and can strand keyboard users. Keep this list non-interactive, or add a focus style that makes focused items visible.

Suggested fix
- <ul style={SR_ONLY} aria-label="Knowledge graph nodes">- {nodes.map((n) =>- onNodeClick ? (- <li key={n.id}>- <button type="button" onClick={() => onNodeClick(n)}>- {n.name}- </button>- </li>- ) : (- <li key={n.id}>{n.name}</li>- ),- )}- </ul>+ <ul style={SR_ONLY} aria-label="Knowledge graph nodes">+ {nodes.map((n) => (+ <li key={n.id}>{n.name}</li>+ ))}+ </ul>
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>
onNodeClick ? (
<likey={n.id}>
<buttontype="button"onClick={()=>onNodeClick(n)}>
{n.name}
</button>
</li>
<ulstyle={SR_ONLY}aria-label="Knowledge graph nodes">
{nodes.map((n)=>(
<likey={n.id}>{n.name}</li>
))}
</ul>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/KnowledgeGraph2D.tsx` around lines 648 - 655, The
SR_ONLY hidden node list in KnowledgeGraph2D.tsx currently renders focusable
<button> elements inside nodes.map when onNodeClick is provided (symbols:
SR_ONLY, nodes.map, onNodeClick), which can receive keyboard focus while
visually hidden; change the hidden list to be non-interactive by replacing the
focusable controls with plain, non-focusable elements (e.g., render a <span> or
similar static element for each node) or remove onClick handlers from the
SR_ONLY list so no element inside the list can receive focus, and if you must
keep interactive behavior move the clickable <button> out of the SR_ONLY
container or explicitly set tabIndex={-1} and aria-hidden to prevent focus but
prefer the non-interactive approach.

Comment on lines +79 to +93
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") { close(); return; }
if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Harden Tab trap when focus is not on boundary elements.

On Line 91–Line 92, wrapping only handles active === first/last. If focus lands on the dialog container (tabIndex={-1}) or otherwise outside focusable, Shift+Tab can escape the modal. Force-focus first/last whenever active is outside the trapped set.

Suggested patch
 if (e.key === "Tab" && panelRef.current) {
const focusable = Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if (focusable.length === 0) { e.preventDefault(); return; }
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement as HTMLElement | null;
- if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }- else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }+ const inTrap = !!active && focusable.includes(active);+ if (!inTrap) {+ e.preventDefault();+ (e.shiftKey ? last : first).focus();+ } else if (e.shiftKey && active === first) {+ e.preventDefault(); last.focus();+ } else if (!e.shiftKey && active === last) {+ e.preventDefault(); first.focus();+ }
}
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
if(e.shiftKey&&active===first){e.preventDefault();last.focus();}
elseif(!e.shiftKey&&active===last){e.preventDefault();first.focus();}
}
constonKey=(e: KeyboardEvent)=>{
if(e.key==="Escape"){close();return;}
if(e.key==="Tab"&&panelRef.current){
constfocusable=Array.from(
panelRef.current.querySelectorAll<HTMLElement>(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
);
if(focusable.length===0){e.preventDefault();return;}
constfirst=focusable[0];
constlast=focusable[focusable.length-1];
constactive=document.activeElementasHTMLElement|null;
constinTrap=!!active&&focusable.includes(active);
if(!inTrap){
e.preventDefault();
(e.shiftKey ? last : first).focus();
}elseif(e.shiftKey&&active===first){
e.preventDefault();last.focus();
}elseif(!e.shiftKey&&active===last){
e.preventDefault();first.focus();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/SignInModal.tsx` around lines 79 - 93, The onKey
handler for the SignInModal’s focus trap (function onKey, using panelRef) only
checks equality with first/last elements and fails when document.activeElement
is outside the focusable list (e.g., the dialog container with tabIndex=-1);
update onKey so that after building focusable (from
panelRef.current.querySelectorAll) you check whether active
(document.activeElement) is contained in that focusable set and, if not, always
preventDefault and move focus to last when Shift+Tab or to first when Tab;
ensure the checks reference the symbols focusable, first, last, active and
preserve the existing Escape handling (close()).

@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez merged commit cb1265e into mainJun 12, 2026
5 of 6 checks passed
@Jose-Gael-Cruz-Lopez
Jose-Gael-Cruz-Lopez deleted the a11y/wave2-phase2 branch June 12, 2026 14:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Jose-Gael-Cruz-Lopez