Skip to content

Announce item name when it's removed from a table - #66

Merged
BryceStevenWilley merged 4 commits into
v1.9.8.postfrom
a11y-aria-live
Jul 23, 2026
Merged

Announce item name when it's removed from a table#66
BryceStevenWilley merged 4 commits into
v1.9.8.postfrom
a11y-aria-live

Conversation

@rajeswari1301

@rajeswari1301 rajeswari1301 commented Jul 15, 2026

Copy link
Copy Markdown

When a user deletes a row from a table, screen reader users had no way to know anything happened. I first tried adding an aria-live region but it didn't work with VoiceOver - docassemble replaces the entire #dabody content on every AJAX response, so any aria-live div inside it gets destroyed and recreated each time, and screen readers don't announce text that's already present in a newly created element (i tested manually updating the text of an existing aria-live div did announce correctly, but the AJAX swap broke it).

Instead, I worked with docassemble's existing behavior, after every AJAX response, daInitialize already moves focus to #daMainQuestion, and VoiceOver reads its full text content. So I added a helper function in functions.py that queues a message when a list or dict item is deleted, and in server.py, after the page is loaded, if the page contains a table with edit/delete buttons (btn-darevisit), that message is injected as a visually hidden span inside the heading.

This way the screen reader reads "Item removed." when focus moves to the heading after the deleting an item.

Co-authored-by: plocket <52798256+plocket@users.noreply.github.com>

@BryceStevenWilley BryceStevenWilley 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.

After testing this a bit, I think that there are two main things that should change:

  • Give a bit more information about what item was removed. It looks like item_popped = this_thread.current_info['action_dict'].pop(..., and then str(item_popped) works well for that (just gives the first column of the row, which IMO is good enough). I think this is needed, Item removed is just a bit too vague.
  • Change how we display the announcement. As is, I think it too easy for users to either miss the announcement if they go to a different part of the page before the whole heading is read, or for it to be read multiple times / later than we intend. We could either:
    1. make an empty announcement section right under the header (not in the header itself, that's confusing if folks are iterating over the headings present on the page), have add_sr_announcement() add the text to the javascript of the page, and then add those messages to the announcement section after the page has loaded (after a set interval). Let's us keep aria-live= polite
    2. use docassemble's builtin log(..., "info") banner system. This uses role=alert (which the below link claims doesn't work on macOS, but seems to work for me on Chrome/macOS. Would need more testing, might not work at all in Firefox, but), and lets us punt on this particular issue: we could just use log(..., "info") for now, and then make sure the banner announcement system is more accessible (#47 specifically means that we'll have to do more testing with banners when we get the chance). We very will might need to take option 1 when fixing banners anyway.

Slight leaning towards option 2 here, eventually getting to option 1 when addressing #47.

https://tetralogical.com/blog/2024/05/01/why-are-my-live-regions-not-working/ was the best source I found on aria-live issues

Comment thread docassemble_webapp/docassemble/webapp/server.py Outdated
Use the popped item's name in the message instead of a generic "Item removed". Also prime an empty aria-live
region once when the page first loads, and writing the message into it on delete. And, added a tiny delay to the heading focus shift so VoiceOver has some time to speak before the focus jumps.
@rajeswari1301

Copy link
Copy Markdown
Author

After testing this a bit, I think that there are two main things that should change:

  • Give a bit more information about what item was removed. It looks like item_popped = this_thread.current_info['action_dict'].pop(..., and then str(item_popped) works well for that (just gives the first column of the row, which IMO is good enough). I think this is needed, Item removed is just a bit too vague.

  • Change how we display the announcement. As is, I think it too easy for users to either miss the announcement if they go to a different part of the page before the whole heading is read, or for it to be read multiple times / later than we intend. We could either:

    1. make an empty announcement section right under the header (not in the header itself, that's confusing if folks are iterating over the headings present on the page), have add_sr_announcement() add the text to the javascript of the page, and then add those messages to the announcement section after the page has loaded (after a set interval). Let's us keep aria-live= polite
    2. use docassemble's builtin log(..., "info") banner system. This uses role=alert (which the below link claims doesn't work on macOS, but seems to work for me on Chrome/macOS. Would need more testing, might not work at all in Firefox, but), and lets us punt on this particular issue: we could just use log(..., "info") for now, and then make sure the banner announcement system is more accessible ("Log" banners not announced to screen readers #47 specifically means that we'll have to do more testing with banners when we get the chance). We very will might need to take option 1 when fixing banners anyway.

Slight leaning towards option 2 here, eventually getting to option 1 when addressing #47.

https://tetralogical.com/blog/2024/05/01/why-are-my-live-regions-not-working/ was the best source I found on aria-live issues

Thank you for this, and for the article.

For the display issue, the article was right, Safari and Chrome with VoiceOver only seem to announce things if they're injected into a region that's already sitting there. So, there's now an empty aria-live="assertive" container that loads once outside the dynamic area. After a delete, the message drops in immediately, and there's a slight delay on the heading focus-shift so VoiceOver doesn't get cut off.

Tested it on macOS with Safari and Chrome. Safari is handling it cleanly but chrome is a little flaky on the initial load but works fine after

Comment thread docassemble_webapp/docassemble/webapp/server.py Outdated
@BryceStevenWilley

Copy link
Copy Markdown

Just some testing comments. Testing this when it's aria-live = assertive

macOS

  • Chrome:
    • works good for me
    • seems to have some issues when you start up VoiceOver in the middle of a page, instead of already having VoiceOver already going when opening the interview. Not ideal, but not usually how it would be used?
    • if stuck in that weird state, refreshing the page manually works fine
  • Safari
    • works good for me
  • Firefox
    • couldn't get it to actually announce.

iPhone:

  • Firefox
    • works good for me
  • Safari
    • works good for me

Linux:

  • Firefox:
    • couldn't get it to actually announce.
  • Chromium
    • announces the deletion, but the only browser / OS combo that announced it after reading the full header

It's a solid improvement over the current behavior.

@BryceStevenWilley BryceStevenWilley 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.

I think this is much better! I think the most efficient option is to keep the changes from functions.py, and move the JS changes and the server.py changes to a different PR. That behavior doesn't seem to be currently doing anything, but I think that's the right direction to continue in, and in a different PR you can integrate more tightly with the message log in that aria-live section.

Comment thread docassemble_webapp/docassemble/webapp/server.py Outdated
Comment thread docassemble_webapp/docassemble/webapp/server.py Outdated
@BryceStevenWilley

Copy link
Copy Markdown

Throwing my testing interview in the comments here so I don't lose it:

metadata:
  title: Default playground interview
  short title: Test
  comment: This is a learning tool.  Feel free to write over it.
---
include:
  - docassemble.AssemblyLine:assembly_line.yml
---
mandatory: True
code: |
  users.gather()
  done
---
event: done
question: idk
---
sections:
  - review_screen: Review
---
event: review_screen
question: Review
review:
  - Edit: users.revisit
    button: |
      **Users**

      % for item in users:
      - ${ item }
      % endfor
---

@rajeswari1301 rajeswari1301 changed the title Announce to screen readers when an item is removed Announce item name when it's removed from a table Jul 22, 2026

@BryceStevenWilley BryceStevenWilley 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.

Looks good to me! Will merge this now, and will do another full assistive tech test on #67

@BryceStevenWilley
BryceStevenWilley merged commit f52f649 into v1.9.8.post Jul 23, 2026
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