emoji-picker-element/README.md

537 lines
18 KiB
Markdown
Raw Normal View History

2020-06-06 06:02:53 +02:00
emoji-picker-element
2020-05-07 05:17:27 +02:00
====
2020-06-06 17:43:38 +02:00
A lightweight emoji picker, distributed as a custom element.
2020-05-07 05:17:27 +02:00
2020-06-09 02:26:51 +02:00
It's built on top of IndexedDB, so it consumes [far less memory](#benchmarks) than other emoji pickers.
2020-06-06 17:43:38 +02:00
It also uses [Svelte](https://svelte.dev), so it has a minimal runtime footprint.
2020-05-07 05:17:27 +02:00
2020-05-18 00:42:13 +02:00
Design goals:
2020-05-07 05:17:27 +02:00
2020-06-09 16:59:16 +02:00
- Store emoji data in IndexedDB
- Render native emoji
2020-06-06 17:43:38 +02:00
- Accessible
- Drop-in as a vanilla web component
2020-05-18 00:42:13 +02:00
## Install
2020-06-06 06:02:53 +02:00
npm install emoji-picker-element
2020-05-18 00:42:13 +02:00
## Usage
```html
2020-06-06 06:02:53 +02:00
<emoji-picker></emoji-picker>
```
2020-05-18 00:42:13 +02:00
```js
2020-06-06 06:02:53 +02:00
import 'emoji-picker-element';
2020-05-18 00:42:13 +02:00
```
2020-06-06 18:54:23 +02:00
Then listen for `emoji-click` events:
2020-06-06 17:43:38 +02:00
```js
2020-06-07 05:47:49 +02:00
document.querySelector('emoji-picker')
.addEventListener('emoji-click', event => console.log(event.detail));
2020-06-06 18:54:23 +02:00
```
This will log:
```json
{
"annotation": "grinning face",
"group": 0,
"order": 1,
"shortcodes": [ "gleeful" ],
"tags": [ "face", "grin" ],
"tokens": [ ":d", "face", "gleeful", "grin", "grinning" ],
"unicode": "😀",
"version": 1,
"emoticon": ":D"
}
```
2020-06-06 23:35:53 +02:00
## Styling
2020-05-18 00:42:13 +02:00
2020-06-07 05:47:49 +02:00
`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 changed with arbitrary CSS. Refer to the API below for style customization.
2020-06-06 23:35:53 +02:00
### Size
2020-06-06 17:43:38 +02:00
2020-06-09 17:00:48 +02:00
`emoji-picker-element` has a default size, but you can change it to whatever you want:
2020-06-06 17:43:38 +02:00
```css
emoji-picker {
width: 400px;
height: 300px;
}
```
For instance, to make it expand to fit whatever container you give it:
```css
emoji-picker {
width: 100%;
height: 100%;
}
```
2020-06-06 23:35:53 +02:00
### Dark mode
2020-06-03 04:02:26 +02:00
2020-06-06 17:43:38 +02:00
By default, `emoji-picker-element` will automatically switch to dark mode based on
[`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme).
2020-06-07 05:48:29 +02:00
Or you can add the class `dark` or `light` to force dark/light mode:
2020-06-03 04:02:26 +02:00
```html
2020-06-06 06:02:53 +02:00
<emoji-picker class="dark"></emoji-picker>
<emoji-picker class="light"></emoji-picker>
2020-06-03 04:02:26 +02:00
```
2020-06-06 23:35:53 +02:00
### CSS variables
2020-06-03 04:02:26 +02:00
2020-06-06 23:18:57 +02:00
Most colors and sizes can be styled with CSS variables. For example:
2020-06-03 04:23:07 +02:00
```css
2020-06-06 06:02:53 +02:00
emoji-picker {
--num-columns: 6;
--emoji-size: 3rem;
--background: gray;
2020-06-03 04:23:07 +02:00
}
```
2020-06-03 04:02:26 +02:00
Here is a full list of options:
<!-- CSS variable options start -->
| Variable | Default | Default (dark) | Description |
| ---------------------------- | ---------- | -------------- | ----------------------------------------------- |
2020-06-06 23:35:53 +02:00
| `--background` | `#fff` | `#222` | Background of the entire `<emoji-picker>` |
| `--border-color` | `#e0e0e0` | `#444` | |
2020-06-08 04:50:31 +02:00
| `--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 |
| `--emoji-padding` | `0.5rem` | | |
| `--emoji-size` | `1.375rem` | | |
| `--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 |
2020-06-08 04:50:31 +02:00
| `--skintone-border-radius` | `1rem` | | border radius of the skintone dropdown |
<!-- CSS variable options end -->
2020-06-06 23:35:53 +02:00
### Focus outline
For accessibility reasons, `emoji-picker-element` displays a prominent focus ring. If you want to hide the focus ring for non-keyboard users (e.g. mouse and touch only), then use the [focus-visible](https://github.com/WICG/focus-visible) polyfill, e.g.:
```js
import { applyFocusVisiblePolyfill } from 'focus-visible';
const picker = new Picker();
applyFocusVisiblePolyfill(picker.shadowRoot);
```
2020-06-09 17:01:36 +02:00
`emoji-picker-element` already ships with the proper CSS for both the `:focus-visible` standard and the polyfill.
2020-06-06 23:35:53 +02:00
## JavaScript API
### Picker
```js
import { Picker } from 'emoji-picker-element';
const picker = new Picker();
document.body.appendChild(picker);
```
2020-06-07 02:36:03 +02:00
The `new Picker(options)` constructor supports several options:
2020-06-06 23:35:53 +02:00
2020-06-07 02:36:03 +02:00
<!-- picker API start -->
#### constructor
\+ **new Picker**(`__namedParameters`: object): *Picker*
**Parameters:**
▪`Default value` **__namedParameters**: *object*= {}
Name | Type | Default | Description |
------ | ------ | ------ | ------ |
`dataSource` | string | "https://cdn.jsdelivr.net/npm/emojibase-data@5/en/data.json" | URL to fetch the emojibase data from |
2020-06-08 01:00:43 +02:00
`i18n` | I18n | - | i18n object (see below for details) |
2020-06-07 02:36:03 +02:00
`locale` | string | "en" | Locale string |
**Returns:** *Picker*
<!-- picker API end -->
2020-06-06 23:35:53 +02:00
These values can also be set at runtime, e.g.:
```js
const picker = new Picker();
picker.dataSource = '/my-emoji.json';
```
#### i18n structure
Here is the default English `i81n` object (`"en"` locale):
<!-- i18n options start -->
```json
{
"categories": {
"smileys-emotion": "Smileys and emoticons",
"people-body": "People and body",
"animals-nature": "Animals and nature",
"food-drink": "Food and drink",
"travel-places": "Travel and places",
"activities": "Activities",
"objects": "Objects",
"symbols": "Symbols",
"flags": "Flags"
},
"categoriesLabel": "Categories",
"emojiUnsupported": "Your browser does not support color emoji.",
"loading": "Loading…",
"networkError": "Could not load emoji. Try refreshing.",
"regionLabel": "Emoji picker",
"search": "Search",
2020-06-09 05:56:10 +02:00
"searchDescription": "When search results are available, press up or down to select and enter to choose.",
2020-06-06 23:35:53 +02:00
"searchResultsLabel": "Search results",
2020-06-09 05:56:10 +02:00
"skinToneDescription": "When expanded, press up or down to select and enter to choose.",
2020-06-10 04:04:23 +02:00
"skinToneLabel": "Choose a skin tone",
2020-06-08 04:50:31 +02:00
"skinTones": [
"Default",
"Light",
"Medium-Light",
"Medium",
"Medium-Dark",
"Dark"
],
"skinTonesTitle": "Skin tones"
2020-06-06 23:35:53 +02:00
}
```
<!-- i18n options end -->
Note that some of these strings are only visible to users of screen readers.
But you should still support them if you internationalize your app!
### Database
2020-06-06 23:35:53 +02:00
You can work with the database API separately, which allows you to query emoji the same
way that the picker does:
```js
2020-06-06 06:02:53 +02:00
import { Database } from 'emoji-picker-element';
const database = new Database();
await database.getEmojiBySearchPrefix('elephant'); // [{unicode: "🐘", ...}]
```
2020-06-09 17:07:50 +02:00
Note that under the hood, IndexedDB data is partitioned based on the `locale`. So if you create two `Database`s with two different `locale`s, it will store twice as much data.
2020-06-07 02:36:03 +02:00
Full API:
2020-06-07 02:36:03 +02:00
<!-- database API start -->
#### constructor
\+ **new Database**(`__namedParameters`: object): *Database*
Create a new Database.
Note that multiple Databases pointing to the same locale will share the
same underlying IndexedDB connection and database.
**Parameters:**
▪`Default value` **__namedParameters**: *object*= {}
Name | Type | Default | Description |
------ | ------ | ------ | ------ |
`dataSource` | string | "https://cdn.jsdelivr.net/npm/emojibase-data@5/en/data.json" | URL to fetch the emojibase data from |
`locale` | string | "en" | Locale string |
**Returns:** *Database*
#### close
**close**(): *Promisevoid*
Closes the underlying IndexedDB connection. The Database is not usable after that (or any other Databases
with the same locale).
**Returns:** *Promisevoid*
___
#### delete
**delete**(): *Promisevoid*
Deletes the underlying IndexedDB database. The Database is not usable after that (or any other Databases
with the same locale).
**Returns:** *Promisevoid*
___
#### getEmojiByGroup
**getEmojiByGroup**(`group`: number): *PromiseEmoji]*
Returns all emoji belonging to a group, ordered by `order`.
2020-06-07 05:46:03 +02:00
Non-numbers throw an error.
2020-06-07 02:36:03 +02:00
**Parameters:**
2020-06-07 02:36:03 +02:00
Name | Type | Description |
------ | ------ | ------ |
`group` | number | the group number |
2020-06-07 02:36:03 +02:00
**Returns:** *Promise[Emoji]*
___
#### getEmojiBySearchQuery
**getEmojiBySearchQuery**(`query`: string): *Promise[Emoji]*
Returns all emoji matching the given search query, ordered by `order`.
2020-06-07 05:46:03 +02:00
Empty/null strings throw an error.
2020-06-07 02:36:03 +02:00
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`query` | string | search query string |
**Returns:** *Promise[Emoji]*
___
#### getEmojiByShortcode
**getEmojiByShortcode**(`shortcode`: string): *Promise[Emoji | null*
Return a single emoji matching the shortcode, or null if not found.
2020-06-07 05:46:03 +02:00
The colons around the shortcode should not be included when querying, e.g.
use "slight_smile", not ":slight_smile:". Uppercase versus lowercase
does not matter. Empty/null strings throw an error.
2020-06-07 02:36:03 +02:00
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`shortcode` | string | |
**Returns:** *PromiseEmoji | null*
___
#### getEmojiByUnicode
**getEmojiByUnicode**(`unicode`: string): *PromiseEmoji | null*
Return a single emoji matching the unicode string, or null if not found.
2020-06-07 05:46:03 +02:00
Empty/null strings throw an error.
2020-06-07 02:36:03 +02:00
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`unicode` | string | unicode string |
**Returns:** *PromiseEmoji | null*
___
#### ready
**ready**(): *Promisevoid*
Resolves when the Database is ready, or throws an error if
the Database could not initialize.
Note that you don't need to do this before calling other APIs they will
all wait for this promise to resolve before doing anything.
**Returns:** *Promisevoid*
<!-- database API end -->
### Emoji object
This object is returned as the Event `detail` in the `emoji-click` event, or when querying the Database. Here is the format:
```ts
interface Emoji {
annotation: string;
emoticon?: string;
group: number;
name: string;
order: number;
shortcodes: string[];
tags?: string[];
version: number;
unicode: string;
}
```
### Tree-shaking
2020-06-06 23:18:57 +02:00
If you want to import the `Database` without the `Picker`, or you want to code-split them separately, then do:
```js
2020-06-06 06:02:53 +02:00
import Picker from 'emoji-picker-element/picker';
import Database from 'emoji-picker-element/database';
```
2020-06-06 23:18:57 +02:00
The reason for this is that `Picker` automatically registers itself as a custom element, following [web component best practices](https://justinfagnani.com/2019/11/01/how-to-publish-web-components-to-npm/). But this adds side effects, so bundlers like Webpack and Rollup do not tree-shake as well, unless the modules are imported from completely separate files.
## Data and offline
### Data source and JSON format
2020-06-06 23:18:57 +02:00
`emoji-picker-element` requires the _full_ [emojibase-data](https://github.com/milesj/emojibase) JSON file, not the "compact" one. If you would like to trim the JSON file down even further, then you can modify the JSON to only contain these keys:
```json
[
"annotation", "emoji", "emoticon", "group",
2020-06-09 17:03:35 +02:00
"order", "shortcodes", "skins", "tags", "version"
]
```
2020-06-06 23:18:57 +02:00
You can fetch the emoji JSON file from wherever you want. However, it's recommended that your server expose an `ETag` header if so, `emoji-picker-element` can avoid re-downloading the entire JSON file over and over again. Instead, it will fire off a `HEAD` request and just check the `ETag`.
2020-06-04 04:08:15 +02:00
2020-06-09 17:07:50 +02:00
If the server hosting the JSON file is not the same as the one containing the emoji picker, then the cross-origin server will also need to expose `Access-Control-Allow-Origin: *` and `Access-Control-Allow-Headers: *`. (Note that `jsdelivr` already does this, which is partly why it is the default.)
2020-06-04 04:08:15 +02:00
2020-06-09 17:07:50 +02:00
Unfortunately [Safari does not currently support `Access-Control-Allow-Headers`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers#Browser_compatibility), meaning that the `ETag` header will not be available cross-origin. In that case, `emoji-picker-element` will fall back to the less performant option. If you want to avoid this, host the JSON file on the same server as your web app.
2020-06-04 04:08:15 +02:00
### Offline-first
2020-06-06 23:18:57 +02:00
`emoji-picker-element` uses a "stale while revalidate" strategy to update emoji data. In other words, it will use any existing data it finds in IndexedDB, and lazily update via the `dataSource` in case that data has changed. This means it will work [offline-first](http://offlinefirst.org/) the second time it runs.
If you would like to manage the database yourself (e.g. to ensure that it's correctly populated before displaying the `Picker`), then create a new `Database` instance and wait for its `ready()` promise to resolve:
```js
const database = new Database()
try {
await database.ready()
} catch (err) {
// Deal with any errors (e.g. offline)
}
```
2020-06-06 23:18:57 +02:00
If `emoji-picker-element` fails to fetch the JSON data the first time it loads, then it will display an error message.
2020-05-18 00:42:13 +02:00
## Design decisions
### IndexedDB
2020-06-06 23:18:57 +02:00
Why IndexedDB? Well, the [`emojibase-data`](https://github.com/milesj/emojibase) English JSON file is [854kB](https://unpkg.com/browse/emojibase-data@5.0.1/en/), and the "compact" version is still 543kB. That's a lot of data to keep in memory just for an emoji picker. And it's not as if that number is ever going down; the Unicode Consortium keeps adding more emoji every year.
2020-05-08 23:59:22 +02:00
Using IndexedDB has a few advantages:
1. We don't need to keep half a megabyte of emoji data in memory at all times.
2. The second time your visitors visit your website, we don't even need to download, parse, and index the emoji data, because it's already available on their hard drive.
2020-06-09 16:59:16 +02:00
3. Heck, you can even preload the IndexedDB data in a web worker or service worker. That way, you only pay the UI thread cost of accessing IndexedDB, not of fetching the data, indexing the data, or inserting it into IndexedDB.
2020-05-07 05:17:27 +02:00
2020-05-18 00:42:13 +02:00
### Native emoji
2020-05-07 05:17:27 +02:00
2020-06-09 16:59:16 +02:00
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), `emoji-picker-element` only renders native emoji. This means it is limited to the emoji actually installed on the user's device.
2020-06-09 16:59:16 +02:00
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.
2020-06-06 06:43:13 +02:00
### JSON loading
Why only allow loading via a URL rather than directly passing in a JSON object? A few reasons:
First, it bloats the size of the JavaScript bundle to do so. `emoji-picker-element` is optimized for second load, where
it doesn't even need to fetch, parse, or read the full JSON object into memory it can just rely on IndexedDB.
Sure, this could be optional, but if an anti-pattern is allowed, then people might do it out of convenience.
Second, browsers deal with JSON more efficiently when it's loaded via `fetch()` rather than embedded in JavaScript. It's
[faster for the browser to parse JSON than JavaScript](https://joreteg.com/blog/improving-redux-state-transfer-performance),
plus using the `await (await fetch()).json()` pattern gives the browser more room for optimizations, since you're
explicitly telling it to cache and parse the data (asynchronously) as JSON. (I'm not aware of any browsers that do
this, e.g. off-main-thread JSON parsing, but it's certainly possible!)
### Browser support
2020-06-06 23:18:57 +02:00
`emoji-picker-element` only supports the latest versions of Chrome, Firefox, and Safari, as well as equivalent browsers (Edge, Opera, etc.).
2020-06-09 02:26:51 +02:00
## Benchmarks
Benchmark code can be found in the `test/` directory. See [Contributing](#contributing) for how to run the scripts.
### Memory usage
This test navigates to four pages: 1) an empty page, 2) the same page containing `emoji-picker-element` with the standard configuration, 3) a page containing the `emojibase` English `compact.json` object, and 4) a page containing the full `data.json` object.
| Scenario | Bytes | Relative to blank page |
| -------- | ----------------- | ---------------------- |
2020-06-09 16:59:16 +02:00
| blank | 763 kB (763097) | 0 B (0) |
| picker | 1.32 MB (1322356) | 559 kB (559259) |
| compact | 1.53 MB (1533547) | 770 kB (770450) |
| full | 1.87 MB (1868599) | 1.11 MB (1105502) |
2020-06-09 02:26:51 +02:00
2020-06-09 17:07:50 +02:00
As you can see, `emoji-picker-element` consumes less memory than merely loading the JSON files and keeping the reference. So any emoji picker that keeps these JSON objects in memory is already using more memory than `emoji-picker-element`, in addition to whatever it's doing with JS/CSS/DOM.
2020-06-09 02:26:51 +02:00
[`performance.measureMemory()`](https://web.dev/monitor-total-page-memory-usage/) in Chrome is used to calculate memory usage.
### Bundle size
2020-06-09 16:59:16 +02:00
30.13kB at the time of writing (minified but not gzipped, for both the `Picker` and the `Database` combined).
2020-06-09 02:26:51 +02:00
## Contributing
Install
yarn
2020-06-06 23:18:57 +02:00
Lint:
yarn lint
Fix most lint issues:
yarn lint:fix
Run the tests:
2020-05-07 05:17:27 +02:00
2020-06-06 23:18:57 +02:00
yarn test
Check code coverage:
2020-06-08 02:39:14 +02:00
yarn cover
2020-06-08 04:48:38 +02:00
Run a local dev server on `localhost:3000`:
yarn dev
2020-06-08 02:39:14 +02:00
Benchmark memory usage:
yarn benchmark:memory
Benchmark bundle size:
yarn benchmark:bundlesize