From d63c1f8c0b0af93bc309c392be91a9d45d4a240b Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Sun, 11 Jul 2021 10:01:21 -0700 Subject: [PATCH] fix: use our own custom element implementation (#170) Fixes #176 --- bin/buildStyles.js | 8 + config/jest.setup.js | 5 +- jest.config.cjs | 2 - package.json | 7 +- rollup.config.js | 25 +- src/picker/PickerElement.js | 117 ++++- src/picker/components/Picker/Picker.html | 13 +- src/picker/components/Picker/Picker.js | 72 +-- src/picker/components/Picker/Picker.svelte | 1 - .../Picker/Picker.scss => styles/picker.scss} | 2 +- src/picker/utils/runAll.js | 1 - src/picker/utils/widthCalculator.js | 10 +- test/spec/picker/attributes.test.js | 9 +- test/spec/picker/constructor.test.js | 3 +- test/spec/picker/element.test.js | 21 +- test/spec/picker/errors.test.js | 12 +- test/spec/picker/lifecycle.test.js | 21 +- test/spec/picker/properties.test.js | 73 +++ test/spec/shared.js | 7 +- yarn.lock | 449 +----------------- 20 files changed, 275 insertions(+), 583 deletions(-) create mode 100644 bin/buildStyles.js rename src/picker/{components/Picker/Picker.scss => styles/picker.scss} (99%) delete mode 100644 src/picker/utils/runAll.js create mode 100644 test/spec/picker/properties.test.js diff --git a/bin/buildStyles.js b/bin/buildStyles.js new file mode 100644 index 00000000..f3c41de0 --- /dev/null +++ b/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 +} diff --git a/config/jest.setup.js b/config/jest.setup.js index 68c67d81..44be39ff 100644 --- a/config/jest.setup.js +++ b/config/jest.setup.js @@ -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 () => { diff --git a/jest.config.cjs b/jest.config.cjs index 424baa91..9d9399ef 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -8,8 +8,6 @@ module.exports = { '^.+\\.svelte$': ['svelte-jester', { preprocess: true, compilerOptions: { - css: true, - customElement: true, dev: false } }] diff --git a/package.json b/package.json index 2f57f477..673bf5a6 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", @@ -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", @@ -155,6 +155,7 @@ "indexedDB", "IDBKeyRange", "Headers", + "HTMLElement", "matchMedia", "performance", "ResizeObserver", @@ -196,7 +197,7 @@ "bundlesize": [ { "path": "./bundle.js", - "maxSize": "41.4 kB", + "maxSize": "41 kB", "compression": "none" }, { diff --git a/rollup.config.js b/rollup.config.js index d56b673b..9fc43849 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -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 @@ -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, @@ -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: [ diff --git a/src/picker/PickerElement.js b/src/picker/PickerElement.js index 43aec837..e8143949 100644 --- a/src/picker/PickerElement.js +++ b/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) diff --git a/src/picker/components/Picker/Picker.html b/src/picker/components/Picker/Picker.html index ebee2e5c..2b67d88c 100644 --- a/src/picker/components/Picker/Picker.html +++ b/src/picker/components/Picker/Picker.html @@ -1,5 +1,4 @@ - -
+ use:calculateIndicatorWidth>
@@ -117,7 +116,7 @@ on:click={onEmojiClick} bind:this={tabpanelElement} > -
+
{#each currentEmojisWithCategories as emojiWithCategory, i (emojiWithCategory.category)}