Skip to content

Editor: add copy option for save failures & a "show/hide details" button - #82496

Open
annezazu wants to merge 2 commits into
WordPress:trunkfrom
annezazu:add/copy-save-error-details
Open

annezazu wants to merge 2 commits into
WordPress:trunkfrom
annezazu:add/copy-save-error-details

Conversation

@annezazu

@annezazu annezazu commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

What?

Gives every save failure notice in the editor a copy-to-clipboard button, and moves the notice out of the notices store and into a component so it can have one.

Follows up on Defensive data design, which asks for exactly this next to error messages:

And have a copy-to-clipboard next to it, so people can put into search/AI/whatever

It builds on the save failure notices from #76470, which put the server's account of a failure behind a "Show details" disclosure.

Why?

An error message is a common time when someone turns for help and adding a copy button, along with "show details" helps make that just a little bit easier.

The disclosure added in #76470 was built by concatenating escaped HTML into the notice string and rendering it with __unstableHTML, because @wordpress/notices casts its content to a string. That flag's own type says it "SHOULD NOT be used for notices", and a string cannot hold an interactive control, so a copy button was not reachable without moving the notice into a component.

I pursued this path, partially because it looks like a better, more readable experience.

How?

createNotice accepts detail and puts it on the notice. NoticeList already forwards every notice field to Notice, so it arrives with no change to InlineNotices. Notice renders it behind a Collapsible disclosure, with the trigger and a copy button sharing the notice's existing actions row. Collapsible supplies aria-expanded, aria-controls and the panel wiring, and its panel uses hiddenUntilFound, so what the notice discloses stays reachable by the browser's find-in-page while collapsed.

The copy button is named for the notice — "Copy error" at the error status, "Copy details" otherwise — and keeps that name while a copy is confirmed through the live region, since renaming a focused element is announced inconsistently.

savePost passes the server's message instead of building HTML by hand, so it no longer needs the __unstableHTML flag, the speak: false option, or the manual speak() call that flag forced. Markup in a server message is stripped rather than the message being dropped, and only when there is a tag to strip, so a parse error that merely contains a < keeps everything after it. Block boundaries become line breaks first, so two sentences reach the clipboard separated rather than run together.

Also fixes InlineNotices, which treated an array of children as content even when every member rendered nothing, leaving an empty wrapper in the DOM.

Testing Instructions

  1. Open a post in the editor.

  2. Trigger a save failure that carries a server message. A quick way is an mu-plugin that rejects the save:

    add_filter( 'rest_pre_insert_post', function ( $prepared, $request ) {
        $title = is_array( $request['title'] ) ? ( $request['title']['raw'] ?? '' ) : $request['title'];
        if ( 0 !== stripos( (string) $title, 'break save' ) ) {
            return $prepared;
        }
        return new WP_Error(
            'rest_cannot_update',
            '<p>The <strong>title</strong> field was rejected by a server-side rule.</p><p>Remove &#8220;Break save&#8221; from the title, then try again.</p>',
            array( 'status' => 400 )
        );
    }, 10, 2 );

    Title the post Break save please and save.

  3. Confirm the notice shows "Show details" and a copy button.

  4. Expand it. Confirm the server's message appears, with its two sentences on separate lines and no markup.

  5. Press the copy button. Confirm the clipboard holds the notice message, a blank line, and the detail.

  6. Go offline (DevTools → Network → Offline) and save. Confirm the notice shows the copy button inline with the message, and no "Show details".

  7. Save successfully. Confirm the error notice is gone.

Testing Instructions for Keyboard

  1. With the failure notice showing, Tab to "Show details". Confirm Space and Enter both toggle it and focus stays on the button.
  2. Tab to the copy button and press Enter. Confirm the icon changes to a checkmark and a screen reader announces "Error copied to clipboard", and that the button's name stays "Copy error".
  3. With the details collapsed, use the browser's find-in-page to search for a word from the server's message. Confirm the panel expands and the match is found (Chromium; other browsers leave it collapsed).

Screenshots or screencast

An error with a server message to disclose:

Screenshot 2026-09-05 at 4 30 11 PM

An error without one, where the copy button sits inline:

Screenshot 2026-09-05 at 4 30 20 PM

Use of AI Tools

Claude Code was used for the implementation, the browser verification and this description.

@coderabbitai

coderabbitai Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. To trigger a review, include @coderabbitai review in the PR description. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 80b20695-b40d-4c72-ad17-a24b433e676b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

🎉 Props

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: annezazu <annezazu@git.wordpress.org>
Co-authored-by: mcsf <mcsf@git.wordpress.org>
Co-authored-by: jasmussen <joen@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
Co-authored-by: aduth <aduth@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Updated as activity occurs, without notifying anyone named here. Add the props-bot label to refresh.

@github-actions github-actions Bot added [Package] Editor /packages/editor [Package] Notices /packages/notices labels Sep 5, 2026
@annezazu
annezazu requested a review from youknowriad September 5, 2026 23:39
@annezazu annezazu changed the title Editor: offer save failures to the clipboard Editor: add copy option for save failures & a "show/hide details" button Sep 5, 2026
@ciampo
ciampo requested a review from a team September 8, 2026 15:57
@jasmussen

Copy link
Copy Markdown
Contributor

Good thoughts, and seems to follow the same error message guidelines that exist in Storybook, which were informed by the same source.

This PR uses Notice because the new Notice V2 (from @wordpress/ui) is blocked from use by the linter. But using that new notice should make it possible to improve the design like so:

image

@youknowriad

Copy link
Copy Markdown
Contributor

Looks like the first step here is going to be to update the "notices" package to rely on the wordpress/ui instead of wordpress/components.

@aduth

aduth commented Sep 15, 2026

Copy link
Copy Markdown
Member

Looks like the first step here is going to be to update the "notices" package to rely on the wordpress/ui instead of wordpress/components.

Related: #82701 (comment)

With the Notice.List component being proposed there, it's a fairly straightforward swap of the internal implementation of @wordpress/notices.

@jasmussen

Copy link
Copy Markdown
Contributor

Just to clarify, I don't think this PR needs to be blocked by using Notice V2. But I'd just use a text button instead of the text + icon button, in Notice V1, rather than the icon only button as is used at present, which looks off.

@youknowriad

Copy link
Copy Markdown
Contributor

For me we should swap the notices first, not because of the design itself but because the current PR is forcing the editor to have two ways to render notices.

@aduth

aduth commented Sep 18, 2026

Copy link
Copy Markdown
Member

Hey 👋 I wanted to give you a heads-up since this pull request is affected by recent validation changes for changelog files.

#83043 adds additional validation for changelog files. You'll note that this pull request is currently failing a "Required changes from trunk" check.

What you'll need to do: You will need to either rebase or merge the latest code from trunk. In addition, a cursory review of open pull requests identified this pull request as potentially failing under the new validation checks. You will want to double-check that any changes to CHANGELOG.md files follow the Maintaining Changelogs guidance, which has been improved as part of these recent changes.

@mcsf mcsf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This isn't a review, just some preliminary observations before I have a proper pass at the PR :)

Comment thread packages/notices/src/components/inline-notices/index.tsx Outdated
Comment thread packages/notices/CHANGELOG.md Outdated
annezazu and others added 2 commits September 22, 2026 16:41
More than one child arrives as an array, and each one is typically
rendered conditionally. The check looked at the array itself, which is
neither `false` nor empty, so `[ false, false ]` counted as content and
left an empty wrapper in the DOM.

`Children.toArray` flattens that array and drops the nothings along the
way — `null`, `undefined`, and the booleans a `&&` leaves behind — so
only the empty string is left to check for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A notice's content is a plain string: `createNotice` casts it, with a
comment saying a React element is not supported. The only way to carry
more than a sentence was to build escaped HTML into the message and
render it with `__unstableHTML`, whose own type says it should not be
used for notices.

Adds a `detail` option to `createNotice` and a matching `detail` prop on
`Notice`. It holds a fuller account as plain text, sits behind a native
`details` disclosure, and is offered for copying together with the
message, so a failure can be pasted into a search or an assistant
without being retyped by hand.

`NoticeList` already forwards every notice field to `Notice`, so a
notice dispatched with a detail reaches the rendered notice with no
change to `InlineNotices`.

Save failures use it in place of the escaped HTML they built by hand,
which retires the `__unstableHTML` flag and the manual `speak()` call
that flag forced. Markup in a server message is stripped rather than the
message being dropped, and only when there is a tag to strip, so a
message that merely contains a `<` keeps everything after it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@annezazu
annezazu force-pushed the add/copy-save-error-details branch from 5f9d9a1 to eaa3e34 Compare October 1, 2026 22:28
@github-actions github-actions Bot added the [Package] Components /packages/components label Oct 1, 2026
@annezazu
annezazu marked this pull request as ready for review October 1, 2026 22:33
@ciampo ciampo added the [Type] Enhancement A suggestion for improvement. label Oct 2, 2026

@ciampo ciampo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Left a few more comments. We'll also need to rebase, solve conflicts, and make sure CHANGELOG entries are in the latest unreleased section.

One aspect to reason about carefully is parity between @wordpress/components and @wordpress/ui Notice.

In the new Notice we don't support a description that can be hidden behind a disclosure, and we don't embed a copy button either.

Let's agree on what features we will want to support in the @wordpress/ui Notice "natively" vs via composition of other components, and thay will inform what features should be part of Notice vs which ones would be composed by external consumers.

Adding a disclosure subcomponent may be easy enough, but adding a notice-specific copy button would be trickier, because it may be difficult to know exaclty which content to copy to the clipboard (unless we decide explicitly that 1 disclosure = 1 copy button that lives inside the diclosure).

cc @WordPress/gutenberg-components @WordPress/gutenberg-design

Comment on lines +52 to +60
const ref = useCopyToClipboard< HTMLButtonElement >(
`${ message }\n\n${ detail }`,
() =>
speak(
isError
? __( 'Error copied to clipboard.' )
: __( 'Notice copied to clipboard.' )
)
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we want to gate the copy button only to notices that have details ?

Comment on lines +116 to +123
/**
* A fuller account of what went wrong, as plain text, for when the message
* cannot say everything a developer needs — the server's own report of a
* failure, say. It sits behind a disclosure so it stays out of the way of
* everyone else, and is offered for copying together with the message,
* ready to paste into a search or an assistant.
*/
detail?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should probably be more generic in the description, rather than implying that this is used for errors.

Also, maybe description is a better name, as it aligns more closely with the Notice.Description subcomponent in the new Notice component in @wordpress/ui?

{ detail ? (
<NoticeDetail
detail={ detail }
message={ toText( spokenMessage ) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We may want to derive plain text from the visible content (and not spokenMessage) for copying. JSX without an override currently copies HTML tags, and speak: false removes the main message from the copied text.

Comment on lines +14 to +21
// The clipboard itself is the browser's, so what is asserted here is what the
// component hands it.
vi.mock( import( '@wordpress/compose' ), async ( importOriginal ) => ( {
...( await importOriginal() ),
useCopyToClipboard: vi.fn(),
} ) );
const mockedUseCopyToClipboard = vi.mocked( useCopyToClipboard );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We could refactor these tests to be Vitest Browser tests, so that we don't need to mock the clipboard functionality

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Components /packages/components [Package] Editor /packages/editor [Package] Notices /packages/notices [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants