Repository navigation
Add emoji picker with Autocomplete grid - #83538
adamsilverstein wants to merge 445 commits into
Conversation
…ence contracts Two regression guards specific to the Dropdown migration: 1. `reaction picker portals outside the collab sidebar` — asserts the popover content has no `.editor-collab-sidebar-panel` ancestor, so that reverting the portal (e.g. by switching back to a non- portaling primitive) fails loudly instead of silently re-clipping. 2. `note remains selected while reaction picker is open` — asserts the thread keeps `aria-expanded="true"` after the picker opens, pinning the `.components-popover` exemption in `note-thread.js`'s onBlur handler. Without this, removing that exemption would unmount the trigger and close the picker mid-click — but the existing 9 tests would still pass because they don't observe selection state between open and close.
`Dropdown` applies `padding: 8px` to the inner
`.components-popover__content` by default. Combined with the
`.components-popover__content { width: min-content }` default and a
fixed `width: var(--wpds-dimension-surface-width-md)` on the inner
Frimousse picker, the popover surface ended up 16px wider than the
picker — visible as a band of empty surface to the right of the grid.
`contentClassName` from `Dropdown` lands on the outer
`.components-popover` wrapper, not on `.components-popover__content`,
so the previous `padding: 0` rule didn't override Dropdown's default.
Nest the override through `.components-popover__content` to bump
specificity above `.components-dropdown__content .components-popover__content`.
Drops the dead `.editor-collab-sidebar-panel__add-reaction-popover`
rule (also targeting the wrong layer) — Dropdown's default 8px
padding is what the curated picker wants anyway.
Adds an E2E regression guard `Frimousse popover wraps tightly to the
picker width` that compares bounding boxes; preferred over a pixel
screenshot since emoji rendering differs across OSes.
…urface token The picker was set to var(--wpds-dimension-surface-width-md) (~400px), but the 8-column emoji grid only consumes ~252px. The visible result was emojis filling the left two-thirds of the popover with a wide empty band on the right. Width now derives from the actual grid: (8 × 32px button) + (7 × 4px gap) + (2 × 4px viewport padding) + a 17px scrollbar gutter, so the popover wraps the visible emoji area and the buttons don't shrink under flex pressure when the scrollbar is present. Two related changes: - Bumps the emoji button from 28px to 32px (better tap target, matches GitHub/Slack-style pickers). - Sets `scrollbar-gutter: stable` on the viewport so the gutter is always reserved — without it, the layout would shift when the picker scrolls. Adds an E2E regression guard `emoji grid fills the picker width` that asserts the last emoji's right edge sits within ~24px of the picker's right edge (covering only viewport padding + scrollbar gutter). The original bug had ~145px of slack; this catches anything remotely close to that.
Two issues conspired to make the same emoji reaction unre-addable on the note (or block) it had just been removed from. Server: the duplicate-reaction guard on POST /comments queried existing reactions with `status => 'any'`, which includes trashed comments. Since WP REST DELETE trashes comments by default, the just-removed reaction was still present in trash and matched the duplicate check, returning `rest_comment_duplicate_reaction`. Limit the lookup to `status => 'approve'` — only active reactions can legitimately block a re-add. Client: `reactionsMap` is derived from each note's server-computed `reaction_summary` field, cached on the note's entity record. Mutating a reaction comment doesn't invalidate that field, so a subsequent toggle on the same emoji read stale `reacted` / `my_reaction_id` values and tried to delete a now-missing record instead of routing into the add path. Invalidate the notes list resolution after each toggle so each parent's `reaction_summary` refetches. Adds `can re-add the same reaction after removing it` E2E test. Verified to fail without either fix and pass with both.
The file contained only a docblock and was require'd from lib/load.php. Block-comments / notes compat lives in wordpress-7.1/block-comments.php now; this shim was a leftover with no callers.
Two follow-ups from review of the re-add fix.
1) Force-delete the reaction comment instead of trashing it.
`deleteEntityRecord` defaults to the WP REST trash workflow, so
each toggle-off was leaving a row behind in `wp_comments`. Reactions
don't have a trash UX, so trashed reactions are pure debris that
accumulates unboundedly with usage. Pass `{ force: true }` so the
row is removed.
2) Replace full notes-list invalidation with a targeted single-record
refetch. The previous fix invalidated `getEntityRecords` for the
entire post's notes, causing a full list re-fetch (response payload
linear in N notes) on every emoji click. Instead, fetch only the
parent note via apiFetch and merge it back via
`receiveEntityRecords` with no `query` arg — that updates the
per-record cache, which the list selector reads through by ID. The
LIST view picks up the fresh `reaction_summary` without re-fetching
any other note.
All 38 block-notes E2E tests still pass.
… picker (#78129) Drops the Frimousse dependency in favor of a WordPress-native emoji picker built from existing components. Routes UI strings through GlotPress and ships per-locale Emojibase data for 28 locales (vs. English only previously). Same UX, same data source, ~125 net lines in packages/editor.
Keep this PR focused on the foundation for basic emoji reactions: the curated 5-emoji quick row served from REST schema, the reaction custom comment type, and the reaction_summary aggregation. The "+" More-emojis trigger, the lazy-loaded native SearchControl+Composite picker, the per-locale Emojibase data plumbing (build copy step, PHP inline-script registration, label-overrides filter), and the emojibase-data devDependency move to a follow-up PR stacked on this one. Picker library selection and bundling strategy still need wider review, per t-hamano in #76767.
Both bits only existed to support the full emoji picker, which is now a follow-up PR. Without the `+` trigger, no caller ever passes a hex codepoint key into the reaction storage, so the encode/decode pair and the HEX_KEY_RE fallback in getEmojiBySlug/getLabelBySlug never fire. The `.editor-collab-sidebar-panel__more-reaction-button` rule styled that trigger and has no remaining consumer.
Four near-identical tests (no parent, parent is a regular comment, content not in the emoji list, anonymous user) all built the same POST request and asserted a specific WP_Error. Replace them with a single parameterized test_cannot_create_reaction_with_invalid_input plus a named-key data provider, so the matrix of error cases is readable at a glance and each new case is one row instead of a new method. Net -23 lines; the duplicate-reaction and "can stack different reactions" tests stay separate because their request shape differs.
Five emojis fit in a single ~176px-wide row that drops below the smiley trigger without clipping. This matches the dominant pattern for emoji reactions (Facebook/Slack/GitHub/Linear/Notion) so the affordance reads as reactions instead of a generic dropdown menu. Composite gets orientation="horizontal" so ArrowLeft/ArrowRight drive the keyboard nav. The e2e test that exercises arrow keys is updated to match.
Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com>
Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com>
Apply review feedback: drop the bespoke 28px-with-!important sizing on the curated add-reaction button in favor of the standard Button "small" size with iconSize=20. Removes redundant flex/centering CSS the Button component already handles.
A resolved note thread shouldn't accept new reactions — they're for in-progress conversations. Disable the curated reaction picker toggle when the note's status is "approved" and keep it accessible so screen readers still announce the control's disabled state.
Replace per-render array.find() calls in ReactionDisplay with a Map keyed by slug, memoized against the emoji list. O(1) lookup keeps the render cheap if the curated set grows (full picker landing later via the stacked PR).
Per review feedback, the smiley face shouldn't be promoted to the public icons API while the reactions feature is still settling. Move the SVG back inline in reaction-display.js (using SVG/Path from @wordpress/primitives so it stays cross-platform) and remove the smiley entry from @wordpress/icons' manifest and library.
Require edit_post capability to create a note or reaction, mirroring core's note guard, so the check no longer depends on a status field being present in the request. Also validate that a reaction's parent note belongs to the same post the reaction targets. Addresses review feedback on PR #76767.
The prepare_links override repointed a note's _links.children from its replies (type=note) to reactions (type=reaction), breaking core's reply-embedding contract for clients relying on _embed. Reactions reach the client through the reaction_summary field and an on-hover fetch, so the override served nothing. Remove it and inherit core's behavior. Addresses review feedback on PR #76767.
The reaction picker fetched the curated emoji list via OPTIONS /wp/v2/comments, reading it from a reaction_emojis schema default. OPTIONS is for discovering allowed operations, not data retrieval, and the curated set is a fixed five-emoji row. Use the hardcoded REACTION_EMOJIS list directly, drop the useReactionEmojis hook and the client request, and remove the read-only reaction_emojis schema property. The gutenberg_note_reaction_emojis filter still drives server-side validation. Addresses review feedback on PR #76767.
…plugin class The reaction controller extended Gutenberg_REST_Comment_Controller_6_9, but that plugin class was deleted when the minimum WordPress version was bumped to 6.9 (notes graduated to core). Loading the plugin therefore fataled with 'Class not found', which cascaded into every PHP, Playwright and performance CI job. Extend the core WP_REST_Comments_Controller directly, add a local check_post_type_supports_notes() (core declares it private, so it cannot be reused from a subclass), and drop the now-dead remove_action() for the deleted 6.9 registration function.
Replace the .editor-collab-sidebar-panel__reaction-button CSS-class
locators with getByRole( 'button', { name: /<emoji>/ } ), following the
Playwright guidance to prefer user-facing attributes over CSS selectors.
The reaction pill's accessible label always contains the emoji name, so
the regex match is stable across the count and tooltip label variants.
Addresses review feedback from @manzoorwanijk.
The numbered-step comments were removed from the build script because they get unwieldy when steps are inserted between existing ones. Drop the lone leftover marker. Addresses review feedback from @manzoorwanijk.
Core's check_read_permission() only excludes the 'note' comment type from
the public read path, where an approved comment on a readable post is
world-readable. Reactions are an internal comment type too, so a direct
GET /wp/v2/comments/{reaction_id} fell through to that public path and
exposed reaction rows - including the reacting user's identity - to any
reader of a published post. The collection endpoint already blocks
non-editors from querying type=note|reaction, but the single-item-by-ID
path did not.
Override check_read_permission() to give reactions the same restriction
core gives notes: never public, readable only by the reacting user or a
user who can edit the comment. This also closes the direct-ID read path
flagged for internal types. Add coverage asserting logged-out and
subscriber reads are denied while the author and an editor are allowed.
Addresses review feedback from @scruffian.
The comment-count query filter bailed entirely whenever the query already contained the note-only exclusion (comment_type != 'note'), which core and older revisions of the filter inject. In that case the 'reaction' type was never excluded, so reactions inflated the post comment count. Iterate the internal comment types and append an exclusion clause for each one not already present, instead of skipping when any exclusion exists. This guarantees reactions are excluded too and keeps the filter idempotent across multiple runs. Addresses review feedback from @scruffian.
Store the validated reaction slug as the comment content instead of the raw request value, so input like '<b>heart</b>' is persisted as 'heart' and stays consistent for reaction grouping and counts. Also reject hex-codepoint slugs that are not assignable Unicode code points (above U+10FFFF or in the UTF-16 surrogate range), which the shape-only regex previously accepted. Addresses CodeRabbit review (M2, m6).
The cached tooltip user-name list was only cleared when toggling an existing reaction pill. Adding the same slug through the picker updated the count but left a stale tooltip. Thread noteId into AddReactionButton and clear the cache entry on the picker path too. Addresses CodeRabbit review (m8).
Wrap the reaction-emoji filter test in try/finally so a failed assertion cannot leak the gutenberg_note_reaction_emojis filter into later tests. Replace the flaky cumulative toHaveCount( 2 ) snackbar assertion in the re-add e2e with per-add checks: assert and dismiss the first success snackbar, then assert the re-add surfaces its own, instead of requiring both auto-dismissing snackbars to be present simultaneously. Addresses CodeRabbit review (m9, m10).
The pre-insert uniqueness check for reactions is not atomic: two near-simultaneous requests for the same user, note, and emoji can both pass the check and insert an approved row, since reactions deliberately bypass core's wp_allow_comment() duplicate detection. After a successful reaction insert, re-query the user's approved reactions for the note and, when more than one matches the slug, keep the earliest (lowest comment ID) and delete the rest. Every concurrent request applies the same deterministic rule, so they all converge on the same surviving row; a request whose own row lost the race repoints its response to the survivor. Add a test that injects a competing reaction via rest_pre_insert_comment (after the pre-insert check, before the insert) and asserts a single row survives and the response references it. Addresses CodeRabbit review (M3).
…dditional-comment-type' into add-notes-emoji-reactions-full-picker # Conflicts: # lib/compat/wordpress-7.1/block-comments.php # lib/compat/wordpress-7.1/class-gutenberg-rest-comment-controller-7-1.php # lib/load.php # package-lock.json # packages/editor/src/components/collab-sidebar/hooks.js # packages/editor/src/components/collab-sidebar/note.js # packages/editor/src/components/collab-sidebar/reaction-display.js # packages/editor/src/components/collab-sidebar/reaction-emoji-picker.js # packages/editor/src/components/collab-sidebar/style.scss # phpunit/experimental/class-wp-rest-comments-controller-gutenberg-test.php # test/e2e/specs/editor/various/block-notes.spec.js
Forced colors repaint text in a system color, flattening the pill emoji to monochrome. Ported from #83538. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFhn8zx9436RNBydSzNUFh
| * @param WP_REST_Request $request Full details about the request. | ||
| * @return true|WP_Error True if the request has access to update the item, error object otherwise. | ||
| */ | ||
| public function update_item_permissions_check( $request ) { |
| // A reaction under a hidden note would escape the trash cascade. | ||
| if ( in_array( $parent_comment->comment_approved, array( 'trash', 'spam' ), true ) ) { |
| className="editor-collab-sidebar-panel__picker-viewport" | ||
| tabIndex={ -1 } | ||
| > | ||
| <Autocomplete.Status>{ status }</Autocomplete.Status> |
| path: addQueryArgs( '/wp/v2/comments', { | ||
| parent: noteId, | ||
| type: 'reaction', | ||
| status: 'all', |
Applies the REST and lifecycle review feedback from #83538 so both branches share one backend: store reactions as emoji hex keys, collapse reaction_summary entries to count and current_user_reaction, pin reactions to the current user, reject reactions on trashed or spam notes, keep reactions out of comment feeds, accept content as a string or { raw }, and drop the untrash cascade. With only five emoji on offer, keys are checked against that set rather than for Unicode syntax, so no non-emoji key can be stored. The backport changelog moves to 7.2 alongside the compat code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFhn8zx9436RNBydSzNUFh
Follows the architecture feedback on #83538: the reaction modules live under collab-sidebar/reactions/, and useReaction( note ) reads the note's own reaction_summary and owns the toggle, so the reactions map and toggle callback no longer thread through the sidebar, the notes list and every thread. The picker is now a @wordpress/ui Menu, which brings menu semantics, an accessible name and Escape handling without the Dropdown wrapper CSS. Reactions are stored as emoji hex keys, and the pills share one reactor-name fetch per note instead of one per emoji. The component tests go; the remaining unit tests cover the summary delta and the shared name fetch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFhn8zx9436RNBydSzNUFh
Cuts the reaction suite from 15 tests to 9, per the test-trimming feedback on #83538. Removal is covered by the re-add test, and the Menu now owns the popup layout, portal and arrow-key behavior that the dropped tests pinned. The hover tests fold into one, and the keyboard test now drives the Menu, including Escape returning focus to the trigger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QFhn8zx9436RNBydSzNUFh
|
Going to try and leave some skimming comments: I apologize if they have already been answered.
As far as things like flags and Twemoji go, I wouldn’t personally recommend trying to account for them. Twemoji itself is somewhat of a painful compromise for site renders because it provides almost no value but blocks the rendering entirely. But there is a big difference between Twemoji’s use on the front-end and the editor: Twemoji is about making content accessible to readers. The editor is an environment where the author expects to be able to create whatever the platform allows them. There is questionable gain for the complexity introduced on Windows of entering two-character Unicode flags if the person using the editor cannot enter those flags in any other part of their site, and if those flags render broken in the editor. In other words, we may have a helpful scope-cut by saying “this only works as well as the native support on the running operating system.” Some editors may get surprising results, ala the famous yellow heart, but platform support is updated more frequently than it ever was before, and is probably continuing to speed up. Did we look at, or is this Nolan Lawson’s What is the general approach for search, search indexing, and partial matches? As a reaction picker I can see the value in having a separate panel, but I would think that if it’s possible to realize this through the high-performance C++ code provided by the platform, then that would be preferable in all ways. I didn’t try this, but OS pickers tend to provide things like Fitzpatrick modifiers, gender selectors, hair color modifier, and other multi-code-point grapheme clusters which people are used to with their mobile devices (at least, if not also on the desktop). |
He @dmsnell - thanks for the review and feedback. As you pointed out, building an emoji picker from scratch is complex and full of all kinds of gotchas. I did spend some time reviewing existing open source solutions without finding anything that was license compatible and that we could use WordPress localization on. I'm not sure I looked at the one you referenced, unfortunately it is Apache licensed and thus not compatible with WordPress unless we request dual licensing (which we have done successfully elsewhere, most recently for libultrahdr) .
using the native picker sounds dreamy, but I'm not sure its technically possible to invoke the native picker directly from the web platform. Still, I'll research that further to make sure. I know in the past this wasn't as straightforward as it sounds.
The current PR takes advantage of a Gutenberg autocomplete grid component, so most of what you are asking about is handled by the component. For what its worth, the team was already building the component, its something we will likely use elsewhere. I'm just using it for emoji.
Yes, my goal would be to lazy load the data so its only loaded if you go to insert an emoji reaction.
Good to know! We still need some way to address platform support differences, but maybe we can use capability detection and fall back to the simplified version. |
…er-autocomplete-grid
Trunk's draft-preserving reply form (#84110) carries the note class, so the hover locator matched two elements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L7rwHtVAa37GWJPC8P3Cwd
Core's delete check lets anyone who can edit the note's post remove a reaction, but #75144 makes removal author-only. Reactions on a resolved thread were only blocked by disabled controls, so a stale tab could still add one. Reject both in the REST controller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L7rwHtVAa37GWJPC8P3Cwd (cherry picked from commit c0df5e3)
The curated reaction set reads by shape alone, so honor the user's high-contrast preference instead of opting the glyph out of it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L7rwHtVAa37GWJPC8P3Cwd (cherry picked from commit a6b8e41)
A corrupt comment_parent loop would otherwise spin the request forever. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L7rwHtVAa37GWJPC8P3Cwd (cherry picked from commit 9d7274c)
A refresh from another emoji's toggle can clear current_user_reaction while a delete is pending, so only fold the removal in when the cached entry still holds that reaction. A second click while a toggle is in flight read the same snapshot and repeated the request, surfacing a 404 or 409 even though the first one landed; ignore it until the first one settles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L7rwHtVAa37GWJPC8P3Cwd (cherry picked from commit 0eecd1a)
…er-autocomplete-grid # Conflicts: # packages/editor/CHANGELOG.md
Reviewers on the matching core change prefer not to define what counts as an "internal" comment type before a custom comment types API exists, since reactions may later apply to public comments too. Drop the gutenberg_get_internal_comment_types() helper and its filter, and list 'note' and 'reaction' at each call site, as on #76767. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THCU4aj11hnJP5ZbUegTRh
Format the count for the user's locale in both the pill and its accessible label, as on #76767. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THCU4aj11hnJP5ZbUegTRh
it happens automatically for text input fields. I’m not sure if it’s possible outside of that, or if there’s a reasonable way to auto-invoke it. it was just a crazy “have we tried this?” question. on everything else, it all makes sense. I would love to avoid shipping all the data, even lazily, because that could also end up introducing a lot of lag when adding the first reaction, but this is I think an inherently unfortunate scenario. if we perform search on the server, that might reduce latency but increase server load, as it would be served dynamically instead of a large static bundle. however, that might also make it more feasible for people to add their own WPmoji the way Slack does, and the grid could be simplified to a simple my comments are meant to be helpful outside questions, not demands. you are welcome to ignore me. |
The picker now outlines the emoji the user already reacted with, and names them as such, so picking one again reads as a toggle. On a resolved thread the add-reaction trigger was shown disabled on some notes and hidden until hover on others; hide it entirely, as on #76767. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THCU4aj11hnJP5ZbUegTRh
The typecheck rejected the picker's trigger element, whose props were typed as unknown, and the grouped-or-flat items union, which matches neither of Base UI's Autocomplete.Root overloads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01THCU4aj11hnJP5ZbUegTRh
I'm not sure average users knpw about the native pickers or how to invoke them. I only know how to use them on the mac because a key on my keyboard is mapped to the picker 😀 and still I rarely if ever use the native picker. Still, I will investigate if the idea is possible.
We can host the data on .org and load it from there if shipping with wp is a concern, I believe a previous review from @t-hamano brought this up, although I can't find the conversation at the moment.
I'm working on extensibility of the emoji set in a follow up:
Thanks! I'm going to pay attention to you even if I don't always agree 😄 (colon smile colon) |
|
Merged trunk and brought over the recent review fixes from #76767. Claude ported these, summary below:
|
Do we need to allow any emoji? The linked issue says:
It would be cool (even 🆒 or 😎) if any emoji could be selected. But this seems excessive maybe, and is shying away from decisions not options. A simple picker of the fixed set would greatly simplify this. Maybe it could be extensible so a plugin could add a full emoji picker for sites that want it. (Maybe this has already been discussed. I haven't kept up with this closely.) |
|
@westonruter, the PR with the curated list of reactions is in progress here: #76767. |
I've decided to give you the choice of any emoji! Just kidding! Looks like there is more consensus on the curated list of emoji PR so we will start with that. I had always planned the full picker as a potential follow up and it was a good exploration of using the autocomplete grid. Hopefully we can revisit this in the future, I'm fairly sure people will want the full set of emoji once they start using them. |
…er-autocomplete-grid # Conflicts: # packages/editor/CHANGELOG.md # packages/editor/src/components/collab-sidebar/README.md # packages/editor/src/components/collab-sidebar/hooks.js


An alternate take on #78176, so the two approaches can be compared side by side. Standalone against trunk: includes the changes from #76767 (previously stacked on it).
Fixes #75144
To keep this one reviewable, the filters for customizing the emoji set, exclusions, and labels moved to a stacked follow-up, #84043.
This round addresses the feedback from @mirka and @fcoveram. The picker now follows the documented
Autocompletepatterns without patching the component's behavior, and the rough edges we hit are collected below under "Autocomplete notes" to pass back.Claude reworked the picker and wrote up the changes:
pr83538-autocomplete-screencast-v3.mp4
AI Use
Code and description came out of a few rounds of back and forth with 🤖 Claude Code. I will review and test.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Jx9yDR5xxMRczChFCjzBNu