From a5e52bcff3e7406c3e4540d05ced20f81c7193f9 Mon Sep 17 00:00:00 2001 From: Jouni Koivuviita Date: Thu, 12 Sep 2019 23:12:16 +0300 Subject: [PATCH 1/7] Update maturity labels --- docs/app-layout.md | 2 +- docs/avatar.md | 2 +- docs/card.md | 2 +- docs/field.md | 2 +- docs/icon.md | 2 +- docs/maturity.md | 10 +++++++--- docs/teleporting-element.md | 2 +- docs/tooltip.md | 2 +- site/src/shared-styles.js | 9 +++++++-- 9 files changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/app-layout.md b/docs/app-layout.md index 72b0341..65f722f 100644 --- a/docs/app-layout.md +++ b/docs/app-layout.md @@ -1,4 +1,4 @@ -# App Layout (Prototype) +# App Layout (Preview) The `` is a responsive application layout that covers multiple common application layout patterns. diff --git a/docs/avatar.md b/docs/avatar.md index e212d00..57f139a 100644 --- a/docs/avatar.md +++ b/docs/avatar.md @@ -1,4 +1,4 @@ -# Avatar (Request for feedback) +# Avatar (Beta) ### Default avatar diff --git a/docs/card.md b/docs/card.md index 01eccbc..b5c13aa 100644 --- a/docs/card.md +++ b/docs/card.md @@ -1,4 +1,4 @@ -# Card (Prototype) +# Card (Preview) ### Simple card ```html,live diff --git a/docs/field.md b/docs/field.md index 9c98093..47e104e 100644 --- a/docs/field.md +++ b/docs/field.md @@ -1,4 +1,4 @@ -# Field (Prototype) +# Field (Preview) `` is a wrapper element that allows you to add a label and an error message to any input element. diff --git a/docs/icon.md b/docs/icon.md index bc96506..1fd3315 100644 --- a/docs/icon.md +++ b/docs/icon.md @@ -1,4 +1,4 @@ -# Icon (Request for feedback) +# Icon (Beta) ## The problem diff --git a/docs/maturity.md b/docs/maturity.md index ae8ed97..24a6c13 100644 --- a/docs/maturity.md +++ b/docs/maturity.md @@ -1,6 +1,6 @@ # Maturity Levels -The components and utilities in this project are labelled with maturity level. The level indicates the varying goals and stages of development. +The components and utilities in this project are labelled with a maturity level. The level indicates the varying goals and stages of development. ### Give feedback @@ -14,14 +14,18 @@ The component/utility is testing a concept, a solution to a specific problem, h The component/utility is not recommended to be used directly in any production app, as it has not been thoroughly tested. -## Prototype Prototype +## Preview Preview The component/utility is in early stage development, and likely to go through major changes. The idea is that it will eventually be suitable for production use, once it has gone through proper testing and feedback rounds. Feedback about the API, performance and user experience is welcomed. -## Request for feedback Request for feedback +## Beta Beta The component/utility seems to solve a specific problem and the solution looks like the right one. Now it’s time to try it out in a real application and provide feedback about any potential bugs or other smaller shortcomings. ## Stable Stable The component/utility has been tested and found to be a good solution and suitable for production use. There are no large issues regarding it’s use. + +## Deprecated Deprecated + +The component/utility did not evolve into a really useful thing, and the development of it has been discontinued. diff --git a/docs/teleporting-element.md b/docs/teleporting-element.md index a8ffc5c..9066377 100644 --- a/docs/teleporting-element.md +++ b/docs/teleporting-element.md @@ -1,4 +1,4 @@ -# Teleporting Element (Proof of concept) +# Teleporting Element (Deprecated) A teleporting element can escape any stacking context, by moving itself directly under the `` element when set “visible”. diff --git a/docs/tooltip.md b/docs/tooltip.md index 0bda37b..e587c02 100644 --- a/docs/tooltip.md +++ b/docs/tooltip.md @@ -1,4 +1,4 @@ -# Tooltip (Proto) +# Tooltip (Preview) A tooltip component based on [Teleporting Element](/teleporting-element), allowing it to escape any stacking contexts. diff --git a/site/src/shared-styles.js b/site/src/shared-styles.js index 50467b4..1f6e128 100644 --- a/site/src/shared-styles.js +++ b/site/src/shared-styles.js @@ -207,12 +207,12 @@ $_documentContainer.innerHTML = ` color: var(--lumo-error-text-color); } - maturity-badge[proto] a { + maturity-badge[preview] a { background-color: rgb(255,241,214); color: rgb(184,121,0); } - maturity-badge[rfc] a { + maturity-badge[beta] a { background-color: var(--lumo-success-color-10pct); color: var(--lumo-success-text-color); } @@ -222,6 +222,11 @@ $_documentContainer.innerHTML = ` color: var(--lumo-primary-text-color); } + maturity-badge[deprecated] a { + background-color: var(--lumo-error-color); + color: var(--lumo-error-contrast-color); + } + code { font-family: "Source Code Pro", monospace; font-size: 0.875em; From 4dbdd691abc72f04506295c4b0fe9bf722348a2f Mon Sep 17 00:00:00 2001 From: Jouni Koivuviita Date: Fri, 13 Sep 2019 12:55:02 +0300 Subject: [PATCH 2/7] Update dependencies Removed the particles effect from the landing page (too resource intensive). --- site/index.html | 2 - site/package-lock.json | 10552 ++++++++++++++++++++++++------------ site/package.json | 24 +- site/polymer.json | 3 +- site/src/about.md | 5 +- site/src/j-site.js | 12 - site/src/shared-styles.js | 8 - 7 files changed, 6995 insertions(+), 3611 deletions(-) diff --git a/site/index.html b/site/index.html index 8a521d3..c989c5d 100644 --- a/site/index.html +++ b/site/index.html @@ -21,8 +21,6 @@ - - + - - + @@ -66,12 +65,12 @@ j-icon[search] { - --svg: var(--j-icon-search); + /* --svg: var(--j-icon-search); */ padding: 8px; } j-icon[bell] { - --svg: var(--j-icon-bell); + /* --svg: var(--j-icon-bell); */ padding: 8px; } diff --git a/test/components.html b/test/components.html new file mode 100644 index 0000000..d9770fd --- /dev/null +++ b/test/components.html @@ -0,0 +1,111 @@ + + + + + j-elements proving ground + + + + + + + + + + + Click me + + + + Icon label + + + + + + + + + + + + + + + + + + + + + Foo + + + + + + + $ + .00 + + + + 🔍 + + + + + Start + End + + + + Start + End + + + + + + + + + + + + + + + Option one + Option two + Option three + + + + + diff --git a/test/custom-select-test.html b/test/custom-select-test.html new file mode 100644 index 0000000..2cc90f5 --- /dev/null +++ b/test/custom-select-test.html @@ -0,0 +1,82 @@ + + + + + custom select test + + + + + + + +
+ +
+

