Skip to content

Commit

Permalink
fix: use our own custom element implementation (#170)
Browse files Browse the repository at this point in the history
Fixes #176
  • Loading branch information
nolanlawson committed Jul 11, 2021
1 parent abc057e commit d63c1f8
Show file tree
Hide file tree
Showing 20 changed files with 275 additions and 583 deletions.
8 changes: 8 additions & 0 deletions bin/buildStyles.js
@@ -0,0 +1,8 @@
import sass from 'sass'
import csso from 'csso'

export function buildStyles () {
const file = './src/picker/styles/picker.scss'
const css = sass.renderSync({ file, outputStyle: 'compressed' }).css.toString('utf8')
return csso.minify(css).css
}
5 changes: 4 additions & 1 deletion config/jest.setup.js
Expand Up @@ -28,10 +28,13 @@ process.env.NODE_ENV = 'test'
global.IDBKeyRange = FDBKeyRange
global.indexedDB = new FDBFactory()

// TODO: figure out how to get the styles into Jest. For now it doesn't really
// matter because none of the tests rely on visibility checks etc.
jest.mock('emoji-picker-element-styles', () => '', { virtual: true })

beforeAll(() => {
jest.spyOn(global.console, 'log').mockImplementation()
jest.spyOn(global.console, 'warn').mockImplementation()
jest.spyOn(global.console, 'error').mockImplementation()
})

afterEach(async () => {
Expand Down
2 changes: 0 additions & 2 deletions jest.config.cjs
Expand Up @@ -8,8 +8,6 @@ module.exports = {
'^.+\\.svelte$': ['svelte-jester', {
preprocess: true,
compilerOptions: {
css: true,
customElement: true,
dev: false
}
}]
Expand Down
7 changes: 4 additions & 3 deletions package.json
Expand Up @@ -76,6 +76,7 @@
"@rollup/plugin-node-resolve": "^10.0.0",
"@rollup/plugin-replace": "^2.3.4",
"@rollup/plugin-strip": "^2.0.1",
"@rollup/plugin-virtual": "^2.0.3",
"@testing-library/dom": "^8.0.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/user-event": "^13.1.9",
Expand All @@ -85,7 +86,7 @@
"compression": "^1.7.4",
"conventional-changelog-cli": "^2.1.1",
"cross-env": "^7.0.3",
"cssnano": "^5.0.6",
"csso": "^4.2.0",
"d2l-resize-aware": "BrightspaceUI/resize-aware#semver:^1.2.2",
"emoji-picker-element-data": "^1.1.0",
"emojibase-data": "^5.1.1",
Expand All @@ -105,7 +106,6 @@
"node-fetch": "^2.6.1",
"npm-run-all": "^4.1.5",
"playwright": "^1.12.3",
"postcss": "^8.2.1",
"pretty-bytes": "^5.4.1",
"puppeteer": "^10.0.0",
"recursive-readdir": "^2.2.2",
Expand Down Expand Up @@ -155,6 +155,7 @@
"indexedDB",
"IDBKeyRange",
"Headers",
"HTMLElement",
"matchMedia",
"performance",
"ResizeObserver",
Expand Down Expand Up @@ -196,7 +197,7 @@
"bundlesize": [
{
"path": "./bundle.js",
"maxSize": "41.4 kB",
"maxSize": "41 kB",
"compression": "none"
},
{
Expand Down
25 changes: 6 additions & 19 deletions rollup.config.js
Expand Up @@ -5,21 +5,13 @@ import strip from '@rollup/plugin-strip'
import svelte from 'rollup-plugin-svelte'
import preprocess from 'svelte-preprocess'
import analyze from 'rollup-plugin-analyzer'
import cssnano from 'cssnano'
import { buildStyles } from './bin/buildStyles'
import virtual from '@rollup/plugin-virtual'

const { NODE_ENV, DEBUG } = process.env
const dev = NODE_ENV !== 'production'

const preprocessConfig = preprocess({
scss: true,
postcss: {
plugins: [
cssnano({
preset: 'default'
})
]
}
})
const preprocessConfig = preprocess()

const origMarkup = preprocessConfig.markup
// minify the HTML by removing extra whitespace
Expand All @@ -42,6 +34,9 @@ const baseConfig = {
plugins: [
resolve(),
cjs(),
virtual({
'emoji-picker-element-styles': `export default ${JSON.stringify(buildStyles())}`
}),
replace({
'process.env.NODE_ENV': dev ? '"development"' : '"production"',
'process.env.PERF': !!process.env.PERF,
Expand All @@ -54,18 +49,10 @@ const baseConfig = {
}),
svelte({
compilerOptions: {
customElement: true,
dev
},
preprocess: preprocessConfig
}),
replace({
preventAssignment: true,
delimiters: ['', ''],
// Reduce bundle size by removing this bit
// https://github.com/sveltejs/svelte/blob/5d82496/src/runtime/internal/Component.ts#L64-L78
'(!customElement)': '(false)'
}),
strip({
include: ['**/*.js', '**/*.svelte'],
functions: [
Expand Down
117 changes: 93 additions & 24 deletions src/picker/PickerElement.js
@@ -1,51 +1,120 @@
import SveltePicker from './components/Picker/Picker.svelte'
import { DEFAULT_DATA_SOURCE, DEFAULT_LOCALE } from '../database/constants'
import { runAll } from './utils/runAll'
import { DEFAULT_CATEGORY_SORTING, DEFAULT_SKIN_TONE_EMOJI } from './constants'
import enI18n from '../picker/i18n/en.js'
import styles from 'emoji-picker-element-styles'
import Database from './ImportedDatabase'

export default class PickerElement extends SveltePicker {
export default class PickerElement extends HTMLElement {
constructor (props) {
performance.mark('initialLoad')
// Make the API simpler, directly pass in the props
super({
props: {
// Set defaults
locale: DEFAULT_LOCALE,
dataSource: DEFAULT_DATA_SOURCE,
...props
}
super()
this.attachShadow({ mode: 'open' })
const style = document.createElement('style')
style.textContent = styles
this.shadowRoot.appendChild(style)
this._ctx = {
// Set defaults
locale: DEFAULT_LOCALE,
dataSource: DEFAULT_DATA_SOURCE,
skinToneEmoji: DEFAULT_SKIN_TONE_EMOJI,
customCategorySorting: DEFAULT_CATEGORY_SORTING,
customEmoji: null,
i18n: enI18n,
...props
}
this._dbFlush() // wait for a flush before creating the db, in case the user calls e.g. a setter or setAttribute
}

connectedCallback () {
this._cmp = new SveltePicker({
target: this.shadowRoot,
props: this._ctx
})
}

disconnectedCallback () {
// For Svelte v <3.33.0, we have to run the destroy logic ourselves because it doesn't have this fix:
// https://github.com/sveltejs/svelte/commit/d4f98f
// We can safely just run on_disconnect and on_destroy to cover all versions of Svelte. In older versions
// the on_destroy array will have length 1, whereas in more recent versions it'll be on_disconnect instead.
// TODO: remove this when we drop support for Svelte < 3.33.0
runAll(this.$$.on_destroy)
runAll(this.$$.on_disconnect)
this._cmp.$destroy()
this._cmp = undefined

const { database } = this._ctx
if (database) {
database.close()
// only happens if the database failed to load in the first place, so we don't care)
.catch(err => console.error(err))
}
}

static get observedAttributes () {
return ['locale', 'data-source', 'skin-tone-emoji'] // complex objects aren't supported, also use kebab-case
}

// via https://github.com/sveltejs/svelte/issues/3852#issuecomment-665037015
attributeChangedCallback (attrName, oldValue, newValue) {
super.attributeChangedCallback(
// convert from kebab-case to camelcase
// see https://github.com/sveltejs/svelte/issues/3852#issuecomment-665037015
this._set(
attrName.replace(/-([a-z])/g, (_, up) => up.toUpperCase()),
oldValue,
newValue
)
}

get database () {
return super.database
_set (prop, newValue) {
this._ctx[prop] = newValue
if (this._cmp) {
this._cmp.$set({ [prop]: newValue })
}
if (['locale', 'dataSource'].includes(prop)) {
this._dbFlush()
}
}

set database (val) {
throw new Error('database is read-only')
_dbCreate () {
const { locale, dataSource, database } = this._ctx
// only create a new database if we really need to
if (!database || database.locale !== locale || database.dataSource !== dataSource) {
this._set('database', new Database({ locale, dataSource }))
}
}

// Update the Database in one microtask if the locale/dataSource change. We do one microtask
// so we don't create two Databases if e.g. both the locale and the dataSource change
_dbFlush () {
Promise.resolve().then(() => (
this._dbCreate()
))
}
}

const props = [
'customEmoji',
'customCategorySorting',
'database',
'dataSource',
'i18n',
'locale',
'skinToneEmoji'
]
const definitions = {}

for (const prop of props) {
definitions[prop] = {
get () {
if (prop === 'database') {
// in rare cases, the microtask may not be flushed yet, so we need to instantiate the DB
// now if the user is asking for it
this._dbCreate()
}
return this._ctx[prop]
},
set (val) {
if (prop === 'database') {
throw new Error('database is read-only')
}
this._set(prop, val)
}
}
}

Object.defineProperties(PickerElement.prototype, definitions)

customElements.define('emoji-picker', PickerElement)
13 changes: 5 additions & 8 deletions src/picker/components/Picker/Picker.html
@@ -1,5 +1,4 @@
<svelte:options tag={null} />
<section
<svelte:options tag={null} /><section
class="picker"
aria-label={i18n.regionLabel}
style={pickerStyle}
Expand Down Expand Up @@ -98,7 +97,7 @@
<div class="indicator-wrapper">
<div class="indicator"
style={indicatorStyle}
bind:this={indicatorElement}>
use:calculateIndicatorWidth>
</div>
</div>

Expand All @@ -117,7 +116,7 @@
on:click={onEmojiClick}
bind:this={tabpanelElement}
>
<div bind:this={tabpanelInnerElement}>
<div use:calculateEmojiGridWidth>
{#each currentEmojisWithCategories as emojiWithCategory, i (emojiWithCategory.category)}
<div
id="menu-label-{i}"
Expand Down Expand Up @@ -167,10 +166,8 @@
style="padding-right: {scrollbarWidth}px;"
on:click={onEmojiClick}
data-testid="favorites">
<!-- TODO: the reason so much of this emoji logic is duplicated is because it would be wasteful
or incorrect to extract this logic into a custom Svelte component, because in customElements
mode, Svelte will render _all_ components as custom elements. Ideally we just want one
custom element wrapper around the outer component -->
<!-- The reason the emoji logic below is largely duplicated is because it turns out we get a smaller
bundle size from just repeating it twice, rather than creating a second Svelte component. -->
{#each currentFavorites as emoji, i (emoji.id)}
<button role="menuitem"
aria-label={labelWithSkin(emoji, currentSkinTone)}
Expand Down

0 comments on commit d63c1f8

Please sign in to comment.