Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Fancy tabs web component - shadow dom v1, custom elements v1, full a11y
<script>
function execPolyfill() {
(function(){
// CustomElementsV1.min.js v1 polyfill from https://github.com/webcomponents/webcomponentsjs/tree/v1/src/CustomElements/v1.
/*
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
'use strict';(function(){function q(a){return l.test(a)&&-1===r.indexOf(a)}function e(){this.a=new Map;this.l=new Map;this.o=new Map;this.m=new Set;this.D=new MutationObserver(this.F.bind(this));this.f=null;this.L=!0;this.h=!1;this.g(document)}var g=document,f=window,r="annotation-xml color-profile font-face font-face-src font-face-uri font-face-format font-face-name missing-glyph".split(" "),l=/^[a-z][.0-9_a-z]*-[\-.0-9_a-z]*$/;e.prototype={J:function(a,b){function c(a){var b=m[a];if(void 0!==b&&
"function"!==typeof b)throw Error(d+" '"+a+"' is not a Function");return b}a=a.toString().toLowerCase();if("function"!==typeof b)throw new TypeError("constructor must be a Constructor");if(!q(a))throw new SyntaxError("The element name '"+a+"' is not valid.");if(this.a.has(a))throw Error("An element with name '"+a+"' is already defined");if(this.l.has(b))throw Error("Definition failed for '"+a+"': The constructor is already used.");var d=a,m=b.prototype;if("object"!==typeof m)throw new TypeError("Definition failed for '"+
a+"': constructor.prototype must be an object");var e=c("connectedCallback"),f=c("disconnectedCallback"),h=c("attributeChangedCallback");this.a.set(d,{name:a,localName:d,constructor:b,w:e,A:f,v:h,K:b.observedAttributes||[]});this.l.set(b,d);this.b(g.childNodes);if(e=this.o.get(d))e.resolve(void 0),this.o.delete(d)},get:function(a){return(a=this.a.get(a))?a.constructor:void 0},M:function(a){if(!l.test(a))return Promise.reject(new SyntaxError("The element name '"+a+"' is not valid."));if(this.a.has(a))return Promise.resolve();
var b={B:null};b.B=new Promise(function(a){b.resolve=a});this.o.set(a,b);return b.B},C:function(){this.h&&(console.warn("flush!!!"),this.m.forEach(function(a){this.s(a.takeRecords())},this))},H:function(a){this.f=a},g:function(a){a.c=new MutationObserver(this.s.bind(this));a.c.observe(a,{childList:!0,subtree:!0});this.h&&this.m.add(a.c)},I:function(a){a.c&&(a.c.disconnect(),a.c=null,this.h&&this.m.delete(a.c))},s:function(a){for(var b=0;b<a.length;b++){var c=a[b];"childList"===c.type&&(this.b(c.addedNodes),
this.G(c.removedNodes))}},b:function(a){for(var b=0;b<a.length;b++){var c=a[b];if(c.nodeType===Node.ELEMENT_NODE){this.I(c);c=g.createTreeWalker(c,NodeFilter.SHOW_ELEMENT,null,!1);do{var d=c.currentNode,e=this.a.get(d.localName);e&&(d.j||this.u(d,e,!0),d.j&&!d.i&&(d.i=!0,e&&e.w&&e.w.call(d)));d.shadowRoot&&this.b(d.shadowRoot.childNodes);if("LINK"===d.tagName){var f=function(){var a=d;return function(){a.removeEventListener("load",f);this.g(a.import);this.b(a.import.childNodes)}.bind(this)}.call(this);
d.import?f():d.addEventListener("load",f)}}while(c.nextNode())}}},G:function(a){for(var b=0;b<a.length;b++){var c=a[b];if(c.nodeType===Node.ELEMENT_NODE){this.g(c);c=g.createTreeWalker(c,NodeFilter.SHOW_ELEMENT,null,!1);do{var d=c.currentNode;if(d.j&&d.i){d.i=!1;var e=this.a.get(d.localName);e&&e.A&&e.A.call(d)}}while(c.nextNode())}}},u:function(a,b,c){a.__proto__=b.constructor.prototype;c&&(this.H(a),a.j=!0,new b.constructor,console.assert(null==this.f));c=b.K;if(b.v&&0<c.length)for(this.D.observe(a,
{attributes:!0,attributeOldValue:!0,attributeFilter:c}),b=0;b<c.length;b++){var d=c[b];if(a.hasAttribute(d)){var e=a.getAttribute(d);a.v(d,null,e)}}},F:function(a){for(var b=0;b<a.length;b++){var c=a[b];if("attributes"===c.type){var d=c.attributeName,e=c.oldValue,f=c.target,g=f.getAttribute(d);f.attributeChangedCallback(d,e,g,c.attributeNamespace)}}}};window.CustomElementsRegistry=e;e.prototype.define=e.prototype.J;e.prototype.get=e.prototype.get;e.prototype.whenDefined=e.prototype.M;e.prototype.flush=
e.prototype.C;e.prototype.polyfilled=e.prototype.L;e.prototype.enableFlush=e.prototype.h;var h=f.HTMLElement;f.HTMLElement=function(){var a=f.customElements;if(a.f){var b=a.f;a.f=null;return b}if(this.constructor)return a=a.l.get(this.constructor),g.b(a,!1);throw Error("unknown constructor. Did you call customElements.define()?");};f.HTMLElement.prototype=Object.create(h.prototype);Object.defineProperty(f.HTMLElement.prototype,"constructor",{value:f.HTMLElement});for(var h="Button Canvas Data Head Mod TableCell TableCol Anchor Area Base Body BR DataList Details Dialog Div DList Embed FieldSet Form Heading HR Html IFrame Image Input Keygen Label Legend LI Link Map Media Menu MenuItem Meta Meter Object OList OptGroup Option Output Paragraph Param Picture Pre Progress Quote Script Select Slot Source Span Style TableCaption Table TableRow TableSection Template TextArea Time Title Track UList Unknown".split(" "),
k=0;k<h.length;k++){var n=window["HTML"+h[k]+"Element"];n&&(n.prototype.__proto__=f.HTMLElement.prototype)}var t=g.createElement;g.b=function(a,b){var c=f.customElements,d=t.call(g,a),e=c.a.get(a.toLowerCase());e&&c.u(d,e,b);c.g(d);return d};g.createElement=function(a){return g.b(a,!0)};var u=g.createElementNS;g.createElementNS=function(a,b){return"http://www.w3.org/1999/xhtml"===a?g.createElement(b):u.call(document,a,b)};var p=Element.prototype.attachShadow;p&&Object.defineProperty(Element.prototype,
"attachShadow",{value:function(a){a=p.call(this,a);f.customElements.g(a);return a}});window.customElements=new e})();
}).call(this)
}
// Remove check when https://github.com/webcomponents/webcomponentsjs/issues/548 is fixed.
if (!!!window.customElements) {
execPolyfill();
}
</script>
<style>
body {
margin: 0;
}
/* Style the element from the outside */
/*
fancy-tabs {
margin-bottom: 32px;
--background-color: black;
}*/
</style>
<fancy-tabs background>
<button slot="title">Tab 1</button>
<button slot="title" selected>Tab 2</button>
<button slot="title">Tab 3</button>
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>
</fancy-tabs>
<!-- Using <a> instead of h2 still works! -->
<!-- <fancy-tabs background>
<a slot="title">Title 1</a>
<a slot="title" selected>Title 2</a>
<a slot="title">Title 3</a>
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>
</fancy-tabs> -->
<script>
(function() {
'use strict';
// Feature detect
if (!(window.customElements && document.body.attachShadow)) {
document.querySelector('fancy-tabs').innerHTML = "<b>Your browser doesn't support Shadow DOM and Custom Elements v1.</b>";
return;
}
let selected_ = null;
// See https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel
customElements.define('fancy-tabs', class extends HTMLElement {
constructor() {
super(); // always call super() first in the ctor.
// Create shadow DOM for the component.
let shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
width: 650px;
font-family: 'Roboto Slab';
contain: content;
}
:host([background]) {
background: var(--background-color, #9E9E9E);
border-radius: 10px;
padding: 10px;
}
#panels {
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
background: white;
border-radius: 3px;
padding: 16px;
height: 250px;
overflow: auto;
}
#tabs {
display: inline-flex;
-webkit-user-select: none;
user-select: none;
}
#tabs slot {
display: inline-flex; /* Safari bug. Treats <slot> as a parent */
}
/* Safari does not support #id prefixes on ::slotted
See https://bugs.webkit.org/show_bug.cgi?id=160538 */
#tabs ::slotted(*) {
font: 400 16px/22px 'Roboto';
padding: 16px 8px;
margin: 0;
text-align: center;
width: 100px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
cursor: pointer;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
background: linear-gradient(#fafafa, #eee);
border: none; /* if the user users a <button> */
}
#tabs ::slotted([aria-selected="true"]) {
font-weight: 600;
background: white;
box-shadow: none;
}
#tabs ::slotted(:focus) {
z-index: 1; /* make sure focus ring doesn't get buried */
}
#panels ::slotted([aria-hidden="true"]) {
display: none;
}
</style>
<div id="tabs">
<slot id="tabsSlot" name="title"></slot>
</div>
<div id="panels">
<slot id="panelsSlot"></slot>
</div>
`;
}
get selected() {
return selected_;
}
set selected(idx) {
selected_ = idx;
this._selectTab(idx);
// Updated the element's selected attribute value when
// backing property changes.
this.setAttribute('selected', idx);
}
connectedCallback() {
this.setAttribute('role', 'tablist');
const tabsSlot = this.shadowRoot.querySelector('#tabsSlot');
const panelsSlot = this.shadowRoot.querySelector('#panelsSlot');
this.tabs = tabsSlot.assignedNodes({flatten: true});
this.panels = panelsSlot.assignedNodes({flatten: true}).filter(el => {
return el.nodeType === Node.ELEMENT_NODE;
});
// Add aria role="tabpanel" to each content panel.
for (let [i, panel] of this.panels.entries()) {
panel.setAttribute('role', 'tabpanel');
panel.setAttribute('tabindex', 0);
}
// Save refer to we can remove listeners later.
this._boundOnTitleClick = this._onTitleClick.bind(this);
this._boundOnKeyDown = this._onKeyDown.bind(this);
tabsSlot.addEventListener('click', this._boundOnTitleClick);
tabsSlot.addEventListener('keydown', this._boundOnKeyDown);
this.selected = this._findFirstSelectedTab() || 0;
}
disconnectedCallback() {
const tabsSlot = this.shadowRoot.querySelector('#tabsSlot');
tabsSlot.removeEventListener('click', this._boundOnTitleClick);
tabsSlot.removeEventListener('keydown', this._boundOnKeyDown);
}
_onTitleClick(e) {
if (e.target.slot === 'title') {
this.selected = this.tabs.indexOf(e.target);
e.target.focus();
}
}
_onKeyDown(e) {
switch (e.code) {
case 'ArrowUp':
case 'ArrowLeft':
e.preventDefault();
var idx = this.selected - 1;
idx = idx < 0 ? this.tabs.length - 1 : idx;
this.tabs[idx].click();
break;
case 'ArrowDown':
case 'ArrowRight':
e.preventDefault();
var idx = this.selected + 1;
this.tabs[idx % this.tabs.length].click();
break;
default:
break;
}
}
_findFirstSelectedTab() {
let selectedIdx;
for (let [i, tab] of this.tabs.entries()) {
tab.setAttribute('role', 'tab');
// Allow users to declaratively select a tab
// Highlight last tab which has the selected attribute.
if (tab.hasAttribute('selected')) {
selectedIdx = i;
}
}
return selectedIdx;
}
_selectTab(idx = null) {
for (let i = 0, tab; tab = this.tabs[i]; ++i) {
let select = i === idx;
tab.setAttribute('tabindex', select ? 0 : -1);
tab.setAttribute('aria-selected', select);
this.panels[i].setAttribute('aria-hidden', !select);
}
}
});
})();
</script>
@derekshull

This comment has been minimized.

Copy link

commented Oct 2, 2016

Is the polyfill at the top for ShadowDOM v1 and Custom Elements v1?

@mkozhukharenko

This comment has been minimized.

Copy link

commented Oct 4, 2016

yep

@ephemere000

This comment has been minimized.

Copy link

commented Oct 21, 2016

I could not make my custom element container work until I found your example. Chromium bug report: Issue 658119 (code provided).

In my case both the container and the children are custom elements. My container would only work if the children were native elements. When I nested custom elements, the container would not see it's children in connectedCallback (not created yet).

I put my custom children in your container and it worked! I finally realized that you instantiate the container and children BEFORE you define them (upgrade). I had done the reverse and included the definitions at the beginning of the page, like we always do for definitions: styles, scripts,.. It was working until I encountered nested custom elements.

First, I find odd that the custom elements children creation sequence is different from a parent node's perspective when compared to native elements. The children are already created in connectedCallback for native elements, not for custom ones. Second, to make my code work, I have to put the definitions' inclusion at the END of the HTML file!

This is a schizophrenic behavior from a coders' perspective, two different creation sequence schemes and sets of rules. It will become a nightmare very quickly and is not maintainable.

Is my understanding right (I deeply hope not), or I'm missing something fundamental here?

Thanks

@ephemere000

This comment has been minimized.

Copy link

commented Oct 25, 2016

I got a reply from dominicc from the Chromium group: "I'm closing this as WontFix because I think this is working as intended and per the spec, even though the behavior is surprising."

Meaning: unintuitive, not clear, not working like native elements and HTML do in general, but what the heck, it is conform to the specs if we look deep into it, so we won't fix and live with it!

The problem was already forseen in the specs design but was dismissed. "Some contributors to the spec considered a callback for "end tag parsed" which would be a good time to inspect children if you were doing "one shot" processing of children, however IIRC the decision was to not add this callback yet."

Acutally, the proposed way is as follows: "If you want a custom element's behavior to depend on its children you should use a MutationObserver or have the children coordinate with the container through events/methods/etc."

The second solution is obviously not viable. The children would have to coordonate with their parents though KNOWN events/methods, meaning that the coder that creates the container MUST ALSO create the children so they can register to or call their container using a KNOWN PUBLISHED interface. Anyone creating a container like your Fancy Tab would also have to create all of the custom element children that it may potentially encounter. If not, even worse, each creator in the community will come up with their own event/methods that all the other custom elements in the world would have to know about and call. Impossible, a no brainer.

Adieu! the ability to create custom containers and elements that can be shared in the community and that are agostic of each other, that behave like native elements. This solution reminds us that a clear interface should be defined between parent and child nodes, a standardized one. I wonder how they made it work for native ones under the hood. We don't have this option with custom elements coming from different sources and libraries.

The second solution, MutationObserver, would be good for a dynamic container where chidren might come and go. A bit overkilled for stable one shot contents containers (for intance GUI). Certainly not necessary if the children are native elements. They use a different logic created by the vendor. OUCH!

One of the 4 conerstones of Webcomponents is the Import mechanism. Some are now looking at Modules instead. All the same. A way to include FIRST the custom elements definitions and then use them in the page. That Import WILL be placed at the beginning of the document, not at the end like the definitions in your example.

I appreciate your example, it made me realize it is pretty naïve (with all due respects, no insult intended and I did it like you intitially), but not usable as a standard example of how to implement an actual custom container. Yours works because of a "hack", an upgrade of the custom elements by including the definitions at the end of the document when everything is already created, which is not the way (and was never the way) the coders intended to include their definitions, through imports or modules at the BEGINING of the document.

Since compostion is a major part of WebComponents and HTML, actually, the whole DOM is about nodes withing nodes contained and layed out within other nodes, the current specs are incomplete and unusable. By importing custom elements at the begining of the document, only native element children are accessible to their parents through some magic implemented by the vendor, creating the children before the parent's callback is invoked. Custom children don't follow this rule. Sad. :(

I doubt anything will be done about it since the Chronium group dismissed it and said "the behavior is surprising" but well, conforms to the [inadequate] spec. So, live with it!

Hopefully, another group will adress this issue. I understand now why Firefox and other vendors are hesitating to implement any of this at the moment. They are waiting for this kind of feedback that they claim they won't dismiss.

Respectfully

@hejty

This comment has been minimized.

Copy link

commented Oct 31, 2016

Hey Eric.

It seems ::slotted + ::before pseudo selectors doesn't play nicely together. CSS ignores whole definition #tabs ::slotted(*)::before { }
https://jsbin.com/gohezulama/4/edit?html,js,output

Is it a bug or did I do something wrong?

Thank you.

@amirulislam862

This comment has been minimized.

Copy link

commented Mar 14, 2018

amirulislam862

@noncototient

This comment has been minimized.

Copy link

commented Sep 7, 2018

https://github.com/webcomponents/webcomponentsjs/issues/548

This appears to be fixed now, so the check on line 29 can be removed

@akanshgulati

This comment has been minimized.

Copy link

commented May 23, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.