Option 1

+

Secondary text for this item

+
+
+

Option 2

+

Secondary text for this item

+
+
+

Option 3

+

Secondary text for this item

+
+ +
+

Sub-item 1

+

Secondary text for this item

+
+
+

Sub-item 2

+

Secondary text for this item

+
+
+

Sub-item 3

+

Secondary text for this item

+
+
+
+

position: fixed;

+
+ + diff --git a/test/custom-select.js b/test/custom-select.js new file mode 100644 index 0000000..57b55a9 --- /dev/null +++ b/test/custom-select.js @@ -0,0 +1,115 @@ +import { PortalElement } from '../src/util/PortalElement'; + +class CustomSelectPopup extends PortalElement { + constructor() { + super(); + this.attachShadow({mode:'open'}).innerHTML = ` + + + `; + + this.addEventListener('click', e => { + const item = e.target.closest('[item]'); + if (item) { + this.disabled = true; + this.dispatchEvent(new CustomEvent('item-selected', { bubbles: true, detail: item })); + this.parentNode.host.focus(); + } + }); + } +} +window.customElements.define('custom-select-popup', CustomSelectPopup); + +class CustomSelect extends HTMLElement { + constructor() { + super(); + this.setAttribute('tabindex', '0'); + this.attachShadow({mode:'open'}).innerHTML = ` + +
+ Select something +
+ + + + `; + + this.addEventListener('click', e => { + Array.from(this.children).filter(node => node.hasAttribute('item')).forEach(item => { + item.removeAttribute('slot'); + }); + const popup = this.shadowRoot.querySelector('custom-select-popup'); + + if (popup.isTarget) { + popup._source.disabled = true; + return; + } + + const coords = this.getBoundingClientRect(); + popup.style.top = (coords.y + coords.height) + 'px'; + popup.style.left = coords.x + 'px'; + popup.disabled = false; + if (popup.__scopeContainer) { + popup.__scopeContainer.className = this.className; + } + popup.focus(); + popup._target.style.width = popup.offsetWidth + 'px'; + popup._target.style.display = 'block'; + e.stopPropagation(); + }); + + this.shadowRoot.addEventListener('item-selected', e => { + e.detail.setAttribute('slot', 'selected'); + }); + } +} +window.customElements.define('custom-select', CustomSelect); diff --git a/src/stylable-mixin.js b/test/stylable-mixin-with-stylesheet-support.js similarity index 76% rename from src/stylable-mixin.js rename to test/stylable-mixin-with-stylesheet-support.js index d500286..f082fc3 100644 --- a/src/stylable-mixin.js +++ b/test/stylable-mixin-with-stylesheet-support.js @@ -1,10 +1,32 @@ +// Works with + // Needed for ShadyCSS // Module identifier let moduleCounter = 0; // Module class prefix const MODULE_CLASS_PREFIX = '_smod_'; +const themes = {}; + export const StylableMixin = superClass => class JStylableMixin extends superClass { + constructor() { + super(); + + // Do only once per component type (not for every instance) + if (!themes[this.nodeName]) { + // document.querySelectorAll is faster than document.styleSheets in the common case where + // there is only one stylesheet per component. And we get to remove the link element from the + // DOM after processing (probably reduces memory consumption) + Array.prototype.forEach.call(document.querySelectorAll(`link[media="${this.nodeName.toLowerCase()}"]`), link => { + themes[this.nodeName] = document.createElement('style'); + Array.prototype.forEach.call(link.sheet.cssRules, rule => { + themes[this.nodeName].innerHTML += rule.cssText; + }); + link.parentNode.removeChild(link); + }); + } + } + connectedCallback() { if (super.connectedCallback) super.connectedCallback(); if (typeof ShadyCSS != 'undefined' && !ShadyCSS.nativeShadow) { @@ -14,6 +36,12 @@ export const StylableMixin = superClass => class JStylableMixin extends superCla } _gatherStyleModules() { + if (themes[this.nodeName] && !this._themeApplied) { + const theme = themes[this.nodeName].cloneNode(true); + this.shadowRoot.appendChild(theme); + this._themeApplied = true; + } + // Gather style modules (scoped and global) let styleModules = Array.from(this.getRootNode().querySelectorAll(`style[type=scoped]`)); styleModules = styleModules.concat(Array.from(document.querySelectorAll(`style[type=global]`))); @@ -97,13 +125,3 @@ function matches(el, selector) { return el.msMatchesSelector ? el.msMatchesSelector(selector) : el.matches(selector) } - -export function applyStyles(styleStr, component, scope) { - if (scope) { - styleStr = styleStr.replace(' + +
+ +
+ +
+ `; + } + } + customElements.define('x-foo', XFoo); + + class Teleport extends PortalElement {} + customElements.define('tele-port', Teleport); + + + class Drawer extends HTMLElement { + connectedCallback() { + this.attachShadow({mode: 'open'}); + this.shadowRoot.innerHTML = ` + +
+ + + + +
+ `; + this.shadowRoot.querySelector('button').addEventListener('click', e => { + this.toggle(); + }); + } + + toggle() { + this.classList.toggle('open'); + const portal = this.shadowRoot.querySelector('tele-port'); + portal.disabled = !portal.disabled; + } + } + customElements.define('x-drawer', Drawer); + + + + + + + +

Named slot

+

Default slot

+
+ +
+ +

Teleported 2

+
+
+ + + Drawer content + + + + diff --git a/test/themer.html b/test/themer.html new file mode 100644 index 0000000..a9c9164 --- /dev/null +++ b/test/themer.html @@ -0,0 +1,69 @@ + + + + + + + + + + foo1 + + + diff --git a/test/themes/index.html b/test/themes/index.html new file mode 100644 index 0000000..f9e41e6 --- /dev/null +++ b/test/themes/index.html @@ -0,0 +1,119 @@ + + + + + themes test + + + + + + + + + + + + +

Default theme

+ + + + x-button + + j-button + + + + + +
+ x-button + j-button +
+ + + + + diff --git a/test/themes/j-button-theme.js b/test/themes/j-button-theme.js new file mode 100644 index 0000000..64c887a --- /dev/null +++ b/test/themes/j-button-theme.js @@ -0,0 +1,15 @@ +let innerHTML = ` + +`; + +export default innerHTML; diff --git a/test/themes/theme-one.js b/test/themes/theme-one.js new file mode 100644 index 0000000..68a6af7 --- /dev/null +++ b/test/themes/theme-one.js @@ -0,0 +1,23 @@ +// By using "var innerHTML" we allow IDEs (at least Atom) to use correct syntax highlighting and code completion +let innerHTML = ` + +`; + +export default innerHTML; + +export function scopeFor(themeName, tagName) { + const temp = document.createElement('div'); + temp.innerHTML = innerHTML; + temp.firstElementChild.setAttribute('type', 'global'); + temp.firstElementChild.setAttribute('for', `.${themeName} ${tagName}, :host(.${themeName}) ${tagName}`); + document.body.appendChild(temp.firstElementChild); +} From 2ba27ee625de4529481792d9c12c4bfd86f65432 Mon Sep 17 00:00:00 2001 From: Jouni Koivuviita Date: Fri, 13 Sep 2019 13:13:36 +0300 Subject: [PATCH 4/7] Add missing foo-bar test component --- test/components.html | 4 ++-- test/foo-bar.js | 27 +++++++++++++++++++++++++++ test/foobar.css.js | 14 ++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 test/foo-bar.js create mode 100644 test/foobar.css.js diff --git a/test/components.html b/test/components.html index d9770fd..4b2375e 100644 --- a/test/components.html +++ b/test/components.html @@ -8,8 +8,8 @@ diff --git a/test/dev.html b/test/dev.html index bb015cc..01603f5 100644 --- a/test/dev.html +++ b/test/dev.html @@ -2,7 +2,7 @@ - j-elements proving ground + JElements proving ground diff --git a/test/icons.html b/test/icons.html index 334fc83..312251e 100644 --- a/test/icons.html +++ b/test/icons.html @@ -2,7 +2,7 @@ - j-elements j-icon + JElements j-icon diff --git a/test/top.html b/test/top.html index b89f926..e248b89 100644 --- a/test/top.html +++ b/test/top.html @@ -2,7 +2,7 @@ - j-elements app-layout + JElements app-layout From 951bbc9ab21acbcbb28a09e9705963882d821278 Mon Sep 17 00:00:00 2001 From: Jouni Koivuviita Date: Fri, 13 Sep 2019 14:44:04 +0300 Subject: [PATCH 7/7] Add MutationAnimationMixin --- docs/mutation-animation.md | 139 +++++++++++++++++++++++++++++ elements.js | 12 +-- site/src/about.md | 6 +- site/src/j-site.js | 6 ++ src/util/MutationAnimationMixin.js | 62 +++++++++++++ 5 files changed, 218 insertions(+), 7 deletions(-) create mode 100644 docs/mutation-animation.md create mode 100644 src/util/MutationAnimationMixin.js diff --git a/docs/mutation-animation.md b/docs/mutation-animation.md new file mode 100644 index 0000000..6d3c642 --- /dev/null +++ b/docs/mutation-animation.md @@ -0,0 +1,139 @@ +# Mutation Animation Mixin (Proof of concept) + +## Problem + +Animating inserted and, especially, removed elements is tricky. For inserted elements, a simple CSS Animation is enough, but you still want to clear the DOM after the animation has finished, not to pollute it with unnecessary class names or other attributes. + +Animating removed nodes is quite a bit more complex, as you need to keep the element in the DOM for the duration of the animation and only after that can you remove it from the DOM. + +These are doable, but require a lot of additional code when you would just want to call `appendChild`, `insertChild` and `removeChild` to manipulate a simple list of items. + +## Solution + +The `MutationAnimationMixin` helper manages all of the above for you. You only need to define the animation keyframes for the insert and remove states. `MutationObserver` is used to catch inserted and removed elements. + +For removed elements, a clone of the element is briefly re-inserted to the DOM for the duration of the animation. A clone is used to avoid any unexpected `connectedCallback` and `disconnectedCallback` calls for the removed element. + +The explicit width and height are set as the element’s inline styles for the duration of the animation (measured before the animation starts). + +> ###### NOTE +> +> You need to define the remove animation. Otherwise elements can not be removed completely (the cloned element will stay visible in the DOM). + +## Example + +First we create a new custom element using the mixin. + +```html + +``` + +Then we can use the new element and apply animations for inserted and removed elements. + +
+```html,live +Add item +Remove item +Add & remove items + + +
Item 1
+
Item 2
+
Item 3
+
Item 4
+
Item 5
+
+ + + + +``` + + diff --git a/elements.js b/elements.js index f68fa50..9e51c49 100644 --- a/elements.js +++ b/elements.js @@ -11,10 +11,11 @@ import {JPlaceholder} from './src/components/j-placeholder.js'; import {JTooltip} from './src/components/j-tooltip.js'; // Utilities -import StylableMixin from './src/util/StylableMixin.js'; import LightStyleElement from './src/util/LightStyleElement.js'; -import TeleportingElement from './src/util/teleporting-element.js'; +import MutationAnimationMixin from './src/util/MutationAnimationMixin.js'; import PortalElement from './src/util/PortalElement.js'; +import StylableMixin from './src/util/StylableMixin.js'; +import TeleportingElement from './src/util/teleporting-element.js'; export { JAppLayout, @@ -27,8 +28,9 @@ export { JInput, JPlaceholder, JTooltip, - StylableMixin, LightStyleElement, - TeleportingElement, - PortalElement + MutationAnimationMixin, + PortalElement, + StylableMixin, + TeleportingElement }; diff --git a/site/src/about.md b/site/src/about.md index c9d98c4..0e479fb 100644 --- a/site/src/about.md +++ b/site/src/about.md @@ -8,7 +8,7 @@

It also includes a collection or ready-to-use web components which test the solutions first-hand.

-

The package has zero dependencies, so it is lightweight to use in your projects.

+

The package has zero dependencies (~11kB gzipped in total), so it is lightweight to use in your projects.

> Since it’s research project, **JElements is not recommended for production use**. It is mainly looking for feedback from other developers, if the concepts are viable for production. See [Maturity Levels](/maturity) for more info. @@ -23,8 +23,9 @@ The utilities are lower-level mixins and elements which help build components that are easier for other developers to use. - [`LightStyleElement`](/light-style-element) is a way to include built-in styling for a web component without the need to use shadow DOM +- [`MutationAnimationMixin`](/mutation-animation) makes it easy to animate elements in and out of the DOM +- [`PortalElement`](/portal-element) provides a mechanism for escaping any stacking context, making it suitable as a base for reusable overlay components - [`StylableMixin`](/stylable-mixin) allows developers using such components to be styled more freely, by allowing them to inject styles inside the shadow DOM -- [`TeleportingElement`](/teleporting-element) provides a mechanism for escaping any stacking context, making it suitable as a base for reusable overlay components ### Components @@ -35,5 +36,6 @@ The utilities are lower-level mixins and elements which help build components th - [Dialog](/dialog) – testing `TeleportingElement`, if it is a viable option for building dialogs - [Field](/field) – add consistently styled labels and error messages for any input element - [Icon](/icon) – easy to style icon component, giving CSS the power to change the icon shape +- [Input](/input) – a text input component with consistent styling across different browsers, prefix and suffix content support and auto-sizing in both horizontal and vertical directions - [Placeholder](/placeholder) – a simple element for showing a placeholder box (useful for quick prototyping) - [Tooltip](/tooltip) – testing `TeleportingElement`, if its a viable option for building tooltips diff --git a/site/src/j-site.js b/site/src/j-site.js index 2570294..9f6abf9 100644 --- a/site/src/j-site.js +++ b/site/src/j-site.js @@ -7,6 +7,11 @@ import '@vaadin/vaadin-lumo-styles/typography.js'; import './shared-styles.js'; import './maturity-badge.js'; +// Needed for docs Example +import {MutationAnimationMixin} from 'j-elements'; +class MyList extends MutationAnimationMixin(HTMLElement) {} +window.customElements.define('my-list', MyList); + // Pages import './index.js'; @@ -50,6 +55,7 @@ class JSite extends HTMLElement { Tooltip
Utilities
Light Style Element + Mutation Animation Mixin Portal Element Stylable Mixin Teleporting Element diff --git a/src/util/MutationAnimationMixin.js b/src/util/MutationAnimationMixin.js new file mode 100644 index 0000000..c9c0b7c --- /dev/null +++ b/src/util/MutationAnimationMixin.js @@ -0,0 +1,62 @@ +const MutationAnimationMixin = superClass => class JMutationAnimationMixin extends superClass { + _initAnimationMutationObserver() { + this._insertClassName = this._insertClassName || 'j-ma-insert'; + this._removeClassName = this._removeClassName || 'j-ma-remove'; + this._insertAnimationName = this._insertAnimationName || 'j-ma-animation'; + this._removeAnimationName = this._removeAnimationName || 'j-ma-animation'; + + // TODO this only handles first added/removed element in the mutation + this._mutationAnimationObserver = new MutationObserver(e => { + e.forEach(mutation => { + if (mutation.addedNodes.length > 0) { + if (mutation.addedNodes[0].nodeType != 1 || mutation.addedNodes[0].classList.contains(this._removeClassName)) { + return; + } + const inserted = mutation.addedNodes[0]; + inserted.style.width = inserted.offsetWidth + 'px'; + inserted.style.height = inserted.offsetHeight + 'px'; + inserted.classList.add(this._insertClassName); + const insertListener = (e) => { + if (e.animationName == this._insertAnimationName) { + inserted.style.width = ''; + inserted.style.height = ''; + inserted.classList.remove(this._insertClassName); + inserted.removeEventListener('animationend', insertListener); + } + } + inserted.addEventListener('animationend', insertListener); + } + + if (mutation.removedNodes.length > 0) { + if (mutation.removedNodes[0].nodeType != 1 || mutation.removedNodes[0].classList.contains(this._removeClassName)) { + return; + } + const clone = mutation.removedNodes[0].cloneNode(true); + clone.classList.add(this._removeClassName); + clone.classList.remove(this._insertClassName); + mutation.target.insertBefore(clone, mutation.nextSibling); + clone.style.width = clone.offsetWidth + 'px'; + clone.style.height = clone.offsetHeight + 'px'; + clone.addEventListener('animationend', e => { + if (e.animationName == this._removeAnimationName) { + e.target.parentNode.removeChild(clone); + } + }); + } + }); + }); + } + + connectedCallback() { + if (super.connectedCallback) super.connectedCallback(); + if (!this._mutationAnimationObserver) this._initAnimationMutationObserver(); + this._mutationAnimationObserver.observe(this, {childList: true}); + } + + disconnectedCallback() { + if (super.disconnectedCallback) super.disconnectedCallback(); + this._mutationAnimationObserver.disconnect(); + } +} + +export default MutationAnimationMixin;