Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Announcer: Part 1 #2362

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open

Announcer: Part 1 #2362

wants to merge 44 commits into from

Conversation

marcysutton
Copy link
Member

@marcysutton marcysutton commented Nov 15, 2024

The initial implementation for a live region component! I'm getting this draft PR up so I can test with the remote URL.

Jira Issue: https://khanacademy.atlassian.net/browse/WB-1768

Outstanding questions/work areas:

  • Testing in more ATs, particularly in webapp and on mobile devices.
  • More integration with React: usage for JSX, debouncing with custom duration in continuously re-rendering components like the Video Player with Clarifications, etc.

Copy link

changeset-bot bot commented Nov 15, 2024

🦋 Changeset detected

Latest commit: 50f263b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@khanacademy/wonder-blocks-announcer Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Contributor

github-actions bot commented Nov 15, 2024

Size Change: +1.99 kB (+2.04%)

Total Size: 99.3 kB

Filename Size Change
packages/wonder-blocks-announcer/dist/es/index.js 1.99 kB +1.99 kB (new file) 🆕
ℹ️ View Unchanged
Filename Size
packages/wonder-blocks-accordion/dist/es/index.js 3.78 kB
packages/wonder-blocks-banner/dist/es/index.js 1.53 kB
packages/wonder-blocks-birthday-picker/dist/es/index.js 1.77 kB
packages/wonder-blocks-breadcrumbs/dist/es/index.js 887 B
packages/wonder-blocks-button/dist/es/index.js 4.04 kB
packages/wonder-blocks-cell/dist/es/index.js 2.01 kB
packages/wonder-blocks-clickable/dist/es/index.js 3.06 kB
packages/wonder-blocks-core/dist/es/index.js 3.48 kB
packages/wonder-blocks-data/dist/es/index.js 6.24 kB
packages/wonder-blocks-dropdown/dist/es/index.js 19.2 kB
packages/wonder-blocks-form/dist/es/index.js 6.28 kB
packages/wonder-blocks-grid/dist/es/index.js 1.36 kB
packages/wonder-blocks-icon-button/dist/es/index.js 2.95 kB
packages/wonder-blocks-icon/dist/es/index.js 871 B
packages/wonder-blocks-labeled-field/dist/es/index.js 72 B
packages/wonder-blocks-layout/dist/es/index.js 1.82 kB
packages/wonder-blocks-link/dist/es/index.js 2.28 kB
packages/wonder-blocks-modal/dist/es/index.js 5.43 kB
packages/wonder-blocks-pill/dist/es/index.js 1.65 kB
packages/wonder-blocks-popover/dist/es/index.js 4.87 kB
packages/wonder-blocks-progress-spinner/dist/es/index.js 1.52 kB
packages/wonder-blocks-search-field/dist/es/index.js 1.38 kB
packages/wonder-blocks-switch/dist/es/index.js 1.94 kB
packages/wonder-blocks-testing-core/dist/es/index.js 3.74 kB
packages/wonder-blocks-testing/dist/es/index.js 1.07 kB
packages/wonder-blocks-theming/dist/es/index.js 693 B
packages/wonder-blocks-timing/dist/es/index.js 1.8 kB
packages/wonder-blocks-tokens/dist/es/index.js 2.36 kB
packages/wonder-blocks-toolbar/dist/es/index.js 905 B
packages/wonder-blocks-tooltip/dist/es/index.js 7.08 kB
packages/wonder-blocks-typography/dist/es/index.js 1.23 kB

compressed-size-action

Copy link
Contributor

github-actions bot commented Nov 15, 2024

A new build was pushed to Chromatic! 🚀

https://5e1bf4b385e3fb0020b7073c-nlcjdakqus.chromatic.com/

Chromatic results:

Metric Total
Captured snapshots 378
Tests with visual changes 1
Total stories 514
Inherited (not captured) snapshots [TurboSnap] 0
Tests on the build 378

@marcysutton marcysutton marked this pull request as ready for review November 20, 2024 21:28
@khan-actions-bot
Copy link
Contributor

khan-actions-bot commented Nov 20, 2024

Gerald

Required Reviewers
  • @Khan/wonder-blocks for changes to .changeset/thirty-ducks-type.md, .storybook/preview.tsx, __docs__/wonder-blocks-announcer/announcer.stories.tsx, packages/wonder-blocks-announcer/package.json, packages/wonder-blocks-announcer/tsconfig-build.json, static/sb-styles/preview.css, packages/wonder-blocks-announcer/src/announce-message.ts, packages/wonder-blocks-announcer/src/announcer.ts, packages/wonder-blocks-announcer/src/clear-messages.ts, packages/wonder-blocks-announcer/src/index.ts, packages/wonder-blocks-announcer/types/announcer.types.ts, packages/wonder-blocks-announcer/src/__tests__/announce-message.test.tsx, packages/wonder-blocks-announcer/src/__tests__/announcer.test.ts, packages/wonder-blocks-announcer/src/__tests__/clear-messages.test.tsx, packages/wonder-blocks-announcer/src/util/dom.ts, packages/wonder-blocks-announcer/src/util/util.ts, packages/wonder-blocks-announcer/src/__tests__/components/announce-message-button.tsx, packages/wonder-blocks-announcer/src/__tests__/util/dom.test.ts, packages/wonder-blocks-announcer/src/__tests__/util/test-utilities.ts, packages/wonder-blocks-announcer/src/__tests__/util/util.test.ts

Don't want to be involved in this pull request? Comment #removeme and we won't notify you of further changes.

@khan-actions-bot khan-actions-bot requested a review from a team November 20, 2024 21:28
Copy link
Contributor

github-actions bot commented Nov 20, 2024

npm Snapshot: Published

🎉 Good news!! We've packaged up the latest commit from this PR (ce3ccdd) and published all packages with changesets to npm.

You can install the packages in webapp by running:

./services/static/dev/tools/deploy_wonder_blocks.js --tag="PR2362"

Packages can also be installed manually by running:

yarn add @khanacademy/wonder-blocks-<package-name>@PR2362

Copy link
Member

@jandrade jandrade left a comment

Choose a reason for hiding this comment

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

This is looking really good! Giving my initial review with some suggestions on how to structure the files to be more consistent with the rest of the repo and asking some questions to get a bit more context on certain parts. This is great progress 👏

packages/wonder-blocks-announcer/CHANGELOG.md Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/src/index.ts Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/src/index.ts Outdated Show resolved Hide resolved
Comment on lines 34 to 36
setTimeout(() => {
return announcer?.announce(message, level, removalDelay);
}, 100);
Copy link
Member

Choose a reason for hiding this comment

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

question: Is there a specific reason why we need to use this timeout? I wonder if there's another way to handle this case, but I don't think I have full context on that.

Copy link
Member Author

Choose a reason for hiding this comment

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

It was for Safari and VoiceOver, but I can play around with it again to see if we really need it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this does seem important for Safari in particular! I made it configurable so it can be manipulated at runtime if necessary (especially for tests).

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 ended up removing this with the move to async! It wasn't needed in my testing for Safari/Voiceover.

Copy link
Member Author

@marcysutton marcysutton Nov 27, 2024

Choose a reason for hiding this comment

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

An initial timeout might still be needed to get reliable announcements! The async code does seem to be messing with Voiceover in both Chrome and Safari, especially on the first couple of messages. When I test in Storybook, polite messages are getting dropped almost entirely and only some assertive messages are making it through. So I'll figure out how to work it back in..probably in the announce method of the Announcer class. I hate to say it, but it might make sense to get rid of the double announcer element approach and simplify things.

Copy link
Member Author

@marcysutton marcysutton Dec 12, 2024

Choose a reason for hiding this comment

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

I did some more testing and added the initial timeout back in for Safari. I also included an option to set the length to 0 so that tests can skip it and work properly. I kept the double announcer stuff in as there wasn't a compelling reason to remove when I tested it.

packages/wonder-blocks-announcer/src/index.ts Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/src/index.ts Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/src/index.ts Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/src/index.ts Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/src/index.ts Show resolved Hide resolved
@khan-actions-bot khan-actions-bot requested a review from a team November 22, 2024 17:28
Copy link
Member

@beaesguerra beaesguerra left a comment

Choose a reason for hiding this comment

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

Great work so far Marcy! Left some feedback and questions 😄

timeoutDelay: {
control: "number",
type: "number",
description: "(milliseconds)",
Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering if there's a way for us to use the function docs for the storybook docs! Normally we're able to get the prop docs automatically from setting component in this block, though this is different since these docs are for functions rather than components!

cc: @jandrade in case you have come across similar things before!

Copy link
Member

Choose a reason for hiding this comment

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

If there isn't a way to do this, it would be helpful to add a description for the different options so it shows up in the docs! This can help developers know when to use what level or when to use debounceThreshold. Same for documenting the clear-messages utility!

image

packages/wonder-blocks-announcer/src/send-message.ts Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/types/Announcer.types.ts Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/src/Announcer.ts Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/src/Announcer.ts Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/src/Announcer.ts Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/src/Announcer.ts Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/src/Announcer.ts Outdated Show resolved Hide resolved
@khan-actions-bot khan-actions-bot requested a review from a team November 22, 2024 21:56
@marcysutton
Copy link
Member Author

marcysutton commented Nov 27, 2024

@jandrade @beaesguerra this is ready for another review! I added quite a few more tests. Yay!

In testing the debounce logic more thoroughly, I realized the first iteration was going to be unpredictable and hard to use. So I refactored it a bit. I'm currently debugging in ATs and working with Storybook though, as this might have broken things. 😬

Some code highlights:

  1. I added a debounceThreshold parameter to allow callers to specify how long to wait before announcing another message. This is intended for repeatedly-rendering components like the ClarificationNotifications component for the video player which are re-rendered every 1/2 second. This seemed like a reasonable way to handle it without having to compare strings. (We might still need to do that so it's not annoying to screen reader users, but I want to try this approach first.)
  2. The debounce function now returns the first message rather than the last, so it will be announced immediately (other implementations use the last callback rather than the first).
  3. I got rid of the removalDelay parameter as it wasn't that useful to begin with, and it made the entire thing super confusing. Elements are still removed after a period time, I just changed it to not be configurable rather than trying to maintain removalDelay and debounceThreshold which both use setTimeouts.
  • The removalDelay is internally set to 5000 ms + the default debounce wait time of 250ms. If a custom debounceThreshold is passed in (say 2000ms), the internal removalDelay would updated to a longer duration for removal to avoid a race condition (2000ms + 5000ms).

My next steps are to test this more in webapp and see if it works as I expected!

Copy link
Member

@jandrade jandrade left a comment

Choose a reason for hiding this comment

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

Thanks for this new iteration. I'm adding some extra suggestions around the code (specially around tests). I'll keep digging into it and will integrate with the Combobox component. Thanks for all this progress!!

packages/wonder-blocks-announcer/src/Announcer.ts Outdated Show resolved Hide resolved
} from "./util/dom";
import {alternateIndex, debounce} from "./util/util";

const REMOVAL_TIMEOUT_DELAY = 5000;
Copy link
Member

Choose a reason for hiding this comment

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

question: Is there a specific reason why we have to wait for 5 seconds? do you think this would need to be configured by the consumer at some point?

Copy link
Member Author

@marcysutton marcysutton Dec 10, 2024

Choose a reason for hiding this comment

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

It's admittedly a bit of a magic number... long enough to let the platform accessibility API read out a message of unknown length before it's removed, and also longer than the default initial timeout. Initially I made this configurable by the user but I removed the option due to added complexity 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.

idea: What if we determined the delay dynamically based on the number of words in the announcement multiplied by something like 500ms? That way long messages are accounted for! This might also account for translations that might be longer, though we would be assuming approximately how long each word is read!

Copy link
Member Author

Choose a reason for hiding this comment

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

It's an interesting thought. I'm hesitant to go that route as screen reader speech rates are configurable, so it's a moving target. Perhaps we could mark that down for a future improvement if it comes up in production?

packages/wonder-blocks-announcer/types/Announcer.types.ts Outdated Show resolved Hide resolved
Comment on lines 76 to 81
createDuplicateRegions(
aWrapper,
"assertive",
this.regionFactory.count,
this.dictionary,
);
Copy link
Member

Choose a reason for hiding this comment

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

thought: One thing that I noticed while playing with Storybook is that if I change the text in the Story and click the "submit" button, sometimes the announcement is not read by Voice Over. I'm not sure where to put this, but saw that sometimes was announced in one of the duplicate regions.

Copy link
Member Author

@marcysutton marcysutton Dec 10, 2024

Choose a reason for hiding this comment

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

Hmm, that is odd. I noticed initial announcements aren't read if I change the politeness level to Assertive, but not for changing the text. The default Storybook frame also seems to impact reliability of messages, so I tend to test it in a standalone window. I think I'll opt for integration testing to observe these kinds of issues since I've spent so long on it already and the behavior is so inconsistent!

@khan-actions-bot khan-actions-bot requested a review from a team December 10, 2024 22:36
Copy link
Member

@beaesguerra beaesguerra left a comment

Choose a reason for hiding this comment

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

Great progress Marcy! Left some questions and comments 😄

* @param {number} wait Length of time to wait before calling callback again
* @returns {string} idRef of targeted live region element
*/
export function debounce(callback: (...args: any[]) => string, wait: number) {
Copy link
Member

Choose a reason for hiding this comment

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

I noticed we also have another debounce utility for dropdowns! The return type is a bit different though. No changes needed now but thought I'd mention it in case we want to consolidate utility functions across WB later on :)

Copy link
Member Author

@marcysutton marcysutton Dec 12, 2024

Choose a reason for hiding this comment

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

Good call out! This one needs to return a promise with a string and a utility method for updating the wait time so it's pretty specific internally.

packages/wonder-blocks-announcer/src/clear-messages.ts Outdated Show resolved Hide resolved
packages/wonder-blocks-announcer/src/announcer.ts Outdated Show resolved Hide resolved
@khan-actions-bot khan-actions-bot requested a review from a team December 12, 2024 19:56
@marcysutton
Copy link
Member Author

Alright, the debounce refactor is in! (it wasn't actually working when I tested it) I had to change it to allow a configurable debounce wait parameter with the simple announceMessage API. This means you can just call it with options without having to configure anything separately (helpful when it's being called in multiple places, so configurations don't interfere with each other).

@jandrade I also fixed that styling issue in Storybook that you were seeing with the Combobox. I added an addBodyClass decorator that can be used for anything...kind of a nice general improvement!

1. Keeps announce from being called too frequently. We can play with the timeout duration.
2. Makes the returned IDREF more reliable in a browser.
1. Keeps announce from being called too frequently. We can play with the timeout duration.
2. Makes the returned IDREF more reliable in a browser.
1. Return first result and debounce subsequent calls within the debounceThreshold
2. Remove removalDelay parameter to simplify API
3. Put debounce utility into a separate file and add tests
Debounce wasn't actually limiting execution in the wait period. Now it does, and the debounce duration is still configurable when calling announceMessage!
I was trying to avoid having to import the Announcer in this test to keep things isolated, but it's so specific to the Announcer that I decided it didn't matter that much. Specifying the Announcer instance for the scope instead of generic thisArg logic simplified things quite a bit as well.
@marcysutton marcysutton changed the base branch from announcer to main December 12, 2024 22:44
Copy link
Member

@beaesguerra beaesguerra left a comment

Choose a reason for hiding this comment

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

This is looking great, I tested the story out with VoiceOver + Safari, NVDA + Chrome/Firefox and the message is announced as I expected! Thanks for your hard work on this!

I had a few suggestions around stories for other scenarios and documentation (non-blocking). I'm looking forward to seeing the announcer get used in context!

(Will let Juan be the final approver though since he may have feedback/insights on how it was integrating it with combobox!)

timeoutDelay: {
control: "number",
type: "number",
description: "(milliseconds)",
Copy link
Member

Choose a reason for hiding this comment

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

If there isn't a way to do this, it would be helpful to add a description for the different options so it shows up in the docs! This can help developers know when to use what level or when to use debounceThreshold. Same for documenting the clear-messages utility!

image

debounceThreshold,
}: AnnounceMessageProps) => {
return (
<Button
Copy link
Member

Choose a reason for hiding this comment

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

(suggestion, no changes necessary) - I was curious about other scenarios that we could add to the story (or another story) so we can test different cases easily:

  • A button that triggers an announcement and another button that clears the specific announcement and/or all announcements
  • 1 button that triggers a polite message, another button that triggers an assertive message to see the behaviour for different announcement levels
  • buttons with different debounceThreshold values to show how that option changes the behaviour

message: "Here is some example text.",
level: "polite",
},
};
Copy link
Member

Choose a reason for hiding this comment

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

We can disable this story for chromatic since we don't need visual regression tests for it! https://www.chromatic.com/docs/disable-snapshots/#with-storybook

@jandrade
Copy link
Member

@marcysutton I've been testing the latest changes in Combobox and I'm still having some trouble announcing messages correctly when the dismiss/clear button is pressed.

The issue is that the button is pressed, the message is queued to be announced, but because the input[combobox] is focused again, then it causes the listbox to open and prioritizes the announcement for that (e.g. Expanded, completion selected). I'm attaching a screenshot here to illustrate this case:

Screen.Recording.2024-12-20.at.6.12.57.PM.mov

I'll try to put a new PR with my current progress.

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.

5 participants