-
Notifications
You must be signed in to change notification settings - Fork 1.6k
fix: announce sending status to screen readers via live region #5781
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
Open
isherstneva
wants to merge
20
commits into
microsoft:main
Choose a base branch
from
isherstneva:fix/sending-live-region-announcement
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
385f011
fix: announce sending status to screen readers via live region
isherstneva 6523036
fix: use React.Fragment instead of fragment shorthand
isherstneva 98695bb
test: advance clock past live region fade before final snapshot
isherstneva 6b936e5
revert: restore original test, snapshot needs regeneration
isherstneva a37c870
test: update snapshots to include sending live region announcement
isherstneva c4a883b
test: update snapshots for sending live region announcement
isherstneva 0e46c91
test: fix assertion tests for sending live region announcement
isherstneva 6c24bc6
test: wait for async telemetry exception event in unknownActivity test
isherstneva 41bd39b
test: stabilize snapshot tests with missing image/scroll waits
isherstneva 533fe17
fix: delay sending announcement to 3 seconds to avoid noise on fast s…
isherstneva a48a1b5
test: regenerate snapshots after 3s sending announcement delay
isherstneva 9c84327
fix: add required curly braces to if statements (curly lint rule)
isherstneva 66cee21
refactor: rename SendSending to LongSend; revert unrelated test changes
isherstneva fb9fb48
test: rename sendSending test to longSend
isherstneva b274a28
ci: retrigger
isherstneva ae3cfd3
ci: retrigger
isherstneva 0f23db2
ci: retrigger
isherstneva 5fc301c
ci: retrigger
isherstneva 26e9a2d
refactor: extract useActivityKeysOfSendStatus hook to eliminate dupli…
isherstneva a7d57db
refactor: return readonly tuple from useActivityKeysOfSendStatus per …
isherstneva File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
__tests__/html2/accessibility/liveRegion/activityStatus.longSend.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en-US"> | ||
| <head> | ||
| <link href="/assets/index.css" rel="stylesheet" type="text/css" /> | ||
| <script crossorigin="anonymous" src="/test-harness.js"></script> | ||
| <script crossorigin="anonymous" src="/test-page-object.js"></script> | ||
| <script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script> | ||
| </head> | ||
| <body> | ||
| <main id="webchat"></main> | ||
| <script> | ||
| run( | ||
| async function () { | ||
| const { directLine, store } = testHelpers.createDirectLineEmulator(); | ||
|
|
||
| WebChat.renderWebChat( | ||
| { | ||
| directLine, | ||
| store, | ||
| styleOptions: { | ||
| sendTimeout: 20000 | ||
| } | ||
| }, | ||
| document.getElementById('webchat') | ||
| ); | ||
|
|
||
| await pageConditions.uiConnected(); | ||
|
|
||
| const { disconnect, flush } = pageObjects.observeLiveRegion(); | ||
|
|
||
| try { | ||
| // Emulate outgoing activity but do not acknowledge it, keeping it in "sending" state. | ||
| await directLine.emulateOutgoingActivity('Hello, World!'); | ||
|
|
||
| const liveRegionText = []; | ||
|
|
||
| await pageConditions.became( | ||
| 'live region narrated sending message', | ||
| () => { | ||
| try { | ||
| liveRegionText.push(...flush()); | ||
|
|
||
| expect(liveRegionText).toContain('Sending message.'); | ||
|
|
||
| return true; | ||
| } catch (err) { | ||
| return false; | ||
| } | ||
| }, | ||
| 4000 | ||
| ); | ||
| } finally { | ||
| disconnect(); | ||
| } | ||
| }, | ||
| { ignoreErrors: true } | ||
| ); | ||
| </script> | ||
| </body> | ||
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import { hooks } from 'botframework-webchat-api'; | ||
| import { memo, useEffect, useRef, useState } from 'react'; | ||
|
|
||
| import { useLiveRegion } from '../../providers/LiveRegionTwin'; | ||
| import { SENDING } from '../../types/internal/SendStatus'; | ||
| import useActivityKeysOfSendStatus from './useActivityKeysOfSendStatus'; | ||
|
|
||
| const { useLocalizer, usePonyfill } = hooks; | ||
|
|
||
| const SENDING_ANNOUNCEMENT_DELAY = 3000; | ||
|
|
||
| /** | ||
| * React component to narrate "Sending message." into the live region, but only when the | ||
| * outgoing activity has been stuck in the `sending` state for at least 3 seconds. | ||
| * | ||
| * Fast sends (acknowledged by the server within 3 seconds) stay silent to avoid noisy | ||
| * announcements. Slow or stalled sends get narrated so the user knows what is happening. | ||
| * | ||
| * Presentational activities (e.g. `event` or `typing`) are excluded to reduce noise. | ||
| */ | ||
| const LiveRegionLongSend = () => { | ||
| const localize = useLocalizer(); | ||
| const [{ clearTimeout, setTimeout }] = usePonyfill(); | ||
|
|
||
| const liveRegionSendSendingAlt = localize('TRANSCRIPT_LIVE_REGION_SEND_SENDING_ALT'); | ||
|
|
||
| /** Keys we have already announced "Sending message." for — prevents repeated announcements. */ | ||
| const announcedKeysRef = useRef<Set<string>>(new Set()); | ||
|
|
||
| /** Monotonic counter; incrementing it causes `useLiveRegion` to queue the announcement. */ | ||
| const [tick, setTick] = useState(0); | ||
|
|
||
| /** Keys of outgoing non-presentational activities that are currently in the sending state. */ | ||
| const [sendingKeys] = useActivityKeysOfSendStatus(SENDING); | ||
|
|
||
| /** | ||
| * Arm a per-key timer when a key newly enters `sendingKeys`. | ||
| * Cancel all pending timers when a key leaves (cleanup handles this via deps change). | ||
| * Clean up the `announcedKeysRef` for keys that are no longer sending. | ||
| */ | ||
| useEffect(() => { | ||
| // Prune announced keys that are no longer sending. | ||
| for (const key of Array.from(announcedKeysRef.current)) { | ||
| if (!sendingKeys.has(key)) { | ||
| announcedKeysRef.current.delete(key); | ||
| } | ||
| } | ||
|
|
||
| if (!sendingKeys.size) { | ||
| return; | ||
| } | ||
|
|
||
| const timeouts: ReturnType<typeof setTimeout>[] = []; | ||
|
|
||
| for (const key of sendingKeys) { | ||
| if (announcedKeysRef.current.has(key)) { | ||
| continue; | ||
| } | ||
|
|
||
| const timeout = setTimeout(() => { | ||
| if (!sendingKeys.has(key)) { | ||
| return; | ||
| } | ||
|
|
||
| announcedKeysRef.current.add(key); | ||
| setTick(t => t + 1); | ||
| }, SENDING_ANNOUNCEMENT_DELAY); | ||
|
|
||
| timeouts.push(timeout); | ||
| } | ||
|
|
||
| return () => timeouts.forEach(id => clearTimeout(id)); | ||
| }, [clearTimeout, sendingKeys, setTimeout]); | ||
|
|
||
| useLiveRegion(() => (tick ? liveRegionSendSendingAlt : false), [liveRegionSendSendingAlt, tick]); | ||
|
|
||
| return null; | ||
| }; | ||
|
|
||
| LiveRegionLongSend.displayName = 'LiveRegionLongSend'; | ||
|
|
||
| export default memo(LiveRegionLongSend); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
packages/component/src/Transcript/LiveRegion/useActivityKeysOfSendStatus.ts
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The hook design doesn't match our convention. Please look at
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { hooks } from 'botframework-webchat-api'; | ||
| import { useMemo } from 'react'; | ||
|
|
||
| import type { SendStatus } from '../../types/internal/SendStatus'; | ||
| import isPresentational from './isPresentational'; | ||
|
|
||
| const { useGetActivityByKey, useSendStatusByActivityKey } = hooks; | ||
|
|
||
| /** | ||
| * Returns the set of keys of outgoing non-presentational activities that currently | ||
| * have the given send status. | ||
| * | ||
| * Presentational activities (e.g. `event` or `typing`) are excluded to reduce noise. | ||
| */ | ||
| export default function useActivityKeysOfSendStatus(status: SendStatus): readonly [Set<string>] { | ||
| const [sendStatusByActivityKey] = useSendStatusByActivityKey(); | ||
| const getActivityByKey = useGetActivityByKey(); | ||
|
|
||
| return [ | ||
| useMemo<Set<string>>(() => { | ||
| const keys = new Set<string>(); | ||
|
|
||
| for (const [key, sendStatus] of sendStatusByActivityKey) { | ||
| if (sendStatus === status && !isPresentational(getActivityByKey(key))) { | ||
| keys.add(key); | ||
| } | ||
| } | ||
|
|
||
| return keys; | ||
| }, [getActivityByKey, sendStatusByActivityKey, status]) | ||
| ] as const; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct the wording “screen reader” → “screen readers” for grammatical correctness in the translator comment.