Repository navigation
Show the code block copy confirmation for 5 seconds, like the message Copy button - #5892
Merged
Merged
Conversation
Eugene (OEvgeny)
marked this pull request as ready for review
October 17, 2026 15:19
Eugene (OEvgeny)
requested review from
William Wong (compulim),
Chris Whitten (cwhitten) and
TJ Durnford (tdurnford)
as code owners
October 17, 2026 15:19
William Wong (compulim)
requested changes
Oct 18, 2026
added 4 commits
October 17, 2026 18:15
Moves the 5 second timer of the "Copied" confirmation (#5872) out of ActivityCopyButton, so the copy button of code blocks can share it. The ponyfill is passed in, because that button renders outside of Web Chat's React tree.
…egion The copy button of code blocks showed its checkmark and "Copied" name for 0.5 seconds, driven by a CSS animation, and was an assertive live region that announced "Copied" and then "Copy" on top of its name changes. - Use useCopiedState() (5 seconds, restarted on copy), with the ponyfill passed to the custom element by useReactCodeBlockCopyButtonClass() - Remove aria-live and aria-pressed: the name of the button conveys "Copied", like the Copy button of messages - Keep the global class code-block-copy-button--copied for host styling - Remove the 0.5 second animation and the CSS that only mattered for it - Tests: fake clock and waitFor() instead of sleeps and unstable snapshots
…eturns an array - Rename useCopiedState() to useAutoResetState(initialValue, resetIntervalInMS, ponyfill), returning [value, setValue]; the value is reset after the interval, setting it again restarts the interval - Both copy buttons keep their 5 second confirmation via COPIED_DURATION - useReactCodeBlockCopyButtonClass() returns a frozen [class] - CodeBlockCopyButtonProps uses InferReactProps - behavior.html: assert name, role, and no live/pressed directly from getAccessibilityNode()
Eugene (OEvgeny)
force-pushed
the
OEvgeny/fix-code-block-copy-confirmation
branch
from
October 18, 2026 01:40
0da9947 to
ab7244c
Compare
William Wong (compulim)
approved these changes
Oct 18, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Changelog Entry
Fixed
Description
#5872 fixed the Copy button of messages. The copy button of code blocks had the same problem: its checkmark and its "Copied" name were driven by a 0.5 second CSS animation, in the default, Fluent, and Copilot themes. It also had
aria-live="assertive"andaria-pressedon the button itself. In Edge, each press raised a live region event and a name change for "Copied", then a live region event and a name change for "Copy" when the animation ended.The confirmation now uses the same timer as the Copy button of messages: 5 seconds, restarted when copying again, not affected by hiding Web Chat. The button is a plain button: its name is "Copied" while confirmed, and the focus indicator stays visible, as before.
Measured in Edge 155:
Design
The 5 second timer of the Copy button of messages is moved into a shared internal hook,
useAutoResetState(initialValue, resetIntervalInMS, ponyfill), used by both buttons. It works likeuseState(), but the value goes back toinitialValueafter the interval, and setting it again restarts the interval. The ponyfill is a parameter because the code block copy button is a custom element rendered in its own React root, so it cannot callusePonyfill().CustomElementsComposernow creates its class withuseReactCodeBlockCopyButtonClass(), which returns[class]and passes the ponyfill down. This mirrorsuseReactCodeBlockClass(), which passes the code highlighter and the localized strings to the code block. With the ponyfill, the confirmation follows the host's clock, and tests can use a fake clock.aria-liveis removed from the button. "Copied" reaches screen readers through the name of the button, which is spoken while the button has the focus, such as after pressing it with the keyboard (see the open question for mouse clicks in Safari). The Copy button of messages uses this channel too, and also queues "Copied" to Web Chat's live region (see below). The live region on the button raised a second event for "Copied", and another for "Copy" when the confirmation ended. With a 5 second confirmation, that would be an assertive "Copy" 5 seconds after the press, interrupting whatever the user does next.The shared live region that the Copy button of messages also uses is not used here, for two reasons:
<dialog>. Everything outside the dialog is inert. In Edge, a bot message added to the shared live region while the dialog was open raised no live region event.aria-pressedis removed, so the button is not exposed as a toggle button, like the Copy button of messages.Open question: in Safari, clicking a button does not focus it, so VoiceOver users who click with the mouse may not hear the name change. I have not verified whether VoiceOver announces the name change of the focused button. Before this change, the live region on the button covered that case. A host-independent announcement, such as
ariaNotify(), would cover both copy buttons. #5872 already listed it as a separate change.Follow-up after #5893 merges:
behavior.highlighted.htmlwill move to the fake clock too. With a fake clock the message arrives before Shiki has loaded, and only #5893's highlighter fix keeps that code block from staying empty.Specific Changes
Added
hooks/internal/useAutoResetState.ts: the timer of the 5 second "Copied" state, extracted fromActivityCopyButtonand made general:[value, setValue]that resets after an interval. Both copy buttons use it withfalseand 5 secondsCodeBlockCopyButton.tsx: usesuseAutoResetState(); removedaria-live,aria-pressedand theanimationendhandler; props validated with valibot and typed withInferReactProps; the default export is nowuseReactCodeBlockCopyButtonClass(), which returns[class]and passes the ponyfill to the custom element. The global classcode-block-copy-button--copiedis still set while confirmed, for host styling, likeactivity-copy-button--copiedCodeBlockCopyButton.module.css: removed the 0.5 second animation, and theopacity: 0of the element that carries the "Copied" name: it is empty, so the rule only mattered for the animationCustomElementsComposer.tsx: registers the class fromuseReactCodeBlockCopyButtonClass()__tests__/html2/markdown/codeBlockCopyButton/behavior.html: fake clock viaponyfill. It asserts "Copied" at 4.999 seconds and "Copy" at 5 seconds, that hiding Web Chat does not change it, the pasted code, and, withgetAccessibilityNode(), the button's name and role without a live region or pressed state. The snapshots are unchanged.adaptiveCards/behavior.html: fake clock; wait for "Copied", advance the clock by 5 seconds, and wait for "Copy", instead of a 500 ms sleep and an unstable snapshot.behavior.highlighted.htmlkeeps the real clock until Ensure 4.5:1 contrast for syntax-highlighted code and keep code blocks when messages are already in the store #5893 is merged.behavior.highlighted.htmlalso waits for the Python code to be highlighted. The snapshots are unchanged.The 3 tests stay JSX; they load
@babel/standalone@7.26.5(already used bycopyButton/behavior.hideAndShow.html), whosedata-type="module"lets them importwaitFor().I have added tests and executed them locally
I have updated
CHANGELOG.mdI have updated documentation
Review Checklist
z-index)package.jsonandpackage-lock.jsonreviewed