Skip to content

Conversation

@pkulkark
Copy link
Member

Description

This PR makes the following fixes:

  1. The Question/Answer label text color is configurable.
  2. Accessibility fixes for issues reported in the task.

Supporting Information

OpenCraft Internal Jira task: BB-10388

Testing Instructions

  1. Deploy this branch on redwood devstack
  2. Configure text color option and confirm the selected color is applied to question/answer text
  3. Confirm the keyboard navigation works correctly i.e. when the next card is loaded, the focus should be back on the card so that user can flip it
  4. Confirm screen reader works correctly.

@codecov
Copy link

codecov bot commented Dec 17, 2025

Codecov Report

❌ Patch coverage is 89.74359% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.19%. Comparing base (02c6739) to head (de06489).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
frontend/src/student-ui/student-ui.tsx 83.67% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master       #7      +/-   ##
==========================================
- Coverage   89.27%   89.19%   -0.08%     
==========================================
  Files          10       10              
  Lines         289      361      +72     
  Branches       21       35      +14     
==========================================
+ Hits          258      322      +64     
- Misses         27       35       +8     
  Partials        4        4              
Flag Coverage Δ
frontend 91.22% <89.74%> (-0.69%) ⬇️
python 73.80% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkulkark pkulkark force-pushed the pooja/accessibility-fix branch from 0a5a1f3 to eb0ee2a Compare December 17, 2025 21:13
@pkulkark pkulkark force-pushed the pooja/accessibility-fix branch from eb0ee2a to b9800f1 Compare December 17, 2025 21:20
Comment on lines 172 to 174
<div className="visually-hidden" role="status" aria-atomic="true" aria-label="Flashcard announcement">
{announcementText}
</div>
Copy link
Member

Choose a reason for hiding this comment

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

This seems to work nicely for the most part. The only thing is for me the card front was not read when the card is first displayed after clicking the "start" button.

Also, I think stripping html from the announcement text goes against the client's QA recommendations too. Perhaps we can just follow the expected results and recommendations more closely here?

The content of the active flashcard side should be announced by screen readers when it becomes active. In addition, users should be informed whether the front or back side is currently displayed.

For this we can perhaps augment the existing aria-label on the card itself.

Use semantic HTML inside the card, such as <h*>, <p>, and <strong>.

Dynamically update aria-hidden="true" for the inactive side, keeping the back side initially hidden.

You've already done this 👍

Use aria-live="polite" to announce the active card content to screen readers.

Maybe we can experiment with this on the card?

Copy link
Member Author

Choose a reason for hiding this comment

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

The only thing is for me the card front was not read when the card is first displayed after clicking the "start" button.

Looks like SRs may not announce if the live region doesn’t get a detectable change at the right time. I’ve updated the implementation to clear the live region and then set it on the next tick so the first card is reliably announced after Start.

I think stripping html from the announcement text goes against the client's QA recommendations too

I don't think that should affect anything. Screen readers shouldn't announce HTML tags; only the accessible text. For the rest I have to admit, I don't know it well enough and mostly relied on recommendations/best practices I found online. Here are what I found:

  • aria-label provides an element’s accessible name (typically announced when the element receives focus); it’s not intended as a “change announcement” channel. So screen readers may not announce it it changes.
  • aria-live is the reliable mechanism for announcements of dynamically changing contents.
  • role="status" is a live region with an implicit aria-live="polite" (ref)
  • Using a plain-text announcement is a common best practice to avoid overly verbose/odd output.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the context! I don't know a heap about aria either.

Can we consider a method that puts the aria-live things on the card itself? So we don't need this extra work to manage state on a separate announcement section?

Copy link
Member Author

Choose a reason for hiding this comment

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

That was my initial thought as well, but even with aria-hidden/inert on the inactive side, making the whole card live itself would mean any change to the injected HTML content, flip state, or index could trigger announcements of large chunks of content. If authors include lots of formatted text, the live region could get really noisy (This was one of the warning I found online as well).

Copy link
Member

Choose a reason for hiding this comment

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

would mean any change to the injected HTML content, flip state, or index could trigger announcements of large chunks of content.

But any of these changes mean different content is displayed (eg. a new card, or now the back of the card), and thus should be announced by the screen reader. This is the same content that we're putting in the new announcement region anyway.

but even with aria-hidden/inert on the inactive side, making the whole card live itself...

What if we set the aria live for just the visible side of the card?

I really want to avoid this secondary live announcement area, as it's a lot of extra logic and state tracking, prone to bugs.

Copy link
Member

Choose a reason for hiding this comment

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

I’ve updated the implementation to clear the live region and then set it on the next tick so the first card is reliably announced after Start.

I just tested with a screen reader and unfortunately I'm not getting the first card announced still. :/

Copy link
Member Author

@pkulkark pkulkark Jan 5, 2026

Choose a reason for hiding this comment

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

But any of these changes mean different content is displayed (eg. a new card, or now the back of the card), and thus should be announced by the screen reader. This is the same content that we're putting in the new announcement region anyway.

Not exactly. If we set aria-live on .fc-card-front / .fc-card-back and leave the authored HTML inside, most screen readers will treat any DOM mutation inside that subtree as eligible for announcement. With rich content that can mean:

  • reading more than you expect (or reading it twice: once as a live update, once because focus moved),
  • weird ordering (e.g., it announces the “Flip” icon text / label / formatting artifacts),
  • lots of chatter if any nested node changes.

I tried to make aria-live conditional tied to isFlipped like this:

  • Front side: aria-live={!isFlipped ? 'polite' : undefined}
  • Back side: aria-live={isFlipped ? 'polite' : undefined}

And tested with VoiceOver (in macos with safari). But it didn't actually announce when flipped. On further investigation, I learned that some screen readers won’t announce just because aria-hidden toggled; they announce when the live region’s text content changes.

So in order to have reliable announcements, we need to either:

  1. force a real text change in that live region (same clear→set trick), or
  2. remount the live subtree on each change (e.g., changing a key), which is also logic, just a different kind.

Unfortunately we can't avoid the extra logic/state for this 🫤

Copy link
Member Author

Choose a reason for hiding this comment

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

I just tested with a screen reader and unfortunately I'm not getting the first card announced still.

Just pushed a fix for it. Could you try again?

Copy link
Member

Choose a reason for hiding this comment

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

If we set aria-live on .fc-card-front / .fc-card-back and leave the authored HTML inside,

Hmm I wonder if we can just have a card content section, and change the content depending on whether the back or front should be displayed? Rather than have both content in the dom at once.

Anyway, maybe we just need to run with what we have for now and investigate in the future if we find the logic hard to maintain.

Copy link
Member

Choose a reason for hiding this comment

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

Just pushed a fix for it. Could you try again?

Doesn't appear to fix it sorry; it actually seems less reliable at reading the card content. :/

Also noting that the screenreader text is rather verbose. For example, on every card is read: "flashcard 1 of 2 click to flip button". And the announcement part starts with "flashcard announcement card 2 of 2". And these are combined when it successfully reads the announcement on a new card. Not sure how much of an issue it is though.

styling: FlashcardStyling,
}

function htmlToText(html: string): string {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we should be stripping html for the screen reader; see next comment about the recommendations.

@samuelallan72
Copy link
Member

@pkulkark

Deploy this branch on redwood devstack

A note on testing: I couldn't get my redwood devstack working properly, so I tested on sumac. I don't think this will affect anything, just noting.

@pkulkark pkulkark force-pushed the pooja/accessibility-fix branch from 48cd19a to c898e66 Compare December 18, 2025 17:24
@pkulkark pkulkark force-pushed the pooja/accessibility-fix branch from dda1187 to 8957849 Compare December 19, 2025 19:33
Sign up for free to 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.

3 participants