feat: add support for custom emoji fonts (#308)

Fixes #82
This commit is contained in:
Nolan Lawson 2022-12-29 10:16:39 -08:00 committed by GitHub
parent af58a9279d
commit da524c240d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 258 additions and 128 deletions

136
README.md
View File

@ -13,8 +13,8 @@ A lightweight emoji picker, distributed as a web component.
- Supports [Emoji v14.0](https://emojipedia.org/emoji-14.0/) (depending on OS) and custom emoji
- Uses IndexedDB, so it consumes [far less memory](https://nolanlawson.com/2020/06/28/introducing-emoji-picker-element-a-memory-efficient-emoji-picker-for-the-web/) than other emoji pickers
- [Small bundle size](https://bundlephobia.com/result?p=emoji-picker-element) (41kB minified, ~14.4kB gzipped)
- Renders native emoji only, no spritesheets
- [Small bundle size](https://bundlephobia.com/result?p=emoji-picker-element) (<15kB min+gz)
- Renders native emoji by default, with support for custom fonts
- [Accessible by default](https://nolanlawson.com/2020/07/01/building-an-accessible-emoji-picker/)
- Framework and bundler not required, just add a `<script>` tag and use it
@ -25,6 +25,9 @@ A lightweight emoji picker, distributed as a web component.
- [emoji-picker-element](#emoji-picker-element-)
* [Usage](#usage)
+ [Examples](#examples)
+ [Emoji support](#emoji-support)
- [Custom emoji font](#custom-emoji-font)
- [Polyfilling flag emoji on Windows](#polyfilling-flag-emoji-on-windows)
* [Styling](#styling)
+ [Size](#size)
+ [Dark mode](#dark-mode)
@ -131,9 +134,60 @@ This will log:
- [Button with tooltip/popover](https://nolanlawson.github.io/emoji-picker-element/demos/tooltip/index.html) ([source](https://github.com/nolanlawson/emoji-picker-element/blob/master/docs/demos/tooltip/index.html))
- [Inserting emoji into a text input](https://nolanlawson.github.io/emoji-picker-element/demos/input/index.html) ([source](https://github.com/nolanlawson/emoji-picker-element/blob/master/docs/demos/input/index.html))
- [In a React app](https://nolanlawson.github.io/emoji-picker-element/demos/react/index.html) ([source](https://github.com/nolanlawson/emoji-picker-element/blob/master/docs/demos/react/index.html))
- [With Twemoji](https://nolanlawson.github.io/emoji-picker-element/demos/twemoji/index.html) ([source](https://github.com/nolanlawson/emoji-picker-element/blob/master/docs/demos/twemoji/index.html)) (**Note:** has a performance cost. Use with care.)
- [Custom emoji font](https://nolanlawson.github.io/emoji-picker-element/demos/twemoji-mozilla/index.html) ([source](https://github.com/nolanlawson/emoji-picker-element/blob/master/docs/demos/twemoji-mozilla/index.html))
- [Fallback for missing flag emoji on Windows](https://nolanlawson.github.io/emoji-picker-element/demos/flags/index.html) ([source](https://github.com/nolanlawson/emoji-picker-element/blob/master/docs/demos/flags/index.html))
### Emoji support
[Emoji support varies](https://nolanlawson.com/2022/04/08/the-struggle-of-using-native-emoji-on-the-web/) across browsers and operating systems. By default, `emoji-picker-element` will hide unsupported emoji from the picker.
To work around this, you can use [a custom emoji font](#custom-emoji-font) or [polyfill flag emoji on Windows](#polyfilling-flag-emoji-on-windows).
#### Custom emoji font
To use a custom emoji font, first set the `--emoji-font-family` CSS property:
```css
emoji-picker {
--emoji-font-family: MyCustomFont;
}
```
Then, specify the maximum emoji version supported by the font (see [Emojipedia](https://emojipedia.org/emoji-versions/) for a list of versions).
In HTML:
```html
<emoji-picker emoji-version="14.0"></emoji-picker>
```
Or JavaScript:
```js
const picker = new Picker({
emojiVersion: 14.0
});
```
If the `emoji-version`/`emojiVersion` option is set, then `emoji-picker-element` will not attempt to detect unsupported emoji or hide them.
Also note that support for color fonts [varies across browsers and OSes](https://caniuse.com/colr), and some browsers may have <a href="https://github.com/nolanlawson/emoji-picker-element/pull/308#issuecomment-1367491149">bugs</a> or not render the font at all. Be careful to test your supported browsers when using this approach.
#### Polyfilling flag emoji on Windows
As of this writing, [Windows does not support country flag emoji](https://answers.microsoft.com/en-us/windows/forum/all/where-are-the-flag-emoji-in-windows-10/93daa6e8-880a-48b1-9891-ab5bfbfbce98). This is only a problem in Chromium-based browsers, because Firefox ships with its own emoji font.
To work around this, you can use [country-flag-emoji-polyfill](https://www.npmjs.com/package/country-flag-emoji-polyfill):
```js
import { polyfillCountryFlagEmojis } from 'country-flag-emoji-polyfill';
// emoji-picker-element will use "Twemoji Mozilla" and fall back to other fonts for non-flag emoji
polyfillCountryFlagEmojis('Twemoji Mozilla');
```
Note that you do not need to do this if you are using [a custom emoji font](#custom-emoji-font).
## Styling
`emoji-picker-element` uses [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM), so its inner styling cannot be (easily) changed with arbitrary CSS. Refer to the API below for style customization.
@ -185,33 +239,34 @@ Here is a full list of options:
<!-- CSS variable options start -->
| Variable | Default | Default (dark) | Description |
| ---------------------------- | ---------------------- | -------------- | ---------------------------------------------------------------------------------------------------- |
| `--background` | `#fff` | `#222` | Background of the entire `<emoji-picker>` |
| `--border-color` | `#e0e0e0` | `#444` | |
| `--border-size` | `1px` | | Width of border used in most of the picker |
| `--button-active-background` | `#e6e6e6` | `#555555` | Background of an active button |
| `--button-hover-background` | `#d9d9d9` | `#484848` | Background of a hovered button |
| `--category-emoji-padding` | `var(--emoji-padding)` | | Vertical/horizontal padding on category emoji, if you want it to be different from `--emoji-padding` |
| `--category-emoji-size` | `var(--emoji-size)` | | Width/height of category emoji, if you want it to be different from `--emoji-size` |
| `--category-font-color` | `#111` | `#efefef` | Font color of custom emoji category headings |
| `--category-font-size` | `1rem` | | Font size of custom emoji category headings |
| `--emoji-padding` | `0.5rem` | | Vertical and horizontal padding on emoji |
| `--emoji-size` | `1.375rem` | | Width and height of all emoji |
| `--indicator-color` | `#385ac1` | `#5373ec` | Color of the nav indicator |
| `--indicator-height` | `3px` | | Height of the nav indicator |
| `--input-border-color` | `#999` | `#ccc` | |
| `--input-border-radius` | `0.5rem` | | |
| `--input-border-size` | `1px` | | |
| `--input-font-color` | `#111` | `#efefef` | |
| `--input-font-size` | `1rem` | | |
| `--input-line-height` | `1.5` | | |
| `--input-padding` | `0.25rem` | | |
| `--input-placeholder-color` | `#999` | `#ccc` | |
| `--num-columns` | `8` | | How many columns to display in the emoji grid |
| `--outline-color` | `#999` | `#fff` | Focus outline color |
| `--outline-size` | `2px` | | Focus outline width |
| `--skintone-border-radius` | `1rem` | | Border radius of the skintone dropdown |
| Variable | Default | Default (dark) | Description |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------- |
| `--background` | `#fff` | `#222` | Background of the entire `<emoji-picker>` |
| `--border-color` | `#e0e0e0` | `#444` | |
| `--border-size` | `1px` | | Width of border used in most of the picker |
| `--button-active-background` | `#e6e6e6` | `#555555` | Background of an active button |
| `--button-hover-background` | `#d9d9d9` | `#484848` | Background of a hovered button |
| `--category-emoji-padding` | `var(--emoji-padding)` | | Vertical/horizontal padding on category emoji, if you want it to be different from `--emoji-padding` |
| `--category-emoji-size` | `var(--emoji-size)` | | Width/height of category emoji, if you want it to be different from `--emoji-size` |
| `--category-font-color` | `#111` | `#efefef` | Font color of custom emoji category headings |
| `--category-font-size` | `1rem` | | Font size of custom emoji category headings |
| `--emoji-font-family` | `"Twemoji Mozilla","Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji","EmojiOne Color","Android Emoji",sans-serif` | | Font family for a custom emoji font (as opposed to native emoji) |
| `--emoji-padding` | `0.5rem` | | Vertical and horizontal padding on emoji |
| `--emoji-size` | `1.375rem` | | Width and height of all emoji |
| `--indicator-color` | `#385ac1` | `#5373ec` | Color of the nav indicator |
| `--indicator-height` | `3px` | | Height of the nav indicator |
| `--input-border-color` | `#999` | `#ccc` | |
| `--input-border-radius` | `0.5rem` | | |
| `--input-border-size` | `1px` | | |
| `--input-font-color` | `#111` | `#efefef` | |
| `--input-font-size` | `1rem` | | |
| `--input-line-height` | `1.5` | | |
| `--input-padding` | `0.25rem` | | |
| `--input-placeholder-color` | `#999` | `#ccc` | |
| `--num-columns` | `8` | | How many columns to display in the emoji grid |
| `--outline-color` | `#999` | `#fff` | Focus outline color |
| `--outline-size` | `2px` | | Focus outline width |
| `--skintone-border-radius` | `1rem` | | Border radius of the skintone dropdown |
<!-- CSS variable options end -->
@ -267,14 +322,15 @@ document.body.appendChild(picker);
The `new Picker(options)` constructor supports several options:
Name | Type | Default | Description |
------ | ------ | ------ | ------ |
`customCategorySorting` | function | - | Function to sort custom category strings (sorted alphabetically by default) |
`customEmoji` | CustomEmoji[] | - | Array of custom emoji |
`dataSource` | string | "https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json" | URL to fetch the emoji data from (`data-source` when used as an attribute) |
`i18n` | I18n | - | i18n object (see below for details) |
`locale` | string | "en" | Locale string |
`skinToneEmoji` | string | "🖐️" | The emoji to use for the skin tone picker (`skin-tone-emoji` when used as an attribute) |
| Name | Type | Default | Description |
|-------------------------|---------------|------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|
| `customCategorySorting` | function | - | Function to sort custom category strings (sorted alphabetically by default) |
| `customEmoji` | CustomEmoji[] | - | Array of custom emoji |
| `dataSource` | string | "https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json" | URL to fetch the emoji data from (`data-source` when used as an attribute) |
| `emojiVersion` | number | - | Maximum supported emoji version as a number (e.g. `14.0` or `13.1`). Setting this disables the default emoji support detection. |
| `i18n` | I18n | - | i18n object (see below for details) |
| `locale` | string | "en" | Locale string |
| `skinToneEmoji` | string | "🖐️" | The emoji to use for the skin tone picker (`skin-tone-emoji` when used as an attribute) |
@ -888,10 +944,12 @@ Using IndexedDB has a few advantages:
### Native emoji
To avoid downloading a large sprite sheet that renders a particular emoji set which may look out-of-place on different platforms, or may have [IP issues](https://blog.emojipedia.org/apples-emoji-crackdown/) `emoji-picker-element` only renders native emoji. This means it is limited to the emoji actually installed on the user's device.
To avoid downloading a large sprite sheet or font file which may look out-of-place on different platforms, or may have [IP issues](https://blog.emojipedia.org/apples-emoji-crackdown/) `emoji-picker-element` only renders native emoji by default. This means it is limited to the emoji font actually installed on the user's device.
To avoid rendering ugly unsupported or half-supported emoji, `emoji-picker-element` will automatically detect emoji support and only render the supported characters. (So no empty boxes or awkward double emoji.) If no color emoji are supported by the browser/OS, then an error message is displayed (e.g. older browsers, some odd Linux configurations).
That said, `emoji-picker-element` does support [custom emoji fonts](#custom-emoji-font) if you really want.
### JSON loading
Browsers deal with JSON more efficiently when it's loaded via `fetch()` rather than embedded in JavaScript. It's

View File

@ -5,7 +5,7 @@ import { promisify } from 'util'
import prettyBytes from 'pretty-bytes'
import fs from 'fs/promises'
const MAX_SIZE_MIN = '42 kB'
const MAX_SIZE_MIN = '42.5 kB'
const MAX_SIZE_MINGZ = '15 kB'
const FILENAME = './bundle.js'

View File

@ -4,9 +4,17 @@ import { markdownTable as table } from 'markdown-table'
import { readFile, writeFile } from './fs.js'
import { replaceInReadme } from './replaceInReadme.js'
import postcss from 'postcss'
import { FONT_FAMILY } from '../src/picker/constants.js'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
// To avoid code duplication, we could not declare this in variables.scss
const MANUAL_VARS = [{
name: '--emoji-font-family',
value: FONT_FAMILY,
comment: 'Font family for a custom emoji font (as opposed to native emoji)'
}]
const START_MARKER = '<!-- CSS variable options start -->'
const END_MARKER = '<!-- CSS variable options end -->'
@ -33,7 +41,7 @@ async function generateCssVariablesData (css) {
const ast = postcss.parse(css)
const hosts = ast.nodes.filter(({ selector }) => ([':host', ':host,\n:host(.light)'].includes(selector)))
const darkHosts = ast.nodes.filter(({ selector }) => selector === ':host(.dark)')
const vars = hosts.map(extractCSSVariables).flat()
const vars = hosts.map(extractCSSVariables).flat().concat(MANUAL_VARS)
const darkVars = darkHosts.map(extractCSSVariables).flat()
const sortedVars = vars.sort((a, b) => a.name < b.name ? -1 : 1)

View File

@ -33,6 +33,12 @@
"description": "The emoji to use for the skin tone picker",
"type": "string",
"default": "\"🖐\""
},
{
"name": "emoji-version",
"description": "Maximum supported emoji version as a number (e.g. `14.0` or `13.1`). Setting this disables the default emoji support detection.",
"type": "string",
"default": null
}
],
"members": [
@ -68,6 +74,11 @@
"name": "customCategorySorting",
"description": "Function to sort custom category strings (sorted alphabetically by default)",
"kind": "field"
},
{
"name": "emojiVersion",
"description": "Maximum supported emoji version as a number (e.g. `14.0` or `13.1`). Setting this disables the default emoji support detection.",
"kind": "field"
}
],
"events": [
@ -126,6 +137,11 @@
"description": "Font size of custom emoji category headings (default: `1rem`)",
"default": "\"1rem\""
},
{
"name": "--emoji-font-family",
"description": "Font family for a custom emoji font (as opposed to native emoji) (default: `\"Twemoji Mozilla\",\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\",\"EmojiOne Color\",\"Android Emoji\",sans-serif`)",
"default": "\"\\\"Twemoji Mozilla\\\",\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\",\\\"Segoe UI Symbol\\\",\\\"Noto Color Emoji\\\",\\\"EmojiOne Color\\\",\\\"Android Emoji\\\",sans-serif\""
},
{
"name": "--emoji-padding",
"description": "Vertical and horizontal padding on emoji (default: `0.5rem`)",

View File

@ -0,0 +1,27 @@
<!doctype html>
<html lang=en>
<head>
<title>emoji-picker-element: using Twemoji Mozilla COLR font</title>
<style>
@font-face {
font-family: "MozillaTwemojiColr";
src: url("https://cdn.jsdelivr.net/npm/twemoji-colr-font@^14/twemoji.woff2") format("woff2");
}
emoji-picker {
--emoji-font-family: MozillaTwemojiColr;
}
</style>
</head>
<body>
<h1>emoji-picker-element: using Twemoji Mozilla COLR font</h1>
<p>
This demo shows how to use emoji-picker-element with the <a href="https://github.com/mozilla/twemoji-colr">Twemoji Mozilla COLR font</a> as a custom emoji font.
Note that this carries a performance cost due to downloading the additional font file. Also note that alignment may be off in Safari due to <a href="https://bugs.webkit.org/show_bug.cgi?id=249943">a WebKit bug</a>, and
that <a href="https://caniuse.com/colr">not all browsers support COLR fonts</a>.
Use this approach with care.
</p>
<emoji-picker emoji-version="14.0"></emoji-picker>
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
</body>
</html>

View File

@ -1,44 +0,0 @@
<!doctype html>
<html lang=en>
<head>
<title>emoji-picker-element: using Twemoji</title>
</head>
<body>
<h1>emoji-picker-element: using Twemoji</h1>
<p>
This demo shows how to use emoji-picker-element with Twemoji.
Note that this carries a performance cost because of 1) using MutationObserver to monitor for DOM changes, and 2) downloading Twemoji images.
Use this approach with care.
</p>
<emoji-picker></emoji-picker>
<script src="https://twemoji.maxcdn.com/v/14.0.0/twemoji.min.js" integrity="sha384-L1ViA0v9uyiBlZsOGT/z9RVgs+Gku2SDCuzaAoLco0hEgfYZYiztb+pRgWYHkDwb" crossorigin="anonymous"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
<script type="module">
const picker = document.querySelector('emoji-picker')
// Adjust twemoji styles
const style = document.createElement('style')
style.textContent = `.twemoji {
width: var(--emoji-size);
height: var(--emoji-size);
pointer-events: none;
}`
picker.shadowRoot.appendChild(style)
const observer = new MutationObserver(() => {
for (const emoji of picker.shadowRoot.querySelectorAll('.emoji')) {
// Avoid infinite loops of MutationObserver
if (!emoji.querySelector('.twemoji')) {
// Do not use default 'emoji' class name because it conflicts with emoji-picker-element's
twemoji.parse(emoji, { className: 'twemoji' })
}
}
})
observer.observe(picker.shadowRoot, {
subtree: true,
childList: true
})
</script>
</body>
</html>

15
picker.d.ts vendored
View File

@ -8,14 +8,15 @@ export default class Picker extends HTMLElement {
customCategorySorting?: (a: string, b: string) => number;
/**
*
* @param dataSource - URL to fetch the emoji data from (`data-source` when used as an attribute)
* @param locale - Locale string
* @param i18n - i18n object (see below for details)
* @param skinToneEmoji - The emoji to use for the skin tone picker (`skin-tone-emoji` when used as an attribute)
* @param customEmoji - Array of custom emoji
* @param customCategorySorting - Function to sort custom category strings (sorted alphabetically by default)
* @param dataSource - URL to fetch the emoji data from (`data-source` when used as an attribute).
* @param locale - Locale string.
* @param i18n - i18n object (see below for details).
* @param skinToneEmoji - The emoji to use for the skin tone picker (`skin-tone-emoji` when used as an attribute).
* @param customEmoji - Array of custom emoji.
* @param customCategorySorting - Function to sort custom category strings (sorted alphabetically by default).
* @param emojiVersion - Maximum supported emoji version as a number (e.g. `14.0` or `13.1`). Setting this disables the default emoji support detection.
*/
constructor({ dataSource, locale, i18n, skinToneEmoji, customEmoji, customCategorySorting }?: PickerConstructorOptions);
constructor({ dataSource, locale, i18n, skinToneEmoji, customEmoji, customCategorySorting, emojiVersion }?: PickerConstructorOptions);
addEventListener<K extends keyof EmojiPickerEventMap>(type: K, listener: (this: Picker, ev: EmojiPickerEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;

1
shared.d.ts vendored
View File

@ -35,6 +35,7 @@ export interface PickerConstructorOptions {
skinToneEmoji?: string;
customEmoji?: CustomEmoji[];
customCategorySorting?: (a: string, b: string) => number;
emojiVersion?: number;
}
export interface I18n {
emojiUnsupportedMessage: string;

View File

@ -1,6 +1,6 @@
import SveltePicker from './components/Picker/Picker.svelte'
import { DEFAULT_DATA_SOURCE, DEFAULT_LOCALE } from '../database/constants'
import { DEFAULT_CATEGORY_SORTING, DEFAULT_SKIN_TONE_EMOJI } from './constants'
import { DEFAULT_CATEGORY_SORTING, DEFAULT_SKIN_TONE_EMOJI, FONT_FAMILY } from './constants'
import enI18n from './i18n/en.js'
import Database from './ImportedDatabase'
@ -11,16 +11,20 @@ const PROPS = [
'dataSource',
'i18n',
'locale',
'skinToneEmoji'
'skinToneEmoji',
'emojiVersion'
]
// Styles injected ourselves, so we can declare the FONT_FAMILY variable in one place
const EXTRA_STYLES = `:host{--emoji-font-family:${FONT_FAMILY}}`
export default class PickerElement extends HTMLElement {
constructor (props) {
performance.mark('initialLoad')
super()
this.attachShadow({ mode: 'open' })
const style = document.createElement('style')
style.textContent = process.env.STYLES
style.textContent = process.env.STYLES + EXTRA_STYLES
this.shadowRoot.appendChild(style)
this._ctx = {
// Set defaults
@ -30,6 +34,7 @@ export default class PickerElement extends HTMLElement {
customCategorySorting: DEFAULT_CATEGORY_SORTING,
customEmoji: null,
i18n: enI18n,
emojiVersion: null,
...props
}
// Handle properties set before the element was upgraded
@ -62,15 +67,16 @@ export default class PickerElement extends HTMLElement {
}
static get observedAttributes () {
return ['locale', 'data-source', 'skin-tone-emoji'] // complex objects aren't supported, also use kebab-case
return ['locale', 'data-source', 'skin-tone-emoji', 'emoji-version'] // complex objects aren't supported, also use kebab-case
}
attributeChangedCallback (attrName, oldValue, newValue) {
// convert from kebab-case to camelcase
// see https://github.com/sveltejs/svelte/issues/3852#issuecomment-665037015
this._set(
// convert from kebab-case to camelcase
// see https://github.com/sveltejs/svelte/issues/3852#issuecomment-665037015
attrName.replace(/-([a-z])/g, (_, up) => up.toUpperCase()),
newValue
// convert string attribute to float if necessary
attrName === 'emoji-version' ? parseFloat(newValue) : newValue
)
}

View File

@ -1,16 +1,15 @@
/* eslint-disable prefer-const,no-labels,no-inner-declarations */
import { onMount, tick } from 'svelte'
import { groups as defaultGroups, customGroup } from '../../groups'
import { MIN_SEARCH_TEXT_LENGTH, NUM_SKIN_TONES } from '../../../shared/constants'
import { requestIdleCallback } from '../../utils/requestIdleCallback'
import { hasZwj } from '../../utils/hasZwj'
import { emojiSupportLevelPromise, supportedZwjEmojis } from '../../utils/emojiSupport'
import { detectEmojiSupportLevel, supportedZwjEmojis } from '../../utils/emojiSupport'
import { applySkinTone } from '../../utils/applySkinTone'
import { halt } from '../../utils/halt'
import { incrementOrDecrement } from '../../utils/incrementOrDecrement'
import {
DEFAULT_NUM_COLUMNS,
FONT_FAMILY,
MOST_COMMONLY_USED_EMOJI,
TIMEOUT_BEFORE_LOADING_MESSAGE
} from '../../constants'
@ -19,7 +18,6 @@ import { summarizeEmojisForUI } from '../../utils/summarizeEmojisForUI'
import * as widthCalculator from '../../utils/widthCalculator'
import { checkZwjSupport } from '../../utils/checkZwjSupport'
import { requestPostAnimationFrame } from '../../utils/requestPostAnimationFrame'
import { tick } from 'svelte'
import { requestAnimationFrame } from '../../utils/requestAnimationFrame'
import { uniq } from '../../../shared/uniq'
import { resetScrollTopIfPossible } from '../../utils/resetScrollTopIfPossible.js'
@ -30,6 +28,7 @@ export let i18n
export let database
export let customEmoji
export let customCategorySorting
export let emojiVersion
// private
let initialLoad = true
@ -97,11 +96,15 @@ const isSkinToneOption = element => /^skintone-/.test(element.id)
// Determine the emoji support level (in requestIdleCallback)
//
emojiSupportLevelPromise.then(level => {
// Can't actually test emoji support in Jest/JSDom, emoji never render in color in Cairo
/* istanbul ignore next */
if (!level) {
message = i18n.emojiUnsupportedMessage
onMount(() => {
if (!emojiVersion) {
detectEmojiSupportLevel().then(level => {
// Can't actually test emoji support in Jest/JSDom, emoji never render in color in Cairo
/* istanbul ignore next */
if (!level) {
message = i18n.emojiUnsupportedMessage
}
})
}
})
@ -142,7 +145,6 @@ $: {
/* eslint-disable no-unused-vars */
$: pickerStyle = `
--font-family: ${FONT_FAMILY};
--num-groups: ${groups.length};
--indicator-opacity: ${searchMode ? 0 : 1};
--num-skintones: ${NUM_SKIN_TONES};`
@ -298,11 +300,11 @@ $: {
const zwjEmojisToCheck = currentEmojis
.filter(emoji => emoji.unicode) // filter custom emoji
.filter(emoji => hasZwj(emoji) && !supportedZwjEmojis.has(emoji.unicode))
if (zwjEmojisToCheck.length) {
if (!emojiVersion && zwjEmojisToCheck.length) {
// render now, check their length later
requestAnimationFrame(() => checkZwjSupportAndUpdate(zwjEmojisToCheck))
} else {
currentEmojis = currentEmojis.filter(isZwjSupported)
currentEmojis = emojiVersion ? currentEmojis : currentEmojis.filter(isZwjSupported)
// Reset scroll top to 0 when emojis change
requestAnimationFrame(() => resetScrollTopIfPossible(tabpanelElement))
}
@ -321,13 +323,13 @@ function isZwjSupported (emoji) {
}
async function filterEmojisByVersion (emojis) {
const emojiSupportLevel = await emojiSupportLevelPromise
const emojiSupportLevel = emojiVersion || await detectEmojiSupportLevel()
// !version corresponds to custom emoji
return emojis.filter(({ version }) => !version || version <= emojiSupportLevel)
}
async function summarizeEmojis (emojis) {
return summarizeEmojisForUI(emojis, await emojiSupportLevelPromise)
return summarizeEmojisForUI(emojis, emojiVersion || await detectEmojiSupportLevel())
}
async function getEmojisByGroup (group) {

View File

@ -24,7 +24,7 @@ export const MOST_COMMONLY_USED_EMOJI = [
]
// It's important to list Twemoji Mozilla before everything else, because Mozilla bundles their
// own font on some platforms (notably Windows and Linux as of this writing). Typically Mozilla
// own font on some platforms (notably Windows and Linux as of this writing). Typically, Mozilla
// updates faster than the underlying OS, and we don't want to render older emoji in one font and
// newer emoji in another font:
// https://github.com/nolanlawson/emoji-picker-element/pull/268#issuecomment-1073347283

View File

@ -89,7 +89,7 @@ button.emoji,
width: var(--total-emoji-size);
line-height: 1;
overflow: hidden;
font-family: var(--font-family);
font-family: var(--emoji-font-family);
cursor: pointer;
// see https://css-tricks.com/solving-sticky-hover-states-with-media-hover-hover/

View File

@ -1,18 +1,28 @@
import { determineEmojiSupportLevel } from './determineEmojiSupportLevel'
import { requestIdleCallback } from './requestIdleCallback'
import { requestIdleCallback } from './requestIdleCallback.js'
// Check which emojis we know for sure aren't supported, based on Unicode version level
export const emojiSupportLevelPromise = new Promise(resolve => (
requestIdleCallback(() => (
resolve(determineEmojiSupportLevel()) // delay so ideally this can run while IDB is first populating
))
))
let promise
export const detectEmojiSupportLevel = () => {
if (!promise) {
// Delay so it can run while the IDB database is being created by the browser (on another thread).
// This helps especially with first load we want to start pre-populating the database on the main thread,
// and then wait for IDB to commit everything, and while waiting we run this check.
promise = new Promise(resolve => (
requestIdleCallback(() => (
resolve(determineEmojiSupportLevel()) // delay so ideally this can run while IDB is first populating
))
))
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
promise.then(emojiSupportLevel => {
console.log('emoji support level', emojiSupportLevel)
})
}
}
return promise
}
// determine which emojis containing ZWJ (zero width joiner) characters
// are supported (rendered as one glyph) rather than unsupported (rendered as two or more glyphs)
export const supportedZwjEmojis = new Map()
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
emojiSupportLevelPromise.then(emojiSupportLevel => {
console.log('emoji support level', emojiSupportLevel)
})
}

View File

@ -0,0 +1,45 @@
import { ALL_EMOJI, basicAfterEach, basicBeforeEach, tick } from '../shared.js'
import Picker from '../../../src/picker/PickerElement.js'
import * as testingLibrary from '@testing-library/dom'
import { getByRole, waitFor } from '@testing-library/dom'
import Database from '../../../src/database/Database.js'
describe('Picker custom emojiVersion tests', () => {
let picker
beforeEach(async () => {
await basicBeforeEach()
})
afterEach(async () => {
await tick(20)
document.body.removeChild(picker)
await tick(20)
await new Database({ dataSource: ALL_EMOJI, locale: 'en' }).delete()
await tick(20)
await basicAfterEach()
})
test('can use the emojiVersion property', async () => {
picker = new Picker({ dataSource: ALL_EMOJI, locale: 'en', emojiVersion: '10.0' })
document.body.appendChild(picker)
await waitFor(() => expect(
// Normally this would be 20, but because we set the emoji version to 10.0, the 🥰 is excluded
testingLibrary.getAllByRole(getByRole(picker.shadowRoot, 'tabpanel'), 'menuitem')).toHaveLength(19),
{ timeout: 2000 }
)
})
test('can use the emoji-version attribute', async () => {
picker = new Picker({ dataSource: ALL_EMOJI, locale: 'en' })
picker.setAttribute('emoji-version', '10.0')
document.body.appendChild(picker)
await waitFor(() => expect(
// Normally this would be 20, but because we set the emoji version to 10.0, the 🥰 is excluded
testingLibrary.getAllByRole(getByRole(picker.shadowRoot, 'tabpanel'), 'menuitem')).toHaveLength(19),
{ timeout: 2000 }
)
})
})