Repository navigation
Show the code block copy confirmation for 5 seconds, like the message Copy button - #5892
Eugene (OEvgeny) wants to merge 4 commits into
Conversation
| WebChat: { ReactWebChat, testIds } | ||
| } = window; // Imports in UMD fashion. | ||
|
|
||
| // Using the real clock: with a fake clock, the message would arrive before the code highlighter is loaded. |
There was a problem hiding this comment.
Yea, but still using a fake clock and then wait 5 seconds for "Copied -> Copy" is not a good idea.
| readonly() | ||
| ); | ||
|
|
||
| type CodeBlockCopyButtonProps = InferInput<typeof codeBlockCopyButtonPropsSchema>; |
There was a problem hiding this comment.
We have a new inference helper.
| type CodeBlockCopyButtonProps = InferInput<typeof codeBlockCopyButtonPropsSchema>; | |
| type CodeBlockCopyButtonProps = InferReactProps<typeof codeBlockCopyButtonPropsSchema>; |
There was a problem hiding this comment.
Done, CodeBlockCopyButtonProps is now InferReactProps<typeof codeBlockCopyButtonPropsSchema>.
There was a problem hiding this comment.
Maybe rename this to useAutoResetState(resetIntervalInMS: number): readonly [...] so it become a bit more useful outside of copy button when needed.
There was a problem hiding this comment.
Renamed to useAutoResetState(initialValue, resetIntervalInMS, ponyfill): readonly [value, setValue]: like useState(), but the value goes back to initialValue after the interval and setting it again restarts it; both copy buttons call it with false and 5 seconds, and the ponyfill stays a parameter because the code block button renders in its own React root without usePonyfill().
| * | ||
| * The custom element renders in its own React root, outside of the React tree of Web Chat, so the ponyfill is passed down. | ||
| */ | ||
| export default function useReactCodeBlockCopyButtonClass(): typeof HTMLElement { |
There was a problem hiding this comment.
Property hook always return in array.
| export default function useReactCodeBlockCopyButtonClass(): typeof HTMLElement { | |
| export default function useReactCodeBlockCopyButtonClass(): readonly [typeof HTMLElement] { |
There was a problem hiding this comment.
Done, useReactCodeBlockCopyButtonClass() now returns a frozen [class] and CustomElementsComposer destructures it.
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()
0da9947 to
ab7244c
Compare
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