Skip to content

Support a custom emoji data source by exposing a callback - #36

Open
nperez0111 wants to merge 7 commits into
liveblocks:mainfrom
nperez0111:main
Open

Support a custom emoji data source by exposing a callback#36
nperez0111 wants to merge 7 commits into
liveblocks:mainfrom
nperez0111:main

Conversation

@nperez0111

Copy link
Copy Markdown

The goal of this change was to expose the emoji-data in Frimousse to be customizable so that the user can add additional locales & sets of emojis to expose within the Frimousse picker.

  • Declare process.env.NODE_ENV for type-checking
  • Replace vitest-fetch-mock with an abort-aware fetch mock
  • Support custom emoji data and locales via resolveEmojiData
  • Document custom emoji data and locales
  • Make countryFlag and skins optional in EmojiDataEmoji
  • Add a custom emoji data example to the site
  • Add a placeholder Liveblocks secret key to the site's env example

AI generated description of the changes:


Support custom emoji data and locales

Emoji data was previously always fetched from Emojibase, which limited the picker to the ~30 locales Emojibase ships and made it impossible to reuse data an app already has in memory. This adds a first-class way to provide your own.
The API

Emoji data is now resolved by a function, and EmojiPicker.Root takes a resolveEmojiData prop to replace it. Its default, defaultEmojiDataResolver, is exported — so custom resolvers can handle the locales they know about and delegate the rest.

import { EmojiPicker, defaultEmojiDataResolver, type EmojiData } from "frimousse";

const myEmojiData: Record<string, EmojiData> = {
  tr: { locale: "tr", emojis: [/* … */], categories: [/* … */], skinTones: {/* … */} },
};

<EmojiPicker.Root
  locale={locale}
  resolveEmojiData={(locale, options) =>
    myEmojiData[locale] ?? defaultEmojiDataResolver(locale, options)
  }
/>;

Resolvers can be sync or async, receive (locale, { emojiVersion, emojibaseUrl, signal }), and locale is now widened to Locale | (string & {}) so any locale is accepted. Validation only happens inside defaultEmojiDataResolver, which still falls back to "en" with a warning for locales Emojibase doesn't support. Data returned by a custom resolver is used exactly as provided — no version or country-flag filtering.

createEmojiDataCache is also exported for resolvers that need to persist across page loads. It's the same localStorage cache defaultEmojiDataResolver uses internally, under the same key namespace, so existing caches are unaffected.

const cache = createEmojiDataCache({ name: "my-app/emoji-data" });

Why a resolver rather than a data prop

The motivating case is an editor shipping 36 locales where 8 aren't in Emojibase. A prop taking a data object is all-or-nothing: you either provide everything or nothing. A resolver lets you provide the 8 and delegate the other 28 in one expression, and it keeps caching, revalidation, and support filtering as implementation details of the default resolver rather than API surface.
Fixes along the way

- Revalidation was global, not per-locale. The session flag that skipped ETag revalidation was a single boolean, so fetching any locale suppressed revalidation for every other locale for the rest of the session. It now tracks locales individually.
Inline resolvers re-fired on every render. The data handler renders inline in Root, so an inline arrow function re-ran the effect on unrelated re-renders such as focus/blur. The resolver is now wrapped in a stable callback. This was previously masked by unconditional caching.

Also included

- test/setup-emojibase.ts replaces vitest-fetch-mock with a small hand-rolled fetch mock that respects AbortSignal. vitest-fetch-mock constructs a Request, which fails under jsdom because Node's undici brand-checks init.signal against its own realm, and it ignores abort signals entirely — the abort test could never pass. The dependency is dropped.
src/globals.d.ts declares process.env.NODE_ENV, which didn't type-check given the explicit types allowlist in tsconfig.json and no @types/node.
countryFlag and skins on EmojiDataEmoji are now optional, since hand-authored data otherwise had to set them to undefined on every emoji.
A live example on the site switching between English, French, and a hand-written Turkish data set, plus a placeholder Liveblocks key in site/.env.example so the site runs without one.

Testing

New unit tests for the cache and the resolver (including per-locale revalidation and the unsupported-locale fallback), and browser tests covering delegation to the default resolver and the inline-resolver regression. 121 tests passing.
Breaking changes

None — the emojiData prop this replaces was never released.

Node's types aren't installed since Frimousse only targets the browser, so
the development-only warnings using process.env.NODE_ENV failed to type-check.
vitest-fetch-mock builds a Request to pass to its handler, and under jsdom
the global AbortSignal comes from jsdom while Request comes from Node's
undici, which brand-checks the signal against its own realm. Passing a signal
to fetch therefore threw before any request was made. It also ignores
AbortSignal entirely, so aborting never rejected.

The Emojibase mock is now a plain fetch stub which handles both cases.
EmojiPicker.Root accepts a resolveEmojiData prop which is called with the
current locale and is expected to return its emoji data. It defaults to
defaultEmojiDataResolver, which is also exported so that custom resolvers
can delegate the locales they don't handle themselves.

  <EmojiPicker.Root
    locale={locale}
    resolveEmojiData={(locale, options) =>
      locale in myEmojiData
        ? myEmojiData[locale]
        : defaultEmojiDataResolver(locale, options)
    }
  />

This makes it possible to support locales Emojibase doesn't cover, or to
replace its data entirely, without it being all-or-nothing.

The local storage caching used for Emojibase data is now built on the
exported createEmojiDataCache, so custom resolvers can cache their own data
the same way. Its default namespace matches the previous keys, so existing
caches are reused as-is.

Resolvers are wrapped in a stable callback, otherwise an inline one would
re-resolve on every render of EmojiPicker.Root.

Emoji data is also revalidated once per locale per session instead of once
per session, which previously meant resolving any locale would skip the
ETag check of every other locale for the rest of the session.
Hand-authoring custom emoji data required setting `countryFlag: undefined`
and `skins: undefined` on every emoji. Both are already validated as
optional and read defensively, so they can be omitted entirely.
Documents `resolveEmojiData` with a live example switching between
English, French, and a hand-written Turkish data set, along with the
`createEmojiDataCache` helper and the updated API reference.
The site fails to start without one, but only the reactions in the
header need a real key.
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.

1 participant