parent
af58a9279d
commit
da524c240d
136
README.md
136
README.md
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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`)",
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -35,6 +35,7 @@ export interface PickerConstructorOptions {
|
|||
skinToneEmoji?: string;
|
||||
customEmoji?: CustomEmoji[];
|
||||
customCategorySorting?: (a: string, b: string) => number;
|
||||
emojiVersion?: number;
|
||||
}
|
||||
export interface I18n {
|
||||
emojiUnsupportedMessage: string;
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
)
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue