Skip to content

feat(component-library): add BccEmojiPicker, wrap BccTextarea with insertAtCaret - #433

Merged
jasverix merged 11 commits into
mainfrom
feat/emoji-picker
Oct 21, 2026
Merged

jasverix merged 11 commits into
mainfrom
feat/emoji-picker

Conversation

@jasverix

@jasverix jasverix commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Change summary

ClickUp: Støtte emojis og scheduled meldinger på pc i chat (DIGI-6074)

Many desktop users have no emoji keyboard, or don't know the shortcut. Browsers can't open the system emoji panel from a web page. bcc-code/bcc-chat#532 added an emoji button to the chat composer, but it only shows the 36 reaction emojis. The PM wants a normal emoji picker. It lives in the component library so other places in the apps can use it too. The plan is in component-library/plans/2026-10-emoji-picker.md. Using it in the chat is planned separately in bcc-chat (ui/plans/2026-10-emoji-picker.md): the chat gets an openEmojiPicker prop, and bcc-event-app passes openBccEmojiPicker to it in the web build only.

The picker is meant for devices with a mouse or trackpad. On phones and tablets the system emoji keyboard is used: the chat hides its emoji button on touch devices, and the native app builds don't include the picker at all.

What's added

  • openBccEmojiPicker({ anchor, onSelect, locale?, searchPlaceholder?, noResultsText?, onHide? }) / closeBccEmojiPicker(): a popover next to anchor (PrimeVue Popover). It stays open so several emojis can be picked, and closes on outside click or Escape. Calling it again with the same anchor closes it, so it works directly as a button's click handler.
  • <BccEmojiPicker @select>: the same panel inline. It has search (names and keywords in the locale), category tabs, a grid grouped by category, arrow-key navigation, Enter selects the first search result, and aria-labels with emoji names. The buttons use mousedown.prevent so a text field keeps its caret.
  • BccTextarea is now a wrapped component (it was a plain re-export of PrimeVue Textarea). It passes all props, attributes and listeners through unchanged and adds an insertAtCaret(text) method. BccInput gets the same method; it does nothing when numeric. insertAtCaret inserts at the caret, replaces a selection, and fires an input event so v-model and auto-resize update. Fields without a selection API (e.g. type="email") get the text appended. Pass it as onSelect to insert emojis into a field.
  • Storybook: Custom/Input/BccEmojiPicker with Default, Norwegian and Popover (inserts at the caret in a BccTextarea), with <script setup> source examples. BccTextarea moved from PrimeVue/BccTextarea to Wrapped/BccTextarea. New BccInput story: InsertAtCaret.

Decisions

  • Data: emojibase-data, pinned to ~17.0.0. Version 18 adds emojis that many devices can't render yet, and uses CLDR beta labels. Rejected: emoji-picker-element (shadow DOM, hard to match design tokens, CDN data by default) and vue3-emoji-picker / emoji-mart (eager data, styling outside the tokens).
  • Bundle cost only for those who use it. The picker is not installed in setup.ts. The popover component and the data load with import() on first open, one explicit import per locale (en, nb, de, es, fr, nl, pl; others fall back to en). A template import would have emitted all ~30 emojibase locales.
  • Emojis the device can't render are hidden. A test face per Emoji version (11–17) and 🇳🇴 for flags are drawn on a canvas. Color emoji fonts ignore the fill color, while a missing glyph or a flag shown as letters does not. Without a canvas (tests, SSR) everything is shown.
  • src/appContext.ts: PrimeVue components need the app's context when they are mounted with render(). setup.ts stores the app there without importing the picker, so tree-shaking is unaffected.
  • Opening click: the popover waits for the click that opened it to finish, and gets the anchor as currentTarget. Otherwise PrimeVue counted that click (or a later click on the anchor) as an outside click and closed it at once.
  • BccTextarea props aren't declared at runtime. Declaring them would turn absent booleans like fluid into false and override PrimeVue's defaults.

Left out on purpose: skin-tone selection, "recently used", custom emojis. Each can be added without breaking the API.

Compatibility

  • BccTextarea: existing usage keeps working. bcc-event-app (NpsQuestion.vue) uses v-model, auto-resize, variant, label and @focus/@blur, which all pass through. A template ref on it now exposes insertAtCaret instead of PrimeVue's instance; neither bcc-event-app nor bcc-chat uses a ref on it.

Numbers (measured on the built dist in a probe app)

  • pnpm test:bundle-size (an app with only BccBadge): 3.3 kB, same as before, with no emoji files emitted.
  • An app that imports openBccEmojiPicker: the initial bundle only gets the loader. The popover chunk is ~147 kB raw (includes PrimeVue Popover and InputText). The data is ~570 kB raw / ~83 kB gzip per locale, plus ~80 kB raw for the version list, all loaded on first open.
  • The emoji 17 data writes some emojis with a redundant FE0F (e.g. 👍). It looks the same when inserted as text.

Test by hand

  • The Storybook stories in Chrome, Safari and Firefox. On Windows, check that flags are hidden in Chrome and that no empty squares show up.
  • BccTextarea with auto-resize: inserting emojis grows the field.
  • Dark mode.

Checked: pnpm lint, pnpm typecheck, pnpm test:unit (51 passed, including tests for the picker and for insertAtCaret on both fields), and Playwright against Storybook (search, tabs, arrow keys, Enter, insert at the caret; open, close with the anchor, reopen with the module loaded, outside click and Escape). pnpm build:vite and pnpm test:bundle-size were run before the BccTextarea commit.

Screenshots

New component, so there is no "before".

After:
after-popover
after-norwegian-search
after-default-search
after-default-dark
after-default-light

Change type

  • No review
  • Small PR
  • Big PR
  • Refactor

Part of the emoji picker plan (component-library/plans/2026-10-emoji-picker.md)

🤖 Generated with Claude Code

https://claude.ai/code/session_01PJ5wpbHr6RegjqdjX2Pm1S

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJ5wpbHr6RegjqdjX2Pm1S
Full emoji picker with search, category tabs and keyboard navigation, built on
emojibase-data 17. The picker and its data load on first open, and emojis the
device can't render are left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJ5wpbHr6RegjqdjX2Pm1S
…plemented

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJ5wpbHr6RegjqdjX2Pm1S
@u12206050

Copy link
Copy Markdown
Member

I think on mobile it would be best for those using capacitor already to utilise the capacitor/keyboard plugin and have it open the native emoji keyboard, since opening this on mobile while the input is selected means both the keyboard and the emoji popup will need space to exist at the same time which becomes quite cramped.

jasverix and others added 3 commits October 21, 2026 07:35
…hat opens it

Wait for the opening click to finish before showing the popover, and pass
the anchor as currentTarget so clicking it doesn't count as an outside click.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cTextarea and BccInput

BccTextarea now wraps PrimeVue Textarea with the same props and emits, and
exposes insertAtCaret(text). BccInput exposes the same method (no-op when
numeric). Both use a shared helper that fires an input event so v-model and
auto-resize update.

The emoji picker stories use insertAtCaret and show script setup source
examples. The Textarea story moves to Wrapped/BccTextarea.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jasverix jasverix changed the title feat(component-library): add BccEmojiPicker and openBccEmojiPicker feat(component-library): add BccEmojiPicker, wrap BccTextarea with insertAtCaret Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

@u12206050 Agreed, and that's how it's set up: the picker is only for devices with a mouse or trackpad. The chat hides its emoji button on touch devices, and the native builds don't include the picker, so on mobile the system keyboard (with its own emoji key) is used. As far as I know, @capacitor/keyboard can show or hide the keyboard but can't switch it to the emoji layout.


Generated by Claude Code

@jasverix

jasverix commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

About capacitor keyboard:

Capacitor does not have a direct API method to programmatically force-open the native system emoji keyboard. Instead, the native OS keyboard handles emoji switching via built-in user buttons (such as the globe or smiley icon on iOS and Android), or you can build a custom in-app emoji picker.

As Claude said, we are going to build this emoji picker only for desktop devices, as the ClickUp task (linked in PR description) says. For mobile apps, we have a plan to not even include the picker in the final build, so the bundle size increase will only apply for the web version, and since we are lazy-loading it, it will not affect any others that the ones using the chat on the web version from a desktop device.

- Keep emojibase-data external in the UMD build, so it no longer inlines
  ~4 MB of locale JSON (UMD back to ~2.6 MB). It stays a dependency, since
  UMD consumers now resolve it at runtime.
- Keep the picker rendered while the popover fades out, and remount it on
  each open so the search resets and failed loads are retried.
- Show `errorText` when the emoji data fails to load.
- Move focus from the top row back to the search field with ArrowUp.
- Filter emojibase groups by key instead of their order number.
- Add an e2e test for toggling and reopening the popover, and unit tests
  for the error state and ArrowUp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TU8ibHPhr5xVkZuwzoznz3

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.

🟡 Changes recommended

Pending opens can survive closure, and data-loading compatibility and error handling remain unresolved.

3 open issues
1 file not reviewed
  • component-library/pnpm-lock.yaml: Generated file
What changed in this PR

Adds a reusable desktop emoji picker to the component library, plus caret-aware insertion for text fields.

Changes:

  • Adds inline and popover pickers with localized search, categories, keyboard navigation, and lazy-loaded data.
  • Exposes insertAtCaret on BccInput and the newly wrapped BccTextarea.
  • Adds documentation, Storybook examples, and automated tests.
File Description
component-library/​vite.config.umd.ts Externalizes emoji data in UMD builds.
component-library/​tests/​e2e/​emoji-picker.spec.ts Tests popover toggling and reopening.
component-library/​src/​setup.ts Registers the app context.
component-library/​src/​index.ts Removes the direct PrimeVue textarea export.
component-library/​src/​components/​wrapped/​insertAtCaret.ts Implements shared caret insertion.
component-library/​src/​components/​wrapped/​insertAtCaret.spec.ts Tests insertion and model updates.
component-library/​src/​components/​wrapped/​index.ts Exports textarea wrapper and method types.
component-library/​src/​components/​wrapped/​BccTextarea.vue Wraps textarea with caret insertion.
component-library/​src/​components/​wrapped/​BccTextarea.stories.ts Documents the wrapped textarea.
component-library/​src/​components/​wrapped/​BccInput.vue Exposes caret insertion.
component-library/​src/​components/​wrapped/​BccInput.stories.ts Demonstrates caret insertion.
component-library/​src/​components/​custom/​styles.css Includes picker styles.
component-library/​src/​components/​custom/​index.ts Exports picker components and APIs.
component-library/​src/​components/​custom/​BccEmojiPicker/​types.ts Defines picker options and data types.
component-library/​src/​components/​custom/​BccEmojiPicker/​state.ts Lazily mounts and controls the popover.
component-library/​src/​components/​custom/​BccEmojiPicker/​data.ts Loads, filters, and searches emoji data.
component-library/​src/​components/​custom/​BccEmojiPicker/​BccEmojiPickerPopover.vue Implements anchored popover behavior.
component-library/​src/​components/​custom/​BccEmojiPicker/​BccEmojiPicker.vue Implements the searchable picker panel.
component-library/​src/​components/​custom/​BccEmojiPicker/​BccEmojiPicker.stories.ts Adds inline and popover examples.
component-library/​src/​components/​custom/​BccEmojiPicker/​BccEmojiPicker.spec.ts Tests data and picker interactions.
component-library/​src/​components/​custom/​BccEmojiPicker/​BccEmojiPicker.css Styles picker layout and states.
component-library/​src/​appContext.ts Shares context for imperative mounting.
component-library/​pnpm-lock.yaml Locks emoji dependencies.
component-library/​plans/​2026-10-emoji-picker.md Documents design and integration plans.
component-library/​package.json Adds the emoji-data dependency.

Review effort: Balanced

Comment on lines +14 to +33

async function show(next: EmojiPickerOpenOptions) {
if (isOpen.value) {
hide();
await nextTick();
}
// Once the popover module is loaded this runs inside the click that opened it. Popover binds its
// outside-click listener on document while that click is still bubbling, so it would close at
// once. Wait for the event to finish first.
await new Promise(resolve => setTimeout(resolve));
options.value = next;
isOpen.value = true;
openCount.value++;
// Popover treats clicks on `currentTarget` as its own, so clicking the anchor isn't an outside click.
popover.value?.show({ currentTarget: next.anchor } as unknown as Event, next.anchor);
}

function hide() {
popover.value?.hide();
}
Comment thread component-library/vite.config.umd.ts
Comment thread component-library/src/components/custom/BccEmojiPicker/BccEmojiPicker.vue Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@jasverix
jasverix merged commit 1d3ebc1 into main Oct 21, 2026
6 checks passed
@jasverix
jasverix deleted the feat/emoji-picker branch October 21, 2026 08:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants