Notes: Open a full searchable emoji picker from the add-reaction button (stacked on #76767) - #78176
Conversation
Introduce a new component that displays a horizontal row of emoji buttons for adding reactions to notes. Features include: - Curated emoji set: 👍 👎 ❤️ 🎉 😄 😕 👀 🚀 - Keyboard navigation with arrow keys, Home, and End - Accessible with role="listbox" and role="option" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduce a component that displays current reactions with counts as pill-shaped buttons. Features include: - Shows reaction counts for each emoji - Highlights user's own reactions with distinct styling - Click to toggle (add/remove) reaction - "+" button opens emoji picker dropdown Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduce a popover component that shows who reacted and when. Features include: - Displays reactions grouped by emoji - Shows user avatars and names - Uses humanTimeDiff() for relative timestamps (e.g., "3 days ago") - Fetches user data for all reactors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extend the useBlockCommentsActions hook with three new functions:
- onAddReaction: Add a reaction to a comment
- onRemoveReaction: Remove user's reaction from a comment
- onToggleReaction: Toggle reaction (add if not present, remove if present)
Reactions are stored in comment meta._wp_reactions with structure:
{ emoji: [{ userId, timestamp }] }
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Connect the reaction components to the Notes sidebar: - Import and render ReactionDisplay in CommentBoard - Add ReactionDetailsPopover for viewing reaction details - Add "See emoji reaction details" menu action - Pass onToggleReaction through component hierarchy - Get current user ID for highlighting own reactions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add SCSS styles for: - Reactions container with flexbox layout - Pill-shaped reaction buttons with active state - Add reaction button with dashed border - Emoji picker dropdown - Reaction details popover with user avatars Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive E2E tests for the emoji reactions feature: - can add an emoji reaction to a note - can remove own emoji reaction by clicking it - can see emoji reaction details - reaction buttons are keyboard accessible - can add multiple different reactions to same note Also adds addReactionToComment helper to BlockCommentUtils. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Resolve merge conflicts with trunk's selectedNote editor state changes (#75177) while preserving emoji reaction features. Fix all 5 emoji reaction E2E tests that were timing out because the Dropdown popover was stealing focus from the thread, triggering the onBlur handler which collapsed the note and unmounted the emoji picker. - Add focusOnMount: false to the reaction Dropdown popoverProps - Add popover focus check to the thread onBlur handler - Update addReactionToComment E2E helper to wait for the emoji picker Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…yling Replace the plus icon with a Google Docs-inspired smiley face SVG, change focusOnMount to 'firstElement' so the emoji picker captures focus and prevents the note from collapsing, and restyle the button to be perfectly round with a clean white background that appears on hover. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the "Add reaction" smiley button from below the note content to the upper-right header area alongside the resolve and actions buttons. Also register _wp_reactions as comment meta in PHP so the REST API accepts it, and fix the 500 error caused by spreading all comment meta (including potentially invalid _wp_note_status) when saving reactions. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Add variant="tertiary" to the reaction pill Button components so the WordPress default dark button styling doesn't override the custom light gray background. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
The Button component automatically adds the is-pressed class when aria-pressed is true, which sets a dark background (#1E1E1E). Override with matching specificity to keep the light blue active state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Increase height to 32px, use equal padding on all sides, and reduce gap between emoji and count to bring the aspect ratio closer to 1:1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The reactions meta registration is a new feature targeting WordPress 7.0, not a 6.9 backport. Move it to its own file in lib/compat/wordpress-7.0/ per reviewer feedback.
The editor assumes users are logged in by default, so these guards are redundant. Matches the pattern used by other collab sidebar actions.
Reactions update comment metadata without adding, removing, or resizing comments, so reflowing is unnecessary.
Replace individual getUser() calls with a single getUsers() request using include, context: view, and _fields to reduce API calls and support low-capability users.
Negative reactions are better expressed as comments in a collaborative editing context. The thinking emoji provides a constructive "I need to consider this" signal instead.
The horizontal layout caused emojis to overflow and get cut off in the sidebar popover.
Start conservatively with ❤️ 🎉 😄 👀 🚀 to avoid skin-tone concerns and keep the picker compact. More reactions can be added later.
|
I'm going to focus my feedback on #82324 as a heads up so we can get on the same page design wise rather than my spiraling out in too many PRs in an unhelpful way 😆 |
…dditional-comment-type' into add-notes-emoji-reactions-full-picker # Conflicts: # package-lock.json # packages/editor/CHANGELOG.md # packages/editor/package.json
🤖 PR meta 🤖📦 Bundle sizeSize Change: +42.5 kB (+0.52%) Total Size: 8.27 MB 📦 View Changed
⚡ PerformanceShow the resultsClient side metrics exclude the server response time. front-end-block-theme
front-end-classic-theme
media-processing
media-upload
post-editor
site-editor
|
…dditional-comment-type' into add-notes-emoji-reactions-full-picker # Conflicts: # packages/editor/src/components/collab-sidebar/test/reaction-display.jsdom.test.tsx
Trunk emptied the Jest allowlist, so these files now run under Vitest with globals off. Import the collectors explicitly, swap the jest APIs for vi, opt into the matchMedia mock the editor store needs, and await the settings reset so Composite's update stays inside act(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjWPxajLy99M2CM4SNukFs
…yout The bare factory objects did not satisfy the mocked modules' types, which the typecheck job rejects. The skin tone flyout animates from opacity 0, so the test needs the reduced-motion matchMedia mock to see it open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjWPxajLy99M2CM4SNukFs
…dditional-comment-type' into add-notes-emoji-reactions-full-picker
The collab sidebar tests call `globalThis.wpVitest`, which the editor's dev project could not see, so the typecheck job read it as an implicit any. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WjWPxajLy99M2CM4SNukFs
…dditional-comment-type' into add-notes-emoji-reactions-full-picker # Conflicts: # packages/editor/CHANGELOG.md # packages/editor/src/components/collab-sidebar/note.jsx
|
I opened #83032 about emoji across WordPress as a whole: the picker, support detection, and fallback rendering. The picker and the reaction key in this PR are part of that picture, so I'm linking it here for anyone following. It doesn't ask for any change in this PR. |
…dditional-comment-type' into add-notes-emoji-reactions-full-picker
The trunk merge brought Prettier 3.9.6, which indents ternary continuation lines with tabs instead of space alignment. Lint JavaScript failed on seven lines across the emoji picker and its tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BT86xJSmmzfTzpuzjzMnJE
…a row (#82313) Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org> Co-authored-by: jasmussen <joen@git.wordpress.org> Co-authored-by: fcoveram <fcoveram@git.wordpress.org> Co-authored-by: annezazu <annezazu@git.wordpress.org>
…dditional-comment-type' into add-notes-emoji-reactions-full-picker # Conflicts: # packages/editor/src/components/collab-sidebar/reaction-display.tsx # storybook/main.ts
The trigger moved to add-reaction-picker.tsx on this branch, so it kept its own inlined smiley SVG. Swap it for the reaction icon from @wordpress/icons (#83338) to match the base branch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GaU1KRgXC1a2DBq9NaPpgx
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017S5XPAZ3r74s5uHxYyj7n8
… into add-notes-emoji-reactions-full-picker
…dditional-comment-type' into add-notes-emoji-reactions-full-picker # Conflicts: # test/e2e/specs/editor/various/block-notes.spec.js
I have switched to using the new reaction icon in #76767 |
|
I'm taking a new look at this, and I found the following. Reaction button when reactions were addedWhen reactions are added to a comment, the reaction button keeps disappearing when the pointer is not over the comment. It should remain visible at the end of the reactions set.
Emoji search popoverLayout and stylesThe emoji search popover displays emojis at a smaller size than in [the design](https://www.figma.com/design/3dd6oLYd29li43VB12AP2j/Notes-emoji-reaction?node-id=54-4882&m=dev). There is also a white spacing on the right side that makes the layout uneven.
Here is the design of it
Search interactionWhen typing to search for an emoji, moving from the input to the emoji grid is not keyboard-friendly. I need to move the focus across several actions before reaching the grid. Here is how it works up to the latest commit. CleanShot.2026-09-25.at.12.37.11.mp4Here is how macOS works. CleanShot.2026-09-25.at.15.54.55.mp4 |
|
@fcoveram - excellent, thanks for the careful review and feedback. The issue with the reaction icon disappearing even a note has reactions is a regression - good catch. I've moved the hover interaction upstream into #76767 so I'll make the fix for that there (it will also apply here). For the layouts/styles, thanks for the diagram showing exactly the formatting we are aiming for, that is super helpful. Also, thanks for the design follow up @mirka. I will work to address all of these points! |
…dditional-comment-type' into add-notes-emoji-reactions-full-picker
|
I think this is looking solid. @draganescu are you able to give a code review soon? It would be helpful to have this land soon so we can get user feedback and shake out any bugs earlier in the cycle. |
…earch Design review found the search field several Tab stops away from the emoji: ArrowDown now moves straight to the first match, and Enter picks the top match without leaving the field, as the macOS picker does. The layout follows the design spec: 24px emoji in 40px cells, six per row, 4px gaps and 16px padding. The picker is sized by its grid instead of a fixed width with a scrollbar allowance, which left a blank band on the right under overlay scrollbars. Category headings use the Design System's heading-sm Text, the label style its grouped selects and menus share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2cavEpjEd24b69inEW9HG
|
@fcoveram thanks again for the detailed review, and @mirka for the pointer on group headings. I had Claude work through each point, here is its rundown of what changed:
Can you give it another look when you get a chance? |
@mirka Thanks for pointing to the autocomplete grid mode, I didn't realize that was possible. I wondered if we could actually just use that component for the emoji picker. I tried that out in #83538 (what changed). The result is promising - the Autocomplete component is much faster (especially on first open) and we automatically get the component's accessibility and styles. |





Summary
Fixes #80122
Stacked on top of #76767 - review that PR first (curated 5-emoji reactions baseline). This PR replaces the curated quick-row popover with a full searchable emoji picker: the 😊 "Add reaction" trigger on a note opens the full picker directly.
Splitting this off per @t-hamano's request: library selection and bundling strategy still need wider discussion, so it shouldn't block the basic-reactions foundation.
The hover-revealed trigger and pill treatment from #82313 (merged here in 75625d0) now live in the base PR #76767, since that is where the trigger is introduced - see @draganescu's question. This PR is back to being only about the full picker.
Picker UX
The trigger opens the full searchable picker:
gutenberg_note_reaction_emojisfilter), so the previous quick-row picks stay one click away. It reorders as the user picks emojis.The curated quick row from #76767 still exists as a fallback: npm consumers of
@wordpress/editorthat don't configure an Emojibase URL (see bundle-size strategy below) get the curated row instead of the full picker.Native picker built from WordPress primitives
Earlier iterations used Frimousse, an MIT-licensed React picker. @t-hamano flagged that Frimousse's UI strings come from Emojibase's
.pofiles - not on GlotPress, no coverage for many WordPress locales. Replaced with a native picker built fromSearchControlover aCompositegrid grouped by Emojibase category, with sticky headers andcontent-visibility: autoper row so all ~1949 emojis render quickly. All chrome strings (Search emoji,Loading…,No emoji found., skin tone label) route through__()and translate via the WordPress translation pipeline.packages/editor/src/components/collab-sidebar/emojibase-data.ts:resolveEmojibaseLocale()maps a BCP-47 / WordPress locale (pt-BR,fr_FR,zh-TW) to the closest of Emojibase's 28 locales, falling back toen.useEmojibaseData(baseUrl, locale)fetchesdata.json+messages.jsononce per locale, with module-level cache and abort-on-unmount.packages/editor/src/components/collab-sidebar/emoji-picker.tsx:<EmojiPicker>renders the grid with full keyboard nav viaComposite(same primitives WPDS uses).Accessibility
Composite.Groupwithrole="rowgroup"labelled by aComposite.GroupLabel, so category context reaches assistive tech while arrowing cell by cell.speak()(debounced, matching the block-inserter pattern), including the zero-results case.content-visibility: autopaint containment clips anything drawn outside the row box, so an outset ring would lose its edges.Storage normalization
Picks from the full picker are stored as a lowercase hex-codepoint sequence joined by
-, e.g.1f44dfor 👍 or1f468-200d-1f4bbfor 👨💻. Variation selector U+FE0F is stripped so visually-equivalent presentations collapse -2764-fe0f(❤️) folds into the curatedheartslug. The hex helpers (emojiToHexKey,hexKeyToEmoji,emojiToStorageKey) extendreaction-emoji-picker.tsxand remain compatible with the curated slug storage from the base PR: a full-picker pick that matches a curated (or filter-provided) reaction stores under the same slug as a quick-row pick, so both paths aggregate into a singlereaction_summarybucket.Bundle-size strategy: lazy load + same-origin data
Two layers keep the editor bundle lean:
React.lazyinreaction-display.tsx. The picker module only loads when the user first clicks (or hovers/focuses) the 😊 "Add reaction" trigger in a session - for editors who never open the picker, it never loads at all.tools/build-scripts/copy-emojibase-data.mjs) copies per-localedata.json+messages.jsonfromemojibase-data(atools/build-scriptsworkspace dependency, not a root dependency) intobuild/emojibase-data/<locale>/after the main build runs (28 locales, ~7 MB on disk). PHP exposes the directory URL to JS via thenoteEmojibaseUrleditor setting. Per-session network cost stays at one locale (~85 KB gzipped), only fetched when the user first opens the picker.For npm consumers of
@wordpress/editoroutside the plugin: when the setting is unset, the trigger falls back to the curated quick row from #76767 - there's no fallthrough to a third-party CDN. Consumers opt in to the full picker by self-hosting the data and setting it.Per-locale label overrides
gutenberg_emoji_picker_label_overridesPHP filter lets sites override Emojibase emoji labels for any locale (e.g. when curated labels in the base PR differ from Emojibase's choice for the same code point). The overrides are exposed to JS via thenoteEmojiLabelOverrideseditor setting and are built from the filtered reaction list, so curated and filter-provided reactions keep their curated labels (e.g. "Heart") inside the full picker.gutenberg_emoji_to_hexcode()decodes UTF-8 byte-by-byte with nombstringdependency.Frequently used section
The full picker opens with a Frequently used section above the category grid, matching the design mocks. It starts seeded with the curated reaction emojis (the filtered list from
useReactionEmojis(), so a site'sgutenberg_note_reaction_emojisadditions are seeded too) and then learns from use: every pick increments a per-user counter (packages/editor/src/components/collab-sidebar/frequent-emojis.ts), stored through the preferences store in thecorescope so it persists to user meta the same way as the skin tone preference.Screenshots
The whole loop: hovering a note to reveal the trigger, opening the picker, searching, picking an emoji, then hovering and joining a reaction someone else left.
At rest and hovered. The note is exactly as tall either way - the trigger floats in the corner rather than claiming a row.
Reactions render as pill buttons with a count. Your own takes the brand outline, other people's stay quiet, and hovering a pill lazy-loads the reactor names. The trigger trails the pills once there are any.
Below: ❤️ and 🎉 left by another editor with 👍 from the current user, then joining the ❤️ takes it to 2, then the tooltip naming who reacted.
The picker itself: opening with the seeded Frequently used row, filtering to a flat results grid, and the skin tone flyout.
Data storage
Reactions are rows in
wp_comments, not meta on the note. Each reaction is its own comment:comment_typeisreaction,comment_parentis the note's comment ID,comment_post_IDis the post,user_idis the reactor, andcomment_contentholds the emoji storage key. Most of this comes from the base PR #76767; this PR adds the hex-codepoint form of the storage key.The reasoning is in the header of
lib/compat/wordpress-7.1/block-comments.php: every reaction gets a real author, date and ID, so the existing comment APIs cover authorship, timestamps and deletion; the type is generic (reaction, notnote_reaction) so it can attach to other resources down the road; and concurrent add/remove resolves as row inserts and deletes rather than read-modify-write on a serialized meta value.What lands in
comment_content. Never raw emoji bytes - the comments table is not guaranteed to be utf8mb4 on every install, and an ASCII key also groups cleanly in the count query. Curated picks store their slug (heart,celebration,smile,eyes,rocket); everything else stores the hex-codepoint key described in Storage normalization above.Writing.
create_item()requires a parent note on the same post, accepts only a curated slug or a valid hex key (code points at or below U+10FFFF, no surrogates), rejects a duplicate emoji from the same user on the same note, and persists the canonical slug rather than the raw request content. Reactions can't be edited - aPUTreturns an error, and toggling one off is a force delete, so a trashed row never blocks re-adding the same emoji.Reading. Notes carry a read-only
reaction_summaryfield keyed by slug, eg.{ "heart": { "count": 3, "reacted": true, "my_reaction_id": 412 } }.get_items()pre-fetches summaries for the whole page of notes with two queries - counts grouped by note and slug, plus the current user's own reaction IDs - so the sidebar list doesn't go N+1.Lifecycle. Core only cascades trash, untrash and delete to
notechildren, soblock-comments.phpadds handlers that carry reactions along with their note.reactionalso joinsgutenberg_internal_comment_types(), which keeps reactions out of admin comment lists, comment counts, and direct comment reads.Not in the comments table. The picker's per-user state lives in the preferences store instead:
emojiPickerFrequentEmojisandemojiPickerSkinTone.Testing
For multi-user testing, try https://wordpress.org/plugins/user-switching/
heartreactionzzzznoresults- verify empty-state messagenpm run test:e2e -- test/e2e/specs/editor/various/block-notes.spec.js --grep "Emoji Reactions"npm run storybook:dev, then open Editor → Notes Reactions for the complete reaction row (smiley trigger and toggleable pills), or Editor → EmojiPicker for the full picker grid in isolationnpm run test:unit -- packages/editor/src/components/collab-sidebarnpm run test:unit:php:base -- --filter=Emoji_Picker_DataAI Use
Claude Code did the typing here, I did the asking. I will review and test.