Skip to content

Add emoji picker with Autocomplete grid - #83538

Open
adamsilverstein wants to merge 445 commits into
trunkfrom
try/notes-emoji-picker-autocomplete-grid
Open

adamsilverstein wants to merge 445 commits into
trunkfrom
try/notes-emoji-picker-autocomplete-grid

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

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 Autocomplete patterns 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:

What?

Rebuilds the Notes full emoji picker from #78176 on the Design System Autocomplete primitive from @wordpress/ui, in its grid mode (inline, always open), instead of the Composite grid plus SearchControl from @wordpress/components.

Why?

Addresses @fcoveram's keyboard feedback on #78176: getting from the search field to the grid took several Tab presses. With Autocomplete the search field is a combobox that drives the grid through aria-activedescendant, which is the macOS behavior from the review:

  • focus stays in the search field
  • the arrow keys move a highlight through the grid, crossing category boundaries
  • Enter picks the highlighted emoji
  • after typing, the top result is highlighted, so type + Enter picks it
  • one Tab reaches the skin-tone button

How?

  • Every reaction, including the five quick ones, is stored as its Emojibase hex key (2764, 1f680), and reaction_summary is keyed the same way. The server accepts a valid hex key and nothing else.
  • If the Emojibase data cannot load, the picker shows "Couldn't load emojis." instead of falling back to the quick reactions.
  • Composed like the documented async items story (https://wordpress.github.io/gutenberg/?path=/docs/design-system-components-form-primitives-autocomplete--docs#async-items): the results go to Autocomplete.Root through items, and Autocomplete.Status and Autocomplete.Empty handle the loading state, the result count, and the empty state. The list stays mounted.
  • filter={ null } instead of mode="none", so aria-autocomplete stays accurate. Filtering is still ours (curated labels, Emojibase labels and tags): items holds the already-filtered results, as category groups while browsing and a flat list while searching.
  • The speak() count announcements, the item-press guard on onValueChange, and the scroll reset on each query are gone. The component's own behavior applies: Status announces, and autoHighlight scrolls the top hit into view.
  • Autocomplete.Group / GroupLabel / Row / Item render grid, rowgroup (labelled by the heading), row, and gridcell.
  • The input is the Design System Input with a search icon prefix and an Autocomplete.Clear suffix.
  • Spacing per @fcoveram's review: gap-sm between the search field and the skin-tone toggle, no extra space below each row or around the list, and group labels keep the Autocomplete inline padding.
  • The popover no longer jumps when it opens or collapses when a search has no results (@mirka's video). The grid's width is held on the list and on the loading state, so the popover keeps one size throughout.

Autocomplete notes

@mirka asked to hear about styling difficulties with the current Autocomplete. These are the places where the picker still has to add its own styles, plus a couple of behaviors worth documenting. None of them patch the component; they are listed here so they can go back into Autocomplete or its docs if that makes sense.

  1. Inline mode has no width when empty. With inline, nothing gives the list an intrinsic width, so when Empty shows (or while loading) the container collapses to the padding and a surrounding popover resizes and repositions. Popup has width variants; inline has nothing comparable. The picker sets min-inline-size on the List.
  2. Sticky group labels. Long grouped lists read better with the heading pinned while its section scrolls. GroupLabel has no background, so making it sticky means adding position: sticky, a z-index, and a background that matches whatever surface it sits on (here the @wordpress/components popover's $white, which is not a token).
  3. Item styles assume a list row. Item brings margin-inline, asymmetric inline padding for a leading icon, left alignment, and a size-md min height. A grid cell wants a centered square with no padding, so the picker overrides justify-content, height, padding, and font-size. The Grid story does the same with inline styles, so a cell size or variant for grid mode might be worth having.
  4. Row layout is left to the consumer. Row has no layout styles, so the column template and gap are the picker's. Probably intended, but a line in the docs (or a columns option) would help the next grid.
  5. Inline scroll container. The List styles are built for Popup's grid areas (scrollable / footer, and Empty uses grid-area: main). Inline, the consumer brings its own scroll container, so scrollbar-gutter and the scroll-padding that keeps a highlighted cell clear of a sticky heading are ours. Documenting ListBody as the inline scroll container might cover this.
  6. Status updates on every keystroke. The count re-renders in the polite live region per keystroke. The previous version debounced it by 500ms, like the block inserter, so screen readers hear the settled count. Not tested with a screen reader yet; a debounce option or docs guidance could help.
  7. Pressing an item writes its value into the input. onValueChange fires with reason item-press and the item's string value. Harmless here, since picking closes the popover (in testing the field kept the query until it unmounted), but a picker that stays open would need to filter on reason. Worth a note in the docs.
  8. Performance with ~1,900 items. All 1,919 cells mount; rows use content-visibility: auto. In headless Chromium on a Mac: open 165-190ms, first keystroke ~40ms, later keystrokes under 20ms, clearing the search ~55ms. No virtualization needed at this size; it was not tried with grid.

Comparison with #78176

Measured before this round, in headless Chromium against the same wp-env, same Emojibase data (1,919 gridcells), dataset already cached, median of 7 opens, two runs each. Open time is from the click to the painted grid.

#78176 (Composite) This PR (Autocomplete grid)
Search field to grid Several Tabs, then roving tabindex Arrow keys from the field, focus stays in the field
Enter on a search result Tab into the grid first Picks the highlighted (top) result
Crosses categories with arrows Yes Yes
Input semantics searchbox + separate grid combobox with aria-haspopup="grid", aria-controls, aria-activedescendant
Open, median 329-344ms 210-274ms
Longest task on open 245-262ms 78-112ms
Type one letter ("h") ~124ms 67-76ms
Clear the search 95-98ms 105-121ms

Timings move by a few tens of ms between runs, so read them as "not slower" more than as a precise speedup. The numbers after switching to items are in note 8 above; they were taken with a different harness, so they are not directly comparable to the table.

Risks and open questions

  • The search field is a combobox, not a searchbox. Screen reader testing would be good, especially VoiceOver's reading of the active cell and the Status count.
  • Escape closes the whole picker on the first press, even with a query typed.
  • Group headings use the Design System label style (per @mirka) rather than Heading/md from the design spec.

Testing Instructions

Test in WordPress Playground

  1. Enable Notes, add a note to a block, and click the "Add reaction" button on it. The popover should open at its final size and position, without a jump (easiest to see with scrollbars set to "Always" in macOS settings).
  2. Without leaving the search field, press ArrowDown, then move around with the arrow keys; the highlight should cross from "Frequently used" into "Smileys & Emotion" and beyond.
  3. Press Enter; the highlighted emoji is added as a reaction.
  4. Reopen the picker and type "heart"; the top result is highlighted. Press Enter to add it.
  5. Type a query with no matches, eg. "zzzq". "No emoji found." shows and the popover keeps its width.
  6. Tab from the search field; focus moves to the skin-tone button.

Tested: the collab sidebar unit tests (204 pass), and the full block-notes.spec.js e2e spec in Chromium, Firefox, and WebKit (81 pass). The empty-state e2e test now also checks the popover width; it fails without the width fix (the popover shrank by ~140px).

Screenshots

Captured with classic (always visible) scrollbars.

Open Arrow key highlight Search No results
Picker open Highlight moved with the arrow keys Search for heart, top result highlighted No emoji found, popover keeps its width

Screencast

Opening the picker, arrow keys across categories, searching, the empty state, and picking with Enter (keys shown bottom left):

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

adamsilverstein and others added 30 commits June 30, 2026 13:00
…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
adamsilverstein added a commit that referenced this pull request Oct 6, 2026
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
@ciampo
ciampo requested a balanced review from Copilot October 6, 2026 12:32

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

REST authorization and resolved-thread enforcement remain incomplete, and picker announcements and tooltip data can become inaccurate.

Review effort: Balanced
Findings: 1 High severity · 3 Medium severity

Open (4)
Resolved since last review (5)

* @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 ) {
Comment on lines +474 to +475
// 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',
adamsilverstein added a commit that referenced this pull request Oct 6, 2026
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
adamsilverstein added a commit that referenced this pull request Oct 6, 2026
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
adamsilverstein added a commit that referenced this pull request Oct 6, 2026
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
@dmsnell

dmsnell commented Oct 6, 2026

Copy link
Copy Markdown
Member

Going to try and leave some skimming comments: I apologize if they have already been answered.

  • Have we looked at how this impacts the payload delivered to the browser and when it loads? There could be a lot of data in the Emoji database, which is content already present in the browser through the native panel.
  • Can we realize this as the native Input Method Editor? E.g. could this be created as a single-character <input>? The native picker conforms to platform standards, is likely more accessible, and won’t end up with the predictable set of weird bugs that a custom one might.

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 <emoji-picker></emoji-picker>? It seemed like there was great care put into that one for accessibility, performance, and quality.


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).

@adamsilverstein

Copy link
Copy Markdown
Member Author

Did we look at, or is this Nolan Lawson’s <emoji-picker></emoji-picker>? It seemed like there was great care put into that one for accessibility, performance, and quality.

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) .

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.

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.

What is the general approach for search, search indexing, and partial matches?

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.

Have we looked at how this impacts the payload delivered to the browser and when it loads? There could be a lot of data in the Emoji database, which is content already present in the browser through the native panel.

Yes, my goal would be to lazy load the data so its only loaded if you go to insert an emoji reaction.

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.

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.

adamsilverstein and others added 9 commits October 7, 2026 10:55
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
@dmsnell

dmsnell commented Oct 7, 2026

Copy link
Copy Markdown
Member

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.

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 OL or TABLE of text cells or BUTTONs

my comments are meant to be helpful outside questions, not demands. you are welcome to ignore me.

adamsilverstein and others added 2 commits October 7, 2026 16:56
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
@adamsilverstein

Copy link
Copy Markdown
Member Author

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.

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.

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.

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.

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.

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 OL or TABLE of text cells or BUTTONs

I'm working on extensibility of the emoji set in a follow up:

my comments are meant to be helpful outside questions, not demands. you are welcome to ignore me.

Thanks! I'm going to pay attention to you even if I don't always agree 😄 (colon smile colon)

@adamsilverstein

Copy link
Copy Markdown
Member Author

Merged trunk and brought over the recent review fixes from #76767.

Claude ported these, summary below:

  • The internal comment types helper and its filter are gone, with 'note' and 'reaction' listed at each call site (a251c4b).
  • Reaction counts use the user's locale in both the pill and its label (f6a32e2).
  • The add-reaction trigger is hidden on resolved threads instead of shown disabled, and the picker outlines the emoji you already reacted with, announced as "heart, your reaction" (7e95aeb).
  • Fixed two type errors in emoji-picker.tsx that were failing the typecheck (74c7340).

@westonruter

Copy link
Copy Markdown
Member

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.

Do we need to allow any emoji? The linked issue says:

Use a curated subset of common emoji reactions (similar to GitHub/Slack approach):

  • Suggested set: 👍 👎 ❤️ 🎉 😄 😕 👀 🚀

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.)

@Mamaduka

Mamaduka commented Oct 7, 2026

Copy link
Copy Markdown
Member

@westonruter, the PR with the curated list of reactions is in progress here: #76767.

@adamsilverstein

Copy link
Copy Markdown
Member Author

and is shying away from decisions not options

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

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

[Feature] Notes Phase 3 of the Gutenberg roadmap around block commenting Needs Design Needs design efforts. [Package] Editor /packages/editor [Package] UI /packages/ui [Type] Feature New feature to highlight in changelogs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add emoji reactions as a first class comment type