|
27 | 27 | };
|
28 | 28 |
|
29 | 29 | // To avoid invoking the parser with `.innerHTML` for every new instance, a
|
30 |
| - // template for the contents of the ShadowDOM is is shared by all |
| 30 | + // template for the contents of the ShadowDOM is shared by all |
31 | 31 | // `<howto-tabs>` instances.
|
32 |
| - const shadowDOMTemplate = document.createElement('template'); |
33 |
| - shadowDOMTemplate.innerHTML = ` |
| 32 | + const template = document.createElement('template'); |
| 33 | + template.innerHTML = ` |
| 34 | + <style> |
| 35 | + :host { |
| 36 | + display: flex; |
| 37 | + flex-wrap: wrap; |
| 38 | + } |
| 39 | + ::slotted(howto-panel) { |
| 40 | + flex-basis: 100%; |
| 41 | + } |
| 42 | + </style> |
34 | 43 | <slot name="tab"></slot>
|
35 | 44 | <slot name="panel"></slot>
|
36 | 45 | `;
|
|
56 | 65 | // using slots.
|
57 | 66 | this.attachShadow({mode: 'open'});
|
58 | 67 | // Import the shared template to create the slots for tabs and panels.
|
59 |
| - this.shadowRoot.appendChild( |
60 |
| - document.importNode(shadowDOMTemplate.content, true) |
61 |
| - ); |
| 68 | + this.shadowRoot.appendChild(template.content.cloneNode(true)); |
| 69 | + |
62 | 70 | this._tabSlot = this.shadowRoot.querySelector('slot[name=tab]');
|
63 | 71 | this._panelSlot = this.shadowRoot.querySelector('slot[name=panel]');
|
| 72 | + |
| 73 | + // This element needs to react to new children as it links up tabs and |
| 74 | + // panel semantically using `aria-labelledby` and `aria-controls`. |
| 75 | + // New children will get slotted automatically and cause `slotchange` |
| 76 | + // to fire, so not `MutationObserver` is needed. |
64 | 77 | this._tabSlot.addEventListener('slotchange', this._onSlotChange);
|
65 | 78 | this._panelSlot.addEventListener('slotchange', this._onSlotChange);
|
66 | 79 | }
|
|
78 | 91 | if (!this.hasAttribute('role'))
|
79 | 92 | this.setAttribute('role', 'tablist');
|
80 | 93 |
|
81 |
| - // Currently, `slotchange` does not fire when an element is upgraded. For |
82 |
| - // this reason, the element always processes the slots after the inner |
83 |
| - // elements have been defined. If the current behavior of the `slotchange` |
84 |
| - // event is change (as proposed in |
85 |
| - // [this issue](https://github.com/whatwg/dom/issues/447)), the code below |
86 |
| - // can be removed. |
| 94 | + // Up until recently, `slotchange` events did not fire when an element is |
| 95 | + // upgraded by the parser. For this reason, the element invokes the |
| 96 | + // handler manually. Once the new behavior lands in all browsers, the code |
| 97 | + // below can be removed. |
87 | 98 | Promise.all([
|
88 |
| - customElements.whenDefined('howto-tabs-tab'), |
89 |
| - customElements.whenDefined('howto-tabs-panel'), |
| 99 | + customElements.whenDefined('howto-tab'), |
| 100 | + customElements.whenDefined('howto-panel'), |
90 | 101 | ])
|
91 | 102 | .then(_ => this._linkPanels());
|
92 | 103 | }
|
93 | 104 |
|
| 105 | + /** |
| 106 | + * `disconnectedCallback` removes the event listeners that |
| 107 | + * `connectedCallback` added. |
| 108 | + */ |
| 109 | + disconnectedCallback() { |
| 110 | + this.removeEventListener('keydown', this._onKeyDown); |
| 111 | + this.removeEventListener('click', this._onClick); |
| 112 | + } |
| 113 | + |
94 | 114 | /**
|
95 | 115 | * `_onSlotChange` is called whenever an element is added or removed from
|
96 | 116 | * one of the ShadowDOM slots.
|
|
114 | 134 | // that controls it.
|
115 | 135 | tabs.forEach(tab => {
|
116 | 136 | const panel = tab.nextElementSibling;
|
117 |
| - if (panel.tagName.toLowerCase() !== 'howto-tabs-panel') { |
| 137 | + if (panel.tagName.toLowerCase() !== 'howto-panel') { |
118 | 138 | console.error(`Tab #${tab.id} is not a` +
|
119 |
| - `sibling of a <howto-tabs-panel>`); |
| 139 | + `sibling of a <howto-panel>`); |
120 | 140 | return;
|
121 | 141 | }
|
122 | 142 |
|
|
144 | 164 | * cheap to read.
|
145 | 165 | */
|
146 | 166 | _allPanels() {
|
147 |
| - return Array.from(this.querySelectorAll('howto-tabs-panel')); |
| 167 | + return Array.from(this.querySelectorAll('howto-panel')); |
148 | 168 | }
|
149 | 169 |
|
150 | 170 | /**
|
151 | 171 | * `_allTabs` returns all the tabs in the tab panel.
|
152 | 172 | */
|
153 | 173 | _allTabs() {
|
154 |
| - return Array.from(this.querySelectorAll('howto-tabs-tab')); |
| 174 | + return Array.from(this.querySelectorAll('howto-tab')); |
155 | 175 | }
|
156 | 176 |
|
157 | 177 | /**
|
|
215 | 235 | panels.forEach(panel => panel.hidden = true);
|
216 | 236 | }
|
217 | 237 |
|
218 |
| - /** |
219 |
| - * `disconnectedCallback` removes the event listeners that |
220 |
| - * `connectedCallback` added. |
221 |
| - */ |
222 |
| - disconnectedCallback() { |
223 |
| - this.removeEventListener('keydown', this._onKeyDown); |
224 |
| - this.removeEventListener('click', this._onClick); |
225 |
| - } |
226 | 238 |
|
227 | 239 | /**
|
228 | 240 | * `_selectTab` marks the given tab as selected.
|
|
300 | 312 | this._selectTab(event.target);
|
301 | 313 | }
|
302 | 314 | }
|
303 |
| - window.customElements.define('howto-tabs', HowtoTabs); |
| 315 | + customElements.define('howto-tabs', HowtoTabs); |
304 | 316 |
|
305 | 317 | // `howtoTabCounter` counts the number of `<howto-tab>` instances created. The
|
306 | 318 | // number is used to generated new, unique IDs.
|
307 | 319 | let howtoTabCounter = 0;
|
308 | 320 | /**
|
309 |
| - * `HowtoTabsTab` is a tab for a `<howto-tabs>` tab panel. `<howto-tabs-tab>` |
| 321 | + * `HowtoTabsTab` is a tab for a `<howto-tabs>` tab panel. `<howto-tab>` |
310 | 322 | * should always be used with `role=heading` in the markup so that the
|
311 | 323 | * semantics remain useable when JavaScript is failing.
|
312 | 324 | *
|
313 |
| - * A `<howto-tabs-tab>` declares which `<howto-tabs=panel>` it belongs to by |
| 325 | + * A `<howto-tab>` declares which `<howto-panel>` it belongs to by |
314 | 326 | * using that panel’s ID as the value for the `aria-controls` attribute.
|
315 | 327 | *
|
316 |
| - * A `<howto-tabs-tab>` will automatically generate a unique ID if none |
| 328 | + * A `<howto-tab>` will automatically generate a unique ID if none |
317 | 329 | * is specified.
|
318 | 330 | */
|
319 |
| - class HowtoTabsTab extends HTMLElement { |
| 331 | + class HowtoTab extends HTMLElement { |
320 | 332 | static get observedAttributes() {
|
321 | 333 | return ['selected'];
|
322 | 334 | }
|
|
330 | 342 | // changes its role to `tab`.
|
331 | 343 | this.setAttribute('role', 'tab');
|
332 | 344 | if (!this.id)
|
333 |
| - this.id = `howto-tabs-tab-generated-${howtoTabCounter++}`; |
| 345 | + this.id = `howto-tab-generated-${howtoTabCounter++}`; |
334 | 346 |
|
335 | 347 | // Set a well-defined initial state.
|
336 | 348 | this.setAttribute('aria-selected', 'false');
|
|
387 | 399 | return this.hasAttribute('selected');
|
388 | 400 | }
|
389 | 401 | }
|
390 |
| - window.customElements.define('howto-tabs-tab', HowtoTabsTab); |
| 402 | + customElements.define('howto-tab', HowtoTab); |
391 | 403 |
|
392 | 404 | let howtoPanelCounter = 0;
|
393 | 405 | /**
|
394 |
| - * `HowtoTabsPanel` is a panel for a `<howto-tabs>` tab panel. |
| 406 | + * `HowtoPanel` is a panel for a `<howto-tabs>` tab panel. |
395 | 407 | */
|
396 |
| - class HowtoTabsPanel extends HTMLElement { |
| 408 | + class HowtoPanel extends HTMLElement { |
397 | 409 | constructor() {
|
398 | 410 | super();
|
399 | 411 | }
|
400 | 412 |
|
401 | 413 | connectedCallback() {
|
402 | 414 | this.setAttribute('role', 'tabpanel');
|
403 | 415 | if (!this.id)
|
404 |
| - this.id = `howto-tabs-panel-generated-${howtoPanelCounter++}`; |
| 416 | + this.id = `howto-panel-generated-${howtoPanelCounter++}`; |
405 | 417 | }
|
406 | 418 | }
|
407 |
| - window.customElements.define('howto-tabs-panel', HowtoTabsPanel); |
| 419 | + customElements.define('howto-panel', HowtoPanel); |
408 | 420 | })();
|
409 | 421 |
|
410 | 422 |
|
0 commit comments