Merge branch 'emojimart' into get_emoji_data_from_native

release
Peder Johnsen 2019-03-20 09:08:32 +00:00
commit 22a5ca587f
38 changed files with 7351 additions and 6000 deletions

View File

@ -2,6 +2,29 @@
"presets": ["react"], "presets": ["react"],
"plugins": [ "plugins": [
"check-es2015-constants", "check-es2015-constants",
"transform-object-rest-spread",
"transform-runtime",
[
"module-resolver",
{
"alias": {
"babel-runtime/core-js/object/get-prototype-of": "./src/polyfills/objectGetPrototypeOf",
"babel-runtime/helpers/extends": "./src/polyfills/extends",
"babel-runtime/helpers/inherits": "./src/polyfills/inherits",
"babel-runtime/helpers/createClass": "./src/polyfills/createClass",
"babel-runtime/helpers/possibleConstructorReturn": "./src/polyfills/possibleConstructorReturn",
"babel-runtime/helpers/classCallCheck": "./src/polyfills/classCallCheck",
"babel-runtime/core-js/object/keys": "./src/polyfills/keys"
}
}
],
[
"transform-define", "scripts/define.js"
]
],
"env": {
"legacy-es": {
"plugins": [
"transform-es2015-arrow-functions", "transform-es2015-arrow-functions",
"transform-es2015-block-scoped-functions", "transform-es2015-block-scoped-functions",
"transform-es2015-block-scoping", "transform-es2015-block-scoping",
@ -19,32 +42,25 @@
"transform-es2015-sticky-regex", "transform-es2015-sticky-regex",
"transform-es2015-template-literals", "transform-es2015-template-literals",
"transform-es2015-unicode-regex", "transform-es2015-unicode-regex",
"transform-regenerator", "transform-regenerator"
"transform-object-rest-spread",
"transform-runtime",
"transform-react-remove-prop-types",
[
"transform-define", "scripts/define.js"
],
[
"module-resolver",
{
"alias": {
"babel-runtime/core-js/object/get-prototype-of": "./src/polyfills/objectGetPrototypeOf",
"babel-runtime/helpers/extends": "./src/polyfills/extends",
"babel-runtime/helpers/inherits": "./src/polyfills/inherits",
"babel-runtime/helpers/createClass": "./src/polyfills/createClass",
"babel-runtime/helpers/possibleConstructorReturn": "./src/polyfills/possibleConstructorReturn"
}
}
] ]
], },
"env": { "legacy-cjs": {
"cjs": {
"plugins": [ "plugins": [
"transform-es2015-modules-commonjs" "transform-es2015-modules-commonjs"
] ]
},
"test": {
"presets": [
[
"env",
{
"targets": {
"node": "current"
}
}
]
]
} }
} }
} }

1
.gitignore vendored
View File

@ -2,5 +2,6 @@
node_modules/ node_modules/
dist/ dist/
dist-es/ dist-es/
dist-modern/
stats.json stats.json
report.html report.html

View File

@ -4,7 +4,6 @@ scripts/
src/ src/
docs/ docs/
spec/
example/ example/
karma.conf.js karma.conf.js
yarn.lock yarn.lock

105
README.md
View File

@ -58,6 +58,7 @@ import { Picker } from 'emoji-mart'
#### I18n #### I18n
```js ```js
search: 'Search', search: 'Search',
clear: 'Clear', // Accessible label on "clear" button
notfound: 'No Emoji Found', notfound: 'No Emoji Found',
skintext: 'Choose your default skin tone', skintext: 'Choose your default skin tone',
categories: { categories: {
@ -72,7 +73,16 @@ categories: {
symbols: 'Symbols', symbols: 'Symbols',
flags: 'Flags', flags: 'Flags',
custom: 'Custom', custom: 'Custom',
} },
categorieslabel: 'Emoji categories', // Accessible title for the list of categories
skintones: {
1: 'Default Skin Tone',
2: 'Light Skin Tone',
3: 'Medium-Light Skin Tone',
4: 'Medium Skin Tone',
5: 'Medium-Dark Skin Tone',
6: 'Dark Skin Tone',
},
``` ```
#### Sheet sizes #### Sheet sizes
@ -141,7 +151,7 @@ import { NimblePicker } from 'emoji-mart'
text: '', text: '',
emoticons: [], emoticons: [],
custom: true, custom: true,
imageUrl: 'https://assets-cdn.github.com/images/icons/emoji/octocat.png?v7' imageUrl: 'https://github.githubassets.com/images/icons/emoji/octocat.png'
} }
``` ```
@ -220,7 +230,7 @@ Following the `dangerouslySetInnerHTML` example above, make sure the wrapping `s
``` ```
## Custom emojis ## Custom emojis
You can provide custom emojis which will show up in their own category. You can provide custom emojis which will show up in their own category. You can either use a single image as imageUrl or use a spritesheet as shown in the second object.
```js ```js
import { Picker } from 'emoji-mart' import { Picker } from 'emoji-mart'
@ -232,7 +242,20 @@ const customEmojis = [
text: '', text: '',
emoticons: [], emoticons: [],
keywords: ['github'], keywords: ['github'],
imageUrl: 'https://assets-cdn.github.com/images/icons/emoji/octocat.png?v7' imageUrl: 'https://github.githubassets.com/images/icons/emoji/octocat.png'
},
{
name: 'Test Flag',
short_names: ['test'],
text: '',
emoticons: [],
keywords: ['test', 'flag'],
spriteUrl: 'https://unpkg.com/emoji-datasource-twitter@4.0.4/img/twitter/sheets-256/64.png',
sheet_x: 1,
sheet_y: 1,
size: 64,
sheetColumns: 52,
sheetRows: 52,
}, },
] ]
@ -245,7 +268,7 @@ You can provide a custom Not Found object which will allow the appearance of the
```js ```js
import { Picker } from 'emoji-mart' import { Picker } from 'emoji-mart'
const notFound = () => <img src='https://assets-cdn.github.com/images/icons/emoji/octocat.png?v7' /> const notFound = () => <img src='https://github.githubassets.com/images/icons/emoji/octocat.png' />
<Picker notFound={notFound} /> <Picker notFound={notFound} />
``` ```
@ -258,7 +281,7 @@ import { Picker } from 'emoji-mart'
const customIcons = { const customIcons = {
categories: { categories: {
recent: () => <img src='https://assets-cdn.github.com/images/icons/emoji/octocat.png?v7' />, recent: () => <img src='https://github.githubassets.com/images/icons/emoji/octocat.png' />,
foods: () => <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0l6.084 24H8L1.916 0zM21 5h-4l-1-4H4l3 12h3l1 4h13L21 5zM6.563 3h7.875l2 8H8.563l-2-8zm8.832 10l-2.856 1.904L12.063 13h3.332zM19 13l-1.5-6h1.938l2 8H16l3-2z"/></svg>, foods: () => <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0l6.084 24H8L1.916 0zM21 5h-4l-1-4H4l3 12h3l1 4h13L21 5zM6.563 3h7.875l2 8H8.563l-2-8zm8.832 10l-2.856 1.904L12.063 13h3.332zM19 13l-1.5-6h1.938l2 8H16l3-2z"/></svg>,
people: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3 2l10 6-10 6z"></path></svg> people: () => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M3 2l10 6-10 6z"></path></svg>
} }
@ -382,12 +405,72 @@ Apple / Google / Twitter / EmojiOne / Messenger / Facebook
## Not opinionated ## Not opinionated
**Emoji Mart** doesnt automatically insert anything into a text input, nor does it show or hide itself. It simply returns an `emoji` object. Its up to the developer to mount/unmount (its fast!) and position the picker. You can use the returned object as props for the `EmojiMart.Emoji` component. You could also use `emoji.colons` to insert text into a textarea or `emoji.native` to use the emoji. **Emoji Mart** doesnt automatically insert anything into a text input, nor does it show or hide itself. It simply returns an `emoji` object. Its up to the developer to mount/unmount (its fast!) and position the picker. You can use the returned object as props for the `EmojiMart.Emoji` component. You could also use `emoji.colons` to insert text into a textarea or `emoji.native` to use the emoji.
## Development
```sh ## Optimizing for production
$ yarn build
$ yarn start ### Modern/ES builds
$ yarn storybook
**Emoji Mart** comes in three flavors:
``` ```
dist
dist-es
dist-modern
```
- `dist` is the standard build with the highest level of compatibility.
- `dist-es` is the same, but uses ES modules for better tree-shaking.
- `dist-modern` removes features not needed in modern evergreen browsers (i.e. latest Chrome, Edge, Firefox, and Safari).
The default builds are `dist` and `dist-es`. (In Webpack, one or the other will be chosen based on your [resolve main fields](https://webpack.js.org/configuration/resolve/#resolve-mainfields).)
If you want to use `dist-modern`, you must explicitly import it:
```js
import { Picker } from 'emoji-mart/dist-modern/index.js'
```
Using something like Babel, you can transpile the modern build to suit your own needs.
### Removing prop-types
To remove [prop-types](https://github.com/facebook/prop-types) in production, use [babel-plugin-transform-react-remove-prop-types](https://github.com/oliviertassinari/babel-plugin-transform-react-remove-prop-types):
```bash
npm install --save-dev babel-plugin-transform-react-remove-prop-types
```
Then add to your `.babelrc`:
```json
"plugins": [
[
"transform-react-remove-prop-types",
{
"removeImport": true,
"additionalLibraries": [
"../../utils/shared-props"
]
}
]
]
```
You'll also need to ensure that Babel is transpiling `emoji-mart`, e.g. [by not excluding `node_modules` in `babel-loader`](https://github.com/babel/babel-loader#usage).
## Development
```bash
yarn build
```
In two separate tabs:
```bash
yarn start
yarn storybook
```
The storybook is hosted at `localhost:6006`, and the code will be built on-the-fly.
## 🎩 Hat tips! ## 🎩 Hat tips!
Powered by [iamcal/emoji-data](https://github.com/iamcal/emoji-data) and inspired by [iamcal/js-emoji](https://github.com/iamcal/js-emoji).<br> Powered by [iamcal/emoji-data](https://github.com/iamcal/emoji-data) and inspired by [iamcal/js-emoji](https://github.com/iamcal/js-emoji).<br>

View File

@ -49,6 +49,10 @@
padding: 12px 4px; padding: 12px 4px;
overflow: hidden; overflow: hidden;
transition: color .1s ease-out; transition: color .1s ease-out;
margin: 0;
box-shadow: none;
background: none;
border: none;
} }
.emoji-mart-anchor:hover, .emoji-mart-anchor:hover,
.emoji-mart-anchor-selected { .emoji-mart-anchor-selected {
@ -74,7 +78,7 @@
.emoji-mart-anchors svg, .emoji-mart-anchors svg,
.emoji-mart-anchors img { .emoji-mart-anchors img {
fill: currentColor; fill: #858585;
height: 18px; height: 18px;
width: 18px; width: 18px;
} }
@ -102,12 +106,22 @@
outline: 0; outline: 0;
} }
.emoji-mart-search input,
.emoji-mart-search input::-webkit-search-decoration,
.emoji-mart-search input::-webkit-search-cancel-button,
.emoji-mart-search input::-webkit-search-results-button,
.emoji-mart-search input::-webkit-search-results-decoration {
/* remove webkit/blink styles for <input type="search">
* via https://stackoverflow.com/a/9422689 */
-webkit-appearance: none;
}
.emoji-mart-search-icon { .emoji-mart-search-icon {
position: absolute; position: absolute;
top: 9px; top: 7px;
right: 16px; right: 11px;
z-index: 2; z-index: 2;
padding: 0; padding: 2px 5px 1px;
border: none; border: none;
background: none; background: none;
} }
@ -146,14 +160,31 @@
background-color: rgba(255, 255, 255, .95); background-color: rgba(255, 255, 255, .95);
} }
.emoji-mart-category-list {
margin: 0;
padding: 0;
}
.emoji-mart-category-list li {
list-style: none;
margin: 0;
padding: 0;
display: inline-block;
}
.emoji-mart-emoji { .emoji-mart-emoji {
position: relative; position: relative;
display: inline-block; display: inline-block;
font-size: 0; font-size: 0;
margin: 0;
padding: 0;
border: none;
background: none;
box-shadow: none;
} }
.emoji-mart-emoji-native { .emoji-mart-emoji-native {
font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji"; font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", "EmojiOne Color", "Android Emoji";
} }
.emoji-mart-no-results { .emoji-mart-no-results {
@ -369,3 +400,17 @@
.emoji-mart-skin-tone-4 { background-color: #bf8f68 } .emoji-mart-skin-tone-4 { background-color: #bf8f68 }
.emoji-mart-skin-tone-5 { background-color: #9b643d } .emoji-mart-skin-tone-5 { background-color: #9b643d }
.emoji-mart-skin-tone-6 { background-color: #594539 } .emoji-mart-skin-tone-6 { background-color: #594539 }
/* For screenreaders only, via https://stackoverflow.com/a/19758620 */
.emoji-mart-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}

File diff suppressed because one or more lines are too long

View File

@ -49,6 +49,10 @@
padding: 12px 4px; padding: 12px 4px;
overflow: hidden; overflow: hidden;
transition: color .1s ease-out; transition: color .1s ease-out;
margin: 0;
box-shadow: none;
background: none;
border: none;
} }
.emoji-mart-anchor:hover, .emoji-mart-anchor:hover,
.emoji-mart-anchor-selected { .emoji-mart-anchor-selected {
@ -74,7 +78,7 @@
.emoji-mart-anchors svg, .emoji-mart-anchors svg,
.emoji-mart-anchors img { .emoji-mart-anchors img {
fill: currentColor; fill: #858585;
height: 18px; height: 18px;
width: 18px; width: 18px;
} }
@ -102,12 +106,22 @@
outline: 0; outline: 0;
} }
.emoji-mart-search input,
.emoji-mart-search input::-webkit-search-decoration,
.emoji-mart-search input::-webkit-search-cancel-button,
.emoji-mart-search input::-webkit-search-results-button,
.emoji-mart-search input::-webkit-search-results-decoration {
/* remove webkit/blink styles for <input type="search">
* via https://stackoverflow.com/a/9422689 */
-webkit-appearance: none;
}
.emoji-mart-search-icon { .emoji-mart-search-icon {
position: absolute; position: absolute;
top: 9px; top: 7px;
right: 16px; right: 11px;
z-index: 2; z-index: 2;
padding: 0; padding: 2px 5px 1px;
border: none; border: none;
background: none; background: none;
} }
@ -146,14 +160,31 @@
background-color: rgba(255, 255, 255, .95); background-color: rgba(255, 255, 255, .95);
} }
.emoji-mart-category-list {
margin: 0;
padding: 0;
}
.emoji-mart-category-list li {
list-style: none;
margin: 0;
padding: 0;
display: inline-block;
}
.emoji-mart-emoji { .emoji-mart-emoji {
position: relative; position: relative;
display: inline-block; display: inline-block;
font-size: 0; font-size: 0;
margin: 0;
padding: 0;
border: none;
background: none;
box-shadow: none;
} }
.emoji-mart-emoji-native { .emoji-mart-emoji-native {
font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji"; font-family: "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Apple Color Emoji", "Twemoji Mozilla", "Noto Color Emoji", "EmojiOne Color", "Android Emoji";
} }
.emoji-mart-no-results { .emoji-mart-no-results {
@ -369,3 +400,17 @@
.emoji-mart-skin-tone-4 { background-color: #bf8f68 } .emoji-mart-skin-tone-4 { background-color: #bf8f68 }
.emoji-mart-skin-tone-5 { background-color: #9b643d } .emoji-mart-skin-tone-5 { background-color: #9b643d }
.emoji-mart-skin-tone-6 { background-color: #594539 } .emoji-mart-skin-tone-6 { background-color: #594539 }
/* For screenreaders only, via https://stackoverflow.com/a/19758620 */
.emoji-mart-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}

View File

@ -16,8 +16,8 @@
text-align: center; text-align: center;
} }
button + button { margin-left: .5em } .sets button + button { margin-left: .5em }
button { .sets button {
padding: .4em .6em; padding: .4em .6em;
border-radius: 5px; border-radius: 5px;
border: 1px solid rgba(0, 0, 0, .1); border: 1px solid rgba(0, 0, 0, .1);
@ -26,7 +26,7 @@
cursor: pointer; cursor: pointer;
} }
button[disabled] { .sets button[disabled] {
border-color: #ae65c5; border-color: #ae65c5;
cursor: default; cursor: default;
} }

View File

@ -14,13 +14,24 @@ const CUSTOM_EMOJIS = [
name: 'Octocat', name: 'Octocat',
short_names: ['octocat'], short_names: ['octocat'],
keywords: ['github'], keywords: ['github'],
imageUrl: 'https://assets-cdn.github.com/images/icons/emoji/octocat.png?v7' imageUrl: 'https://github.githubassets.com/images/icons/emoji/octocat.png'
}, },
{ {
name: 'Squirrel', name: 'Squirrel',
short_names: ['shipit', 'squirrel'], short_names: ['shipit', 'squirrel'],
keywords: ['github'], keywords: ['github'],
imageUrl: 'https://assets-cdn.github.com/images/icons/emoji/shipit.png?v7' imageUrl: 'https://github.githubassets.com/images/icons/emoji/shipit.png'
},
{
name: 'Test Flag',
short_names: ['test'],
keywords: ['test', 'flag'],
spriteUrl: 'https://unpkg.com/emoji-datasource-twitter@4.0.4/img/twitter/sheets-256/64.png',
sheet_x: 1,
sheet_y: 1,
size: 64,
sheetColumns: 52,
sheetRows: 52,
}, },
] ]
@ -42,7 +53,7 @@ class Example extends React.Component {
<h1>Emoji Mart 🏬</h1> <h1>Emoji Mart 🏬</h1>
</div> </div>
<div className="row"> <div className="row sets">
{['native', 'apple', 'google', 'twitter', 'emojione', 'messenger', 'facebook'].map((set) => { {['native', 'apple', 'google', 'twitter', 'emojione', 'messenger', 'facebook'].map((set) => {
var props = { disabled: !this.state.native && set == this.state.set } var props = { disabled: !this.state.native && set == this.state.set }

View File

@ -1,73 +0,0 @@
// Karma configuration
// Generated on Fri Jan 27 2017 13:33:03 GMT-0700 (MST)
var webpackConfig = require('./spec/webpack.config.js');
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'spec/*spec.js',
],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'spec/*spec.js': ['webpack'],
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
webpack: webpackConfig,
})
}

View File

@ -1,6 +1,6 @@
{ {
"name": "emoji-mart", "name": "emoji-mart",
"version": "2.9.1", "version": "2.10.0",
"description": "Customizable Slack-like emoji picker for React", "description": "Customizable Slack-like emoji picker for React",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist-es/index.js", "module": "dist-es/index.js",
@ -19,7 +19,9 @@
"url": "https://github.com/missive/emoji-mart/issues" "url": "https://github.com/missive/emoji-mart/issues"
}, },
"homepage": "https://github.com/missive/emoji-mart", "homepage": "https://github.com/missive/emoji-mart",
"dependencies": {}, "dependencies": {
"prop-types": "^15.6.0"
},
"peerDependencies": { "peerDependencies": {
"react": "^0.14.0 || ^15.0.0-0 || ^16.0.0" "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0"
}, },
@ -29,55 +31,52 @@
"@storybook/addon-links": "^3.2.10", "@storybook/addon-links": "^3.2.10",
"@storybook/addon-options": "3.2.10", "@storybook/addon-options": "3.2.10",
"@storybook/react": "^3.2.11", "@storybook/react": "^3.2.11",
"babel-cli": "^6.26.0", "babel-cli": "^6.0.0",
"babel-core": "6.7.2", "babel-core": "^6.0.0",
"babel-loader": "^7.1.2", "babel-jest": "^23.6.0",
"babel-loader": "^7.0.0",
"babel-plugin-module-resolver": "2.7.1", "babel-plugin-module-resolver": "2.7.1",
"babel-plugin-transform-define": "^1.3.0", "babel-plugin-transform-define": "^1.3.0",
"babel-plugin-transform-es2015-destructuring": "6.9.0", "babel-plugin-transform-es2015-destructuring": "6.9.0",
"babel-plugin-transform-object-rest-spread": "6.8.0", "babel-plugin-transform-object-rest-spread": "6.8.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.8",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "6.6.0", "babel-preset-es2015": "6.6.0",
"babel-preset-react": "6.5.0", "babel-preset-react": "6.5.0",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"emoji-datasource": "4.0.4", "emoji-datasource": "4.0.4",
"emojilib": "^2.2.1", "emojilib": "^2.2.1",
"inflection": "1.10.0", "inflection": "1.10.0",
"jasmine-core": "^2.5.2", "jest": "^23.0.0",
"karma": "^1.4.0",
"karma-chrome-launcher": "^2.0.0",
"karma-cli": "^1.0.1",
"karma-jasmine": "^1.1.0",
"karma-webpack": "^2.0.4",
"mkdirp": "0.5.1", "mkdirp": "0.5.1",
"prettier": "1.11.1", "prettier": "1.11.1",
"prop-types": "^15.6.0",
"react": "^16.0.0", "react": "^16.0.0",
"react-dom": "^16.0.0", "react-dom": "^16.0.0",
"react-test-renderer": "^16.8.4",
"rimraf": "2.5.2", "rimraf": "2.5.2",
"size-limit": "^0.11.4", "size-limit": "^0.11.4",
"webpack": "^3.6.0" "webpack": "^3.6.0"
}, },
"scripts": { "scripts": {
"clean": "rm -rf dist/ dist-es/", "clean": "rm -rf dist/ dist-es/ dist-modern/",
"build:data": "node scripts/build-data", "build:data": "node scripts/build-data",
"build:dist": "npm run build:cjs && npm run build:es", "build:dist": "npm run build:cjs && npm run build:es && npm run build:modern",
"build:cjs": "BABEL_ENV=cjs babel src --out-dir dist --copy-files", "build:cjs": "BABEL_ENV=legacy-cjs babel src --out-dir dist --copy-files --ignore '**/*.test.js'",
"build:es": "babel src --out-dir dist-es --copy-files", "build:es": "BABEL_ENV=legacy-es babel src --out-dir dist-es --copy-files --ignore '**/*.test.js'",
"build:modern": "babel src --out-dir dist-modern --copy-files --ignore '**/*.test.js'",
"build:docs": "cp css/emoji-mart.css docs && webpack --config ./docs/webpack.config.js", "build:docs": "cp css/emoji-mart.css docs && webpack --config ./docs/webpack.config.js",
"build": "npm run clean && npm run build:dist", "build": "npm run clean && npm run build:dist",
"watch": "BABEL_ENV=cjs babel src --watch --out-dir dist --copy-files", "watch": "BABEL_ENV=cjs babel src --watch --out-dir dist --copy-files --ignore '**/*.test.js'",
"start": "npm run watch", "start": "npm run watch",
"stats": "webpack --config ./spec/webpack.config.js --json > spec/stats.json",
"react:clean": "rimraf node_modules/{react,react-dom,react-addons-test-utils}", "react:clean": "rimraf node_modules/{react,react-dom,react-addons-test-utils}",
"react:14": "npm run react:clean && npm i react@^0.14 react-dom@^0.14 react-addons-test-utils@^0.14 --save-dev", "react:14": "npm run react:clean && npm i react@^0.14 react-dom@^0.14 react-addons-test-utils@^0.14 --save-dev",
"react:15": "npm run react:clean && npm i react@^15 react-dom@^15 react-addons-test-utils@^15 --save-dev", "react:15": "npm run react:clean && npm i react@^15 react-dom@^15 react-addons-test-utils@^15 --save-dev",
"test": "NODE_ENV=test karma start && size-limit", "test": "npm run clean && jest",
"prepublishOnly": "npm run build", "prepublishOnly": "npm run build",
"storybook": "start-storybook -p 6006", "storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook", "build-storybook": "build-storybook",
"prettier": "prettier --write \"{src,scripts,spec}/**/*.js\"" "prettier": "prettier --write \"{src,scripts}/**/*.js\"",
"prepare": "npm run build:dist"
}, },
"size-limit": [ "size-limit": [
{ {

View File

@ -1,44 +0,0 @@
import emojiIndex from '../src/utils/emoji-index/emoji-index'
describe('#emojiIndex', () => {
describe('search', function() {
it('should work', () => {
expect(emojiIndex.search('pineapple')).toEqual([
{
id: 'pineapple',
name: 'Pineapple',
colons: ':pineapple:',
emoticons: [],
unified: '1f34d',
skin: null,
native: '🍍',
},
])
})
it('should filter only emojis we care about, exclude pineapple', () => {
let emojisToShowFilter = (data) => {
data.unified !== '1F34D'
}
expect(
emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id),
).not.toContain('pineapple')
})
it('can include/exclude categories', () => {
expect(emojiIndex.search('flag', { include: ['people'] })).toEqual([])
})
it('can search for thinking_face', () => {
expect(emojiIndex.search('thinking_fac').map((x) => x.id)).toEqual([
'thinking_face',
])
})
it('can search for woman-facepalming', () => {
expect(emojiIndex.search('woman-facep').map((x) => x.id)).toEqual([
'woman-facepalming',
])
})
})
})

View File

@ -1,47 +0,0 @@
import React from 'react'
import TestUtils from 'react-dom/test-utils'
import data from '../data/all.json'
import { NimblePicker } from '../src/components'
const { click } = TestUtils.Simulate
const {
renderIntoDocument,
scryRenderedComponentsWithType,
findRenderedComponentWithType,
} = TestUtils
const render = (props = {}) => {
const defaultProps = { data }
return renderIntoDocument(<NimblePicker {...defaultProps} {...props} />)
}
describe('NimblePicker', () => {
let subject
it('works', () => {
subject = render()
expect(subject).toBeDefined()
})
describe('categories', () => {
it('shows 10 by default', () => {
subject = render()
expect(subject.categories.length).toEqual(10)
})
it('will not show some based upon our filter', () => {
subject = render({ emojisToShowFilter: (unified) => false })
expect(subject.categories.length).toEqual(2)
})
it('maintains category ids after it is filtered', () => {
subject = render({ emojisToShowFilter: (emoji) => true })
const categoriesWithIds = subject.categories.filter(
(category) => category.id,
)
expect(categoriesWithIds.length).toEqual(10)
})
})
})

View File

@ -1,61 +0,0 @@
var path = require('path')
var pack = require('../package.json')
var webpack = require('webpack')
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin
var PROD = process.env.NODE_ENV === 'production'
var TEST = process.env.NODE_ENV === 'test'
var config = {
entry: path.resolve('src/index.js'),
output: {
path: path.resolve('spec'),
filename: 'bundle.js',
library: 'EmojiMart',
libraryTarget: 'umd',
},
externals: [],
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
include: [path.resolve('src'), path.resolve('spec')],
},
],
},
resolve: {
extensions: ['.js'],
},
plugins: [
new webpack.DefinePlugin({
EMOJI_DATASOURCE_VERSION: `'${pack.devDependencies['emoji-datasource']}'`,
}),
],
bail: true,
}
if (!TEST) {
config.externals = config.externals.concat([
{
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
},
])
config.plugins = config.plugins.concat([
new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false }),
])
}
module.exports = config

View File

@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Renders <NotFound> component 1`] = `
<div
className="emoji-mart-no-results"
>
<button
aria-label="🕵️, sleuth_or_spy"
className="emoji-mart-emoji emoji-mart-emoji-native"
onClick={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
title={null}
>
<span
style={
Object {
"display": "inline-block",
"fontSize": 38,
"height": 38,
"width": 38,
"wordBreak": "keep-all",
}
}
>
🕵️
</span>
</button>
<div
className="emoji-mart-no-results-label"
>
No Emoji Found
</div>
</div>
`;

View File

@ -0,0 +1,32 @@
import React from 'react'
import NotFound from '../not-found'
import renderer from 'react-test-renderer'
import data from '../../../data/apple'
const i18n = {
notfound: 'No Emoji Found',
}
test('Renders <NotFound> component', () => {
const emojiProps = {
native: true,
skin: 1,
size: 24,
set: 'apple',
sheetSize: 64,
forceSize: true,
tooltip: false,
}
const component = renderer.create(
<NotFound
data={data}
notFound={() => {}}
notFoundEmoji={'sleuth_or_spy'}
emojiProps={emojiProps}
i18n={i18n}
/>,
)
let tree = component.toJSON()
expect(tree).toMatchSnapshot()
})

View File

@ -28,7 +28,7 @@ export default class Anchors extends React.PureComponent {
{ selected } = this.state { selected } = this.state
return ( return (
<div className="emoji-mart-anchors"> <nav className="emoji-mart-anchors" aria-label={i18n.categorieslabel}>
{categories.map((category, i) => { {categories.map((category, i) => {
var { id, name, anchor } = category, var { id, name, anchor } = category,
isSelected = name == selected isSelected = name == selected
@ -38,8 +38,9 @@ export default class Anchors extends React.PureComponent {
} }
return ( return (
<span <button
key={id} key={id}
aria-label={i18n.categories[id]}
title={i18n.categories[id]} title={i18n.categories[id]}
data-index={i} data-index={i}
onClick={this.handleClick} onClick={this.handleClick}
@ -55,15 +56,15 @@ export default class Anchors extends React.PureComponent {
className="emoji-mart-anchor-bar" className="emoji-mart-anchor-bar"
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
/> />
</span> </button>
) )
})} })}
</div> </nav>
) )
} }
} }
Anchors.propTypes = { Anchors.propTypes /* remove-proptypes */ = {
categories: PropTypes.array, categories: PropTypes.array,
onAnchorClick: PropTypes.func, onAnchorClick: PropTypes.func,
icons: PropTypes.object, icons: PropTypes.object,

View File

@ -16,8 +16,6 @@ export default class Category extends React.Component {
} }
componentDidMount() { componentDidMount() {
this.parent = this.container.parentNode
this.margin = 0 this.margin = 0
this.minMargin = 0 this.minMargin = 0
@ -66,11 +64,18 @@ export default class Category extends React.Component {
} }
memoizeSize() { memoizeSize() {
if (!this.container) {
// probably this is a test environment, e.g. jest
this.top = 0
this.maxMargin = 0
return
}
var parent = this.container.parentElement
var { top, height } = this.container.getBoundingClientRect() var { top, height } = this.container.getBoundingClientRect()
var { top: parentTop } = this.parent.getBoundingClientRect() var { top: parentTop } = parent.getBoundingClientRect()
var { height: labelHeight } = this.label.getBoundingClientRect() var { height: labelHeight } = this.label.getBoundingClientRect()
this.top = top - parentTop + this.parent.scrollTop this.top = top - parentTop + parent.scrollTop
if (height == 0) { if (height == 0) {
this.maxMargin = 0 this.maxMargin = 0
@ -176,9 +181,10 @@ export default class Category extends React.Component {
} }
return ( return (
<div <section
ref={this.setContainerRef} ref={this.setContainerRef}
className="emoji-mart-category" className="emoji-mart-category"
aria-label={i18n.categories[id]}
style={containerStyles} style={containerStyles}
> >
<div <div
@ -186,15 +192,23 @@ export default class Category extends React.Component {
data-name={name} data-name={name}
className="emoji-mart-category-label" className="emoji-mart-category-label"
> >
<span style={labelSpanStyles} ref={this.setLabelRef}> <span
style={labelSpanStyles}
ref={this.setLabelRef}
aria-hidden={true /* already labeled by the section aria-label */}
>
{i18n.categories[id]} {i18n.categories[id]}
</span> </span>
</div> </div>
<ul className="emoji-mart-category-list">
{emojis && {emojis &&
emojis.map((emoji) => emojis.map((emoji) => (
NimbleEmoji({ emoji: emoji, data: this.data, ...emojiProps }), <li key={emoji.id || emoji}>
)} {NimbleEmoji({ emoji: emoji, data: this.data, ...emojiProps })}
</li>
))}
</ul>
{emojis && {emojis &&
!emojis.length && ( !emojis.length && (
@ -206,12 +220,12 @@ export default class Category extends React.Component {
emojiProps={emojiProps} emojiProps={emojiProps}
/> />
)} )}
</div> </section>
) )
} }
} }
Category.propTypes = { Category.propTypes /* remove-proptypes */ = {
emojis: PropTypes.array, emojis: PropTypes.array,
hasStickyPosition: PropTypes.bool, hasStickyPosition: PropTypes.bool,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,

View File

@ -3,7 +3,8 @@ import React from 'react'
import data from '../../../data/all.json' import data from '../../../data/all.json'
import NimbleEmoji from './nimble-emoji' import NimbleEmoji from './nimble-emoji'
import { EmojiPropTypes, EmojiDefaultProps } from '../../utils/shared-props' import { EmojiPropTypes } from '../../utils/shared-props'
import { EmojiDefaultProps } from '../../utils/shared-default-props'
const Emoji = (props) => { const Emoji = (props) => {
for (let k in Emoji.defaultProps) { for (let k in Emoji.defaultProps) {
@ -15,7 +16,7 @@ const Emoji = (props) => {
return NimbleEmoji({ ...props }) return NimbleEmoji({ ...props })
} }
Emoji.propTypes = EmojiPropTypes Emoji.propTypes /* remove-proptypes */ = EmojiPropTypes
Emoji.defaultProps = { ...EmojiDefaultProps, data } Emoji.defaultProps = { ...EmojiDefaultProps, data }
export default Emoji export default Emoji

View File

@ -3,7 +3,8 @@ import PropTypes from 'prop-types'
import { getData, getSanitizedData, unifiedToNative } from '../../utils' import { getData, getSanitizedData, unifiedToNative } from '../../utils'
import { uncompress } from '../../utils/data' import { uncompress } from '../../utils/data'
import { EmojiPropTypes, EmojiDefaultProps } from '../../utils/shared-props' import { EmojiPropTypes } from '../../utils/shared-props'
import { EmojiDefaultProps } from '../../utils/shared-default-props'
const _getData = (props) => { const _getData = (props) => {
var { emoji, skin, set, data } = props var { emoji, skin, set, data } = props
@ -97,6 +98,12 @@ const NimbleEmoji = (props) => {
style = {}, style = {},
children = props.children, children = props.children,
className = 'emoji-mart-emoji', className = 'emoji-mart-emoji',
nativeEmoji = unified && unifiedToNative(unified),
// combine the emoji itself and all shortcodes into an accessible label
label = [nativeEmoji]
.concat(short_names)
.filter(Boolean)
.join(', '),
title = null title = null
if (!unified && !custom) { if (!unified && !custom) {
@ -114,12 +121,13 @@ const NimbleEmoji = (props) => {
if (props.native && unified) { if (props.native && unified) {
className += ' emoji-mart-emoji-native' className += ' emoji-mart-emoji-native'
style = { fontSize: props.size } style = { fontSize: props.size }
children = unifiedToNative(unified) children = nativeEmoji
if (props.forceSize) { if (props.forceSize) {
style.display = 'inline-block' style.display = 'inline-block'
style.width = props.size style.width = props.size
style.height = props.size style.height = props.size
style.wordBreak = 'keep-all'
} }
} else if (custom) { } else if (custom) {
className += ' emoji-mart-emoji-custom' className += ' emoji-mart-emoji-custom'
@ -127,9 +135,22 @@ const NimbleEmoji = (props) => {
width: props.size, width: props.size,
height: props.size, height: props.size,
display: 'inline-block', display: 'inline-block',
}
if (data.spriteUrl) {
style = {
...style,
backgroundImage: `url(${data.spriteUrl})`,
backgroundSize: `${100 * props.sheetColumns}% ${100 *
props.sheetRows}%`,
backgroundPosition: _getPosition(props),
}
} else {
style = {
...style,
backgroundImage: `url(${imageUrl})`, backgroundImage: `url(${imageUrl})`,
backgroundSize: 'contain', backgroundSize: 'contain',
} }
}
} else { } else {
let setHasEmoji = let setHasEmoji =
data[`has_img_${props.set}`] == undefined || data[`has_img_${props.set}`] data[`has_img_${props.set}`] == undefined || data[`has_img_${props.set}`]
@ -149,7 +170,8 @@ const NimbleEmoji = (props) => {
props.set, props.set,
props.sheetSize, props.sheetSize,
)})`, )})`,
backgroundSize: `${100 * props.sheetColumns}% ${100 * props.sheetRows}%`, backgroundSize: `${100 * props.sheetColumns}% ${100 *
props.sheetRows}%`,
backgroundPosition: _getPosition(props), backgroundPosition: _getPosition(props),
} }
} }
@ -157,26 +179,29 @@ const NimbleEmoji = (props) => {
if (props.html) { if (props.html) {
style = _convertStyleToCSS(style) style = _convertStyleToCSS(style)
return `<span style='${style}' ${ return `<button style='${style}' aria-label='${label}' ${
title ? `title='${title}'` : '' title ? `title='${title}'` : ''
} class='${className}'>${children || ''}</span>` } class='${className}'>${children || ''}</button>`
} else { } else {
return ( return (
<span <button
key={props.emoji.id || props.emoji}
onClick={(e) => _handleClick(e, props)} onClick={(e) => _handleClick(e, props)}
onMouseEnter={(e) => _handleOver(e, props)} onMouseEnter={(e) => _handleOver(e, props)}
onMouseLeave={(e) => _handleLeave(e, props)} onMouseLeave={(e) => _handleLeave(e, props)}
aria-label={label}
title={title} title={title}
className={className} className={className}
> >
<span style={style}>{children}</span> <span style={style}>{children}</span>
</span> </button>
) )
} }
} }
NimbleEmoji.propTypes = { ...EmojiPropTypes, data: PropTypes.object.isRequired } NimbleEmoji.propTypes /* remove-proptypes */ = {
...EmojiPropTypes,
data: PropTypes.object.isRequired,
}
NimbleEmoji.defaultProps = EmojiDefaultProps NimbleEmoji.defaultProps = EmojiDefaultProps
export default NimbleEmoji export default NimbleEmoji

View File

@ -26,8 +26,7 @@ export default class NotFound extends React.PureComponent {
} }
} }
NotFound.propTypes = { NotFound.propTypes /* remove-proptypes */ = {
notFound: PropTypes.func.isRequired, notFound: PropTypes.func.isRequired,
notFoundString: PropTypes.string.isRequired,
emojiProps: PropTypes.object.isRequired, emojiProps: PropTypes.object.isRequired,
} }

View File

@ -0,0 +1,29 @@
import React from 'react'
import NimblePicker from '../nimble-picker'
import renderer from 'react-test-renderer'
import data from '../../../../data/apple'
function render(props = {}) {
const defaultProps = { data }
const component = renderer.create(
<NimblePicker {...props} {...defaultProps} />,
)
return component.getInstance()
}
test('shows 10 categories by default', () => {
const subject = render()
expect(subject.categories.length).toEqual(10)
})
test('will not show some categories based upon our filter', () => {
const subject = render({ emojisToShowFilter: () => false })
expect(subject.categories.length).toEqual(2)
})
test('maintains category ids after it is filtered', () => {
const subject = render({ emojisToShowFilter: () => true })
const categoriesWithIds = subject.categories.filter((category) => category.id)
expect(categoriesWithIds.length).toEqual(10)
})

View File

@ -6,17 +6,19 @@ import PropTypes from 'prop-types'
import * as icons from '../../svgs' import * as icons from '../../svgs'
import store from '../../utils/store' import store from '../../utils/store'
import frequently from '../../utils/frequently' import frequently from '../../utils/frequently'
import { deepMerge, measureScrollbar } from '../../utils' import { deepMerge, measureScrollbar, getSanitizedData } from '../../utils'
import { uncompress } from '../../utils/data' import { uncompress } from '../../utils/data'
import { PickerPropTypes, PickerDefaultProps } from '../../utils/shared-props' import { PickerPropTypes } from '../../utils/shared-props'
import Anchors from '../anchors' import Anchors from '../anchors'
import Category from '../category' import Category from '../category'
import Preview from '../preview' import Preview from '../preview'
import Search from '../search' import Search from '../search'
import { PickerDefaultProps } from '../../utils/shared-default-props'
const I18N = { const I18N = {
search: 'Search', search: 'Search',
clear: 'Clear', // Accessible label on "clear" button
notfound: 'No Emoji Found', notfound: 'No Emoji Found',
skintext: 'Choose your default skin tone', skintext: 'Choose your default skin tone',
categories: { categories: {
@ -32,6 +34,15 @@ const I18N = {
flags: 'Flags', flags: 'Flags',
custom: 'Custom', custom: 'Custom',
}, },
categorieslabel: 'Emoji categories', // Accessible title for the list of categories
skintones: {
1: 'Default Skin Tone',
2: 'Light Skin Tone',
3: 'Medium-Light Skin Tone',
4: 'Medium Skin Tone',
5: 'Medium-Dark Skin Tone',
6: 'Dark Skin Tone',
},
} }
export default class NimblePicker extends React.PureComponent { export default class NimblePicker extends React.PureComponent {
@ -396,7 +407,13 @@ export default class NimblePicker extends React.PureComponent {
if ( if (
this.SEARCH_CATEGORY.emojis && this.SEARCH_CATEGORY.emojis &&
(emoji = this.SEARCH_CATEGORY.emojis[0]) this.SEARCH_CATEGORY.emojis.length &&
(emoji = getSanitizedData(
this.SEARCH_CATEGORY.emojis[0],
this.state.skin,
this.props.set,
this.props.data,
))
) { ) {
this.handleEmojiSelect(emoji) this.handleEmojiSelect(emoji)
} }
@ -483,9 +500,10 @@ export default class NimblePicker extends React.PureComponent {
width = perLine * (emojiSize + 12) + 12 + 2 + measureScrollbar() width = perLine * (emojiSize + 12) + 12 + 2 + measureScrollbar()
return ( return (
<div <section
style={{ width: width, ...style }} style={{ width: width, ...style }}
className="emoji-mart" className="emoji-mart"
aria-label={title}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
> >
<div className="emoji-mart-bar"> <div className="emoji-mart-bar">
@ -560,7 +578,7 @@ export default class NimblePicker extends React.PureComponent {
})} })}
</div> </div>
{showPreview && ( {(showPreview || showSkinTones) && (
<div className="emoji-mart-bar"> <div className="emoji-mart-bar">
<Preview <Preview
ref={this.setPreviewRef} ref={this.setPreviewRef}
@ -568,6 +586,7 @@ export default class NimblePicker extends React.PureComponent {
title={title} title={title}
emoji={emoji} emoji={emoji}
showSkinTones={showSkinTones} showSkinTones={showSkinTones}
showPreview={showPreview}
emojiProps={{ emojiProps={{
native: native, native: native,
size: 38, size: 38,
@ -587,12 +606,12 @@ export default class NimblePicker extends React.PureComponent {
/> />
</div> </div>
)} )}
</div> </section>
) )
} }
} }
NimblePicker.propTypes = { NimblePicker.propTypes /* remove-proptypes */ = {
...PickerPropTypes, ...PickerPropTypes,
data: PropTypes.object.isRequired, data: PropTypes.object.isRequired,
} }

View File

@ -3,7 +3,8 @@ import React from 'react'
import data from '../../../data/all.json' import data from '../../../data/all.json'
import NimblePicker from './nimble-picker' import NimblePicker from './nimble-picker'
import { PickerPropTypes, PickerDefaultProps } from '../../utils/shared-props' import { PickerPropTypes } from '../../utils/shared-props'
import { PickerDefaultProps } from '../../utils/shared-default-props'
export default class Picker extends React.PureComponent { export default class Picker extends React.PureComponent {
render() { render() {
@ -11,5 +12,5 @@ export default class Picker extends React.PureComponent {
} }
} }
Picker.propTypes = PickerPropTypes Picker.propTypes /* remove-proptypes */ = PickerPropTypes
Picker.defaultProps = { ...PickerDefaultProps, data } Picker.defaultProps = { ...PickerDefaultProps, data }

View File

@ -23,9 +23,10 @@ export default class Preview extends React.PureComponent {
title, title,
emoji: idleEmoji, emoji: idleEmoji,
i18n, i18n,
showPreview,
} = this.props } = this.props
if (emoji) { if (emoji && showPreview) {
var emojiData = getData(emoji, null, null, this.data), var emojiData = getData(emoji, null, null, this.data),
{ emoticons = [] } = emojiData, { emoticons = [] } = emojiData,
knownEmoticons = [], knownEmoticons = [],
@ -42,7 +43,7 @@ export default class Preview extends React.PureComponent {
return ( return (
<div className="emoji-mart-preview"> <div className="emoji-mart-preview">
<div className="emoji-mart-preview-emoji"> <div className="emoji-mart-preview-emoji" aria-hidden="true">
{NimbleEmoji({ {NimbleEmoji({
key: emoji.id, key: emoji.id,
emoji: emoji, emoji: emoji,
@ -51,7 +52,7 @@ export default class Preview extends React.PureComponent {
})} })}
</div> </div>
<div className="emoji-mart-preview-data"> <div className="emoji-mart-preview-data" aria-hidden="true">
<div className="emoji-mart-preview-name">{emoji.name}</div> <div className="emoji-mart-preview-name">{emoji.name}</div>
<div className="emoji-mart-preview-shortnames"> <div className="emoji-mart-preview-shortnames">
{emojiData.short_names.map((short_name) => ( {emojiData.short_names.map((short_name) => (
@ -73,13 +74,13 @@ export default class Preview extends React.PureComponent {
} else { } else {
return ( return (
<div className="emoji-mart-preview"> <div className="emoji-mart-preview">
<div className="emoji-mart-preview-emoji"> <div className="emoji-mart-preview-emoji" aria-hidden="true">
{idleEmoji && {idleEmoji &&
idleEmoji.length && idleEmoji.length &&
NimbleEmoji({ emoji: idleEmoji, data: this.data, ...emojiProps })} NimbleEmoji({ emoji: idleEmoji, data: this.data, ...emojiProps })}
</div> </div>
<div className="emoji-mart-preview-data"> <div className="emoji-mart-preview-data" aria-hidden="true">
<span className="emoji-mart-title-label">{title}</span> <span className="emoji-mart-title-label">{title}</span>
</div> </div>
@ -113,7 +114,7 @@ export default class Preview extends React.PureComponent {
} }
} }
Preview.propTypes = { Preview.propTypes /* remove-proptypes */ = {
showSkinTones: PropTypes.bool, showSkinTones: PropTypes.bool,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
emoji: PropTypes.string.isRequired, emoji: PropTypes.string.isRequired,

View File

@ -3,6 +3,9 @@ import PropTypes from 'prop-types'
import { search as icons } from '../svgs' import { search as icons } from '../svgs'
import NimbleEmojiIndex from '../utils/emoji-index/nimble-emoji-index' import NimbleEmojiIndex from '../utils/emoji-index/nimble-emoji-index'
import { throttleIdleTask } from '../utils/index'
let id = 0
export default class Search extends React.PureComponent { export default class Search extends React.PureComponent {
constructor(props) { constructor(props) {
@ -10,14 +13,25 @@ export default class Search extends React.PureComponent {
this.state = { this.state = {
icon: icons.search, icon: icons.search,
isSearching: false, isSearching: false,
id: ++id,
} }
this.data = props.data this.data = props.data
this.emojiIndex = new NimbleEmojiIndex(this.data) this.emojiIndex = new NimbleEmojiIndex(this.data)
this.setRef = this.setRef.bind(this) this.setRef = this.setRef.bind(this)
this.handleChange = this.handleChange.bind(this)
this.clear = this.clear.bind(this) this.clear = this.clear.bind(this)
this.handleKeyUp = this.handleKeyUp.bind(this) this.handleKeyUp = this.handleKeyUp.bind(this)
// throttle keyboard input so that typing isn't delayed
this.handleChange = throttleIdleTask(this.handleChange.bind(this))
}
componentDidMount() {
// in some cases (e.g. preact) the input may already be pre-populated
// this.input is undefined in Jest tests
if (this.input && this.input.value) {
this.search(this.input.value)
}
} }
search(value) { search(value) {
@ -46,6 +60,7 @@ export default class Search extends React.PureComponent {
clear() { clear() {
if (this.input.value == '') return if (this.input.value == '') return
this.input.value = '' this.input.value = ''
this.input.focus()
this.search('') this.search('')
} }
@ -64,32 +79,42 @@ export default class Search extends React.PureComponent {
} }
render() { render() {
var { i18n, autoFocus } = this.props const { i18n, autoFocus } = this.props
var { icon, isSearching } = this.state const { icon, isSearching, id } = this.state
const inputId = `emoji-mart-search-${id}`
return ( return (
<div className="emoji-mart-search"> <section className="emoji-mart-search" aria-label={i18n.search}>
<input <input
id={inputId}
ref={this.setRef} ref={this.setRef}
type="text" type="search"
onChange={this.handleChange} onChange={this.handleChange}
placeholder={i18n.search} placeholder={i18n.search}
autoFocus={autoFocus} autoFocus={autoFocus}
/> />
{/*
* Use a <label> in addition to the placeholder for accessibility, but place it off-screen
* http://www.maxability.co.in/2016/01/placeholder-attribute-and-why-it-is-not-accessible/
*/}
<label className="emoji-mart-sr-only" htmlFor={inputId}>
{i18n.search}
</label>
<button <button
className="emoji-mart-search-icon" className="emoji-mart-search-icon"
onClick={this.clear} onClick={this.clear}
onKeyUp={this.handleKeyUp} onKeyUp={this.handleKeyUp}
aria-label={i18n.clear}
disabled={!isSearching} disabled={!isSearching}
> >
{icon()} {icon()}
</button> </button>
</div> </section>
) )
} }
} }
Search.propTypes = { Search.propTypes /* remove-proptypes */ = {
onSearch: PropTypes.func, onSearch: PropTypes.func,
maxResults: PropTypes.number, maxResults: PropTypes.number,
emojisToShowFilter: PropTypes.func, emojisToShowFilter: PropTypes.func,

View File

@ -8,6 +8,14 @@ export default class SkinsDot extends Skins {
super(props) super(props)
this.handleClick = this.handleClick.bind(this) this.handleClick = this.handleClick.bind(this)
this.handleKeyDown = this.handleKeyDown.bind(this)
}
handleKeyDown(event) {
// if either enter or space is pressed, then execute
if (event.keyCode === 13 || event.keyCode === 32) {
this.handleClick(event)
}
} }
render() { render() {
@ -17,13 +25,29 @@ export default class SkinsDot extends Skins {
for (let skinTone = 1; skinTone <= 6; skinTone++) { for (let skinTone = 1; skinTone <= 6; skinTone++) {
const selected = skinTone === skin const selected = skinTone === skin
const visible = opened || selected
skinToneNodes.push( skinToneNodes.push(
<span <span
key={`skin-tone-${skinTone}`} key={`skin-tone-${skinTone}`}
className={`emoji-mart-skin-swatch${selected ? ' selected' : ''}`} className={`emoji-mart-skin-swatch${selected ? ' selected' : ''}`}
aria-label={i18n.skintones[skinTone]}
aria-hidden={!visible}
{...(opened ? { role: 'menuitem' } : {})}
> >
<span <span
onClick={this.handleClick} onClick={this.handleClick}
onKeyDown={this.handleKeyDown}
role="button"
{...(selected
? {
'aria-haspopup': true,
'aria-expanded': !!opened,
}
: {})}
{...(opened ? { 'aria-pressed': !!selected } : {})}
tabIndex={visible ? '0' : ''}
aria-label={i18n.skintones[skinTone]}
title={i18n.skintones[skinTone]}
data-skin={skinTone} data-skin={skinTone}
className={`emoji-mart-skin emoji-mart-skin-tone-${skinTone}`} className={`emoji-mart-skin emoji-mart-skin-tone-${skinTone}`}
/> />
@ -32,14 +56,17 @@ export default class SkinsDot extends Skins {
} }
return ( return (
<div className={`emoji-mart-skin-swatches${opened ? ' opened' : ''}`}> <section
{skinToneNodes} className={`emoji-mart-skin-swatches${opened ? ' opened' : ''}`}
</div> aria-label={i18n.skintext}
>
<div {...(opened ? { role: 'menubar' } : {})}>{skinToneNodes}</div>
</section>
) )
} }
} }
SkinsDot.propTypes = { SkinsDot.propTypes /* remove-proptypes */ = {
onChange: PropTypes.func, onChange: PropTypes.func,
skin: PropTypes.number.isRequired, skin: PropTypes.number.isRequired,
i18n: PropTypes.object, i18n: PropTypes.object,

View File

@ -58,7 +58,7 @@ export default class SkinsEmoji extends Skins {
} }
} }
SkinsEmoji.propTypes = { SkinsEmoji.propTypes /* remove-proptypes */ = {
onChange: PropTypes.func, onChange: PropTypes.func,
skin: PropTypes.number.isRequired, skin: PropTypes.number.isRequired,
emojiProps: PropTypes.object.isRequired, emojiProps: PropTypes.object.isRequired,

View File

@ -30,7 +30,7 @@ export default class Skins extends React.PureComponent {
} }
} }
Skins.propTypes = { Skins.propTypes /* remove-proptypes */ = {
onChange: PropTypes.func, onChange: PropTypes.func,
skin: PropTypes.number.isRequired, skin: PropTypes.number.isRequired,
} }

View File

@ -1,5 +1,7 @@
export { default as emojiIndex } from './utils/emoji-index/emoji-index' export { default as emojiIndex } from './utils/emoji-index/emoji-index'
export { default as NimbleEmojiIndex } from './utils/emoji-index/nimble-emoji-index' export {
default as NimbleEmojiIndex,
} from './utils/emoji-index/nimble-emoji-index'
export { default as store } from './utils/store' export { default as store } from './utils/store'
export { default as frequently } from './utils/frequently' export { default as frequently } from './utils/frequently'
export { getEmojiDataFromNative } from './utils' export { getEmojiDataFromNative } from './utils'

View File

@ -0,0 +1,5 @@
export default function(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}

38
src/polyfills/keys.js Normal file
View File

@ -0,0 +1,38 @@
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
var hasOwnProperty = Object.prototype.hasOwnProperty,
hasDontEnumBug = !{ toString: null }.propertyIsEnumerable('toString'),
dontEnums = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor',
],
dontEnumsLength = dontEnums.length
export default function(obj) {
if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) {
throw new TypeError('Object.keys called on non-object')
}
var result = [],
prop,
i
for (prop in obj) {
if (hasOwnProperty.call(obj, prop)) {
result.push(prop)
}
}
if (hasDontEnumBug) {
for (i = 0; i < dontEnumsLength; i++) {
if (hasOwnProperty.call(obj, dontEnums[i])) {
result.push(dontEnums[i])
}
}
}
return result
}

View File

@ -0,0 +1,40 @@
import emojiIndex from '../emoji-index.js'
test('should work', () => {
expect(emojiIndex.search('pineapple')).toEqual([
{
id: 'pineapple',
name: 'Pineapple',
colons: ':pineapple:',
emoticons: [],
unified: '1f34d',
skin: null,
native: '🍍',
},
])
})
test('should filter only emojis we care about, exclude pineapple', () => {
let emojisToShowFilter = (data) => {
data.unified !== '1F34D'
}
expect(
emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id),
).not.toContain('pineapple')
})
test('can include/exclude categories', () => {
expect(emojiIndex.search('flag', { include: ['people'] })).toEqual([])
})
test('can search for thinking_face', () => {
expect(emojiIndex.search('thinking_fac').map((x) => x.id)).toEqual([
'thinking_face',
])
})
test('can search for woman-facepalming', () => {
expect(emojiIndex.search('woman-facep').map((x) => x.id)).toEqual([
'woman-facepalming',
])
})

View File

@ -224,6 +224,26 @@ function measureScrollbar() {
return scrollbarWidth return scrollbarWidth
} }
// Use requestIdleCallback() if available, else fall back to setTimeout().
// Throttle so as not to run too frequently.
function throttleIdleTask(func) {
const doIdleTask =
typeof requestIdleCallback === 'function' ? requestIdleCallback : setTimeout
let running = false
return function throttled() {
if (running) {
return
}
running = true
doIdleTask(() => {
running = false
func()
})
}
}
export { export {
getData, getData,
getEmojiDataFromNative, getEmojiDataFromNative,
@ -233,4 +253,5 @@ export {
deepMerge, deepMerge,
unifiedToNative, unifiedToNative,
measureScrollbar, measureScrollbar,
throttleIdleTask,
} }

View File

@ -0,0 +1,46 @@
const EmojiDefaultProps = {
skin: 1,
set: 'apple',
sheetSize: 64,
sheetColumns: 52,
sheetRows: 52,
native: false,
forceSize: false,
tooltip: false,
backgroundImageFn: (set, sheetSize) =>
`https://unpkg.com/emoji-datasource-${set}@${EMOJI_DATASOURCE_VERSION}/img/${set}/sheets-256/${sheetSize}.png`,
onOver: () => {},
onLeave: () => {},
onClick: () => {},
}
const PickerDefaultProps = {
onClick: () => {},
onSelect: () => {},
onSkinChange: () => {},
emojiSize: 24,
perLine: 9,
i18n: {},
style: {},
title: 'Emoji Mart™',
emoji: 'department_store',
color: '#ae65c5',
set: EmojiDefaultProps.set,
skin: null,
defaultSkin: EmojiDefaultProps.skin,
native: EmojiDefaultProps.native,
sheetSize: EmojiDefaultProps.sheetSize,
backgroundImageFn: EmojiDefaultProps.backgroundImageFn,
emojisToShowFilter: null,
showPreview: true,
showSkinTones: true,
emojiTooltip: EmojiDefaultProps.tooltip,
autoFocus: false,
custom: [],
skinEmoji: '',
notFound: () => {},
notFoundEmoji: 'sleuth_or_spy',
icons: {},
}
export { PickerDefaultProps, EmojiDefaultProps }

View File

@ -26,22 +26,6 @@ const EmojiPropTypes = {
emoji: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, emoji: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
} }
const EmojiDefaultProps = {
skin: 1,
set: 'apple',
sheetSize: 64,
sheetColumns: 52,
sheetRows: 52,
native: false,
forceSize: false,
tooltip: false,
backgroundImageFn: (set, sheetSize) =>
`https://unpkg.com/emoji-datasource-${set}@${EMOJI_DATASOURCE_VERSION}/img/${set}/sheets-256/${sheetSize}.png`,
onOver: () => {},
onLeave: () => {},
onClick: () => {},
}
const PickerPropTypes = { const PickerPropTypes = {
onClick: PropTypes.func, onClick: PropTypes.func,
onSelect: PropTypes.func, onSelect: PropTypes.func,
@ -72,7 +56,13 @@ const PickerPropTypes = {
short_names: PropTypes.arrayOf(PropTypes.string).isRequired, short_names: PropTypes.arrayOf(PropTypes.string).isRequired,
emoticons: PropTypes.arrayOf(PropTypes.string), emoticons: PropTypes.arrayOf(PropTypes.string),
keywords: PropTypes.arrayOf(PropTypes.string), keywords: PropTypes.arrayOf(PropTypes.string),
imageUrl: PropTypes.string.isRequired, imageUrl: PropTypes.string,
spriteUrl: PropTypes.string,
sheet_x: PropTypes.number,
sheet_y: PropTypes.number,
size: PropTypes.number,
sheetColumns: PropTypes.number,
sheetRows: PropTypes.number,
}), }),
), ),
skinEmoji: PropTypes.string, skinEmoji: PropTypes.string,
@ -81,38 +71,4 @@ const PickerPropTypes = {
icons: PropTypes.object, icons: PropTypes.object,
} }
const PickerDefaultProps = { export { EmojiPropTypes, PickerPropTypes }
onClick: () => {},
onSelect: () => {},
onSkinChange: () => {},
emojiSize: 24,
perLine: 9,
i18n: {},
style: {},
title: 'Emoji Mart™',
emoji: 'department_store',
color: '#ae65c5',
set: EmojiDefaultProps.set,
skin: null,
defaultSkin: EmojiDefaultProps.skin,
native: EmojiDefaultProps.native,
sheetSize: EmojiDefaultProps.sheetSize,
backgroundImageFn: EmojiDefaultProps.backgroundImageFn,
emojisToShowFilter: null,
showPreview: true,
showSkinTones: true,
emojiTooltip: EmojiDefaultProps.tooltip,
autoFocus: false,
custom: [],
skinEmoji: '',
notFound: () => {},
notFoundEmoji: 'sleuth_or_spy',
icons: {},
}
export {
EmojiPropTypes,
EmojiDefaultProps,
PickerPropTypes,
PickerDefaultProps,
}

View File

@ -21,13 +21,13 @@ const CUSTOM_EMOJIS = [
name: 'Octocat', name: 'Octocat',
short_names: ['octocat'], short_names: ['octocat'],
keywords: ['github'], keywords: ['github'],
imageUrl: 'https://assets-cdn.github.com/images/icons/emoji/octocat.png?v7', imageUrl: 'https://github.githubassets.com/images/icons/emoji/octocat.png',
}, },
{ {
name: 'Squirrel', name: 'Squirrel',
short_names: ['shipit', 'squirrel'], short_names: ['shipit', 'squirrel'],
keywords: ['github'], keywords: ['github'],
imageUrl: 'https://assets-cdn.github.com/images/icons/emoji/shipit.png?v7', imageUrl: 'https://github.githubassets.com/images/icons/emoji/shipit.png',
}, },
] ]
@ -56,7 +56,7 @@ storiesOf('Picker', module)
.add('Custom “Not found” component', () => ( .add('Custom “Not found” component', () => (
<Picker <Picker
notFound={() => ( notFound={() => (
<img src="https://assets-cdn.github.com/images/icons/emoji/octocat.png?v7" /> <img src="https://github.githubassets.com/images/icons/emoji/octocat.png" />
)} )}
/> />
)) ))
@ -67,7 +67,7 @@ storiesOf('Picker', module)
icons={{ icons={{
categories: { categories: {
recent: () => ( recent: () => (
<img src="https://assets-cdn.github.com/images/icons/emoji/octocat.png?v7" /> <img src="https://github.githubassets.com/images/icons/emoji/octocat.png" />
), ),
people: () => ( people: () => (
<svg <svg
@ -105,7 +105,7 @@ storiesOf('Picker', module)
</svg> </svg>
), ),
activity: () => ( activity: () => (
<img src="https://assets-cdn.github.com/images/icons/emoji/shipit.png?v7" /> <img src="https://github.githubassets.com/images/icons/emoji/shipit.png" />
), ),
places: () => ( places: () => (
<svg <svg

7215
yarn.lock

File diff suppressed because it is too large Load Diff