blob: 2d518caaefa62147f041eccb1ca50ca3b53e1cec [file] [log] [blame]
Copybara botbe50d492023-11-30 00:16:42 +01001'use strict';
2
3var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray');
4
5var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2);
6
7var _constants = require('../utils/constants');
8
9function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
10
11(function () {
12 'use strict';
13
14 var ACCORDION = 'mdlext-accordion';
15 var ACCORDION_VERTICAL = 'mdlext-accordion--vertical';
16 var ACCORDION_HORIZONTAL = 'mdlext-accordion--horizontal';
17 var PANEL = 'mdlext-accordion__panel';
18 var PANEL_ROLE = 'presentation';
19 var TAB = 'mdlext-accordion__tab';
20 var TAB_CAPTION = 'mdlext-accordion__tab__caption';
21 var TAB_ROLE = 'tab';
22 var TABPANEL = 'mdlext-accordion__tabpanel';
23 var TABPANEL_ROLE = 'tabpanel';
24 var RIPPLE_EFFECT = 'mdlext-js-ripple-effect';
25 var RIPPLE = 'mdlext-accordion__tab--ripple';
26 var ANIMATION_EFFECT = 'mdlext-js-animation-effect';
27 var ANIMATION = 'mdlext-accordion__tabpanel--animation';
28
29 /**
30 * @constructor
31 * @param {Element} element The element that will be upgraded.
32 */
33 var MaterialExtAccordion = function MaterialExtAccordion(element) {
34
35 // Stores the Accordion HTML element.
36 this.element_ = element;
37
38 // Initialize instance.
39 this.init();
40 };
41 window['MaterialExtAccordion'] = MaterialExtAccordion;
42
43 // Helpers
44 var accordionPanelElements = function accordionPanelElements(element) {
45 if (!element) {
46 return {
47 panel: null,
48 tab: null,
49 tabpanel: null
50 };
51 } else if (element.classList.contains(PANEL)) {
52 return {
53 panel: element,
54 tab: element.querySelector('.' + TAB),
55 tabpanel: element.querySelector('.' + TABPANEL)
56 };
57 } else {
58 return {
59 panel: element.parentNode,
60 tab: element.parentNode.querySelector('.' + TAB),
61 tabpanel: element.parentNode.querySelector('.' + TABPANEL)
62 };
63 }
64 };
65
66 // Private methods.
67
68 /**
69 * Handles custom command event, 'open', 'close', 'toggle' or upgrade
70 * @param event. A custom event
71 * @private
72 */
73 MaterialExtAccordion.prototype.commandHandler_ = function (event) {
74 event.preventDefault();
75 event.stopPropagation();
76
77 if (event && event.detail) {
78 this.command(event.detail);
79 }
80 };
81
82 /**
83 * Dispatch toggle event
84 * @param {string} state
85 * @param {Element} tab
86 * @param {Element} tabpanel
87 * @private
88 */
89 MaterialExtAccordion.prototype.dispatchToggleEvent_ = function (state, tab, tabpanel) {
90 var ce = new CustomEvent('toggle', {
91 bubbles: true,
92 cancelable: true,
93 detail: { state: state, tab: tab, tabpanel: tabpanel }
94 });
95 this.element_.dispatchEvent(ce);
96 };
97
98 /**
99 * Open tab
100 * @param {Element} panel
101 * @param {Element} tab
102 * @param {Element} tabpanel
103 * @private
104 */
105 MaterialExtAccordion.prototype.openTab_ = function (panel, tab, tabpanel) {
106 panel.classList.add(_constants.IS_EXPANDED);
107 tab.setAttribute(_constants.ARIA_EXPANDED, 'true');
108 tabpanel.removeAttribute('hidden');
109 tabpanel.setAttribute(_constants.ARIA_HIDDEN, 'false');
110 this.dispatchToggleEvent_('open', tab, tabpanel);
111 };
112
113 /**
114 * Close tab
115 * @param {Element} panel
116 * @param {Element} tab
117 * @param {Element} tabpanel
118 * @private
119 */
120 MaterialExtAccordion.prototype.closeTab_ = function (panel, tab, tabpanel) {
121 panel.classList.remove(_constants.IS_EXPANDED);
122 tab.setAttribute(_constants.ARIA_EXPANDED, 'false');
123 tabpanel.setAttribute('hidden', '');
124 tabpanel.setAttribute(_constants.ARIA_HIDDEN, 'true');
125 this.dispatchToggleEvent_('close', tab, tabpanel);
126 };
127
128 /**
129 * Toggle tab
130 * @param {Element} panel
131 * @param {Element} tab
132 * @param {Element} tabpanel
133 * @private
134 */
135 MaterialExtAccordion.prototype.toggleTab_ = function (panel, tab, tabpanel) {
136 if (!(this.element_.hasAttribute('disabled') || tab.hasAttribute('disabled'))) {
137 if (tab.getAttribute(_constants.ARIA_EXPANDED).toLowerCase() === 'true') {
138 this.closeTab_(panel, tab, tabpanel);
139 } else {
140 if (this.element_.getAttribute(_constants.ARIA_MULTISELECTABLE).toLowerCase() !== 'true') {
141 this.closeTabs_();
142 }
143 this.openTab_(panel, tab, tabpanel);
144 }
145 }
146 };
147
148 /**
149 * Open tabs
150 * @private
151 */
152 MaterialExtAccordion.prototype.openTabs_ = function () {
153 var _this = this;
154
155 if (this.element_.getAttribute(_constants.ARIA_MULTISELECTABLE).toLowerCase() === 'true') {
156 [].concat((0, _toConsumableArray3.default)(this.element_.querySelectorAll('.' + ACCORDION + ' > .' + PANEL))).filter(function (panel) {
157 return !panel.classList.contains(_constants.IS_EXPANDED);
158 }).forEach(function (closedItem) {
159 var tab = closedItem.querySelector('.' + TAB);
160 if (!tab.hasAttribute('disabled')) {
161 _this.openTab_(closedItem, tab, closedItem.querySelector('.' + TABPANEL));
162 }
163 });
164 }
165 };
166
167 /**
168 * Close tabs
169 * @private
170 */
171 MaterialExtAccordion.prototype.closeTabs_ = function () {
172 var _this2 = this;
173
174 [].concat((0, _toConsumableArray3.default)(this.element_.querySelectorAll('.' + ACCORDION + ' > .' + PANEL + '.' + _constants.IS_EXPANDED))).forEach(function (panel) {
175 var tab = panel.querySelector('.' + TAB);
176 if (!tab.hasAttribute('disabled')) {
177 _this2.closeTab_(panel, tab, panel.querySelector('.' + TABPANEL));
178 }
179 });
180 };
181
182 // Public methods.
183
184 /**
185 * Upgrade an individual accordion tab
186 * @public
187 * @param {Element} tabElement The HTML element for the accordion panel.
188 */
189 MaterialExtAccordion.prototype.upgradeTab = function (tabElement) {
190 var _this3 = this;
191
192 var _accordionPanelElemen = accordionPanelElements(tabElement),
193 panel = _accordionPanelElemen.panel,
194 tab = _accordionPanelElemen.tab,
195 tabpanel = _accordionPanelElemen.tabpanel;
196
197 var disableTab = function disableTab() {
198 panel.classList.remove(_constants.IS_EXPANDED);
199 tab.setAttribute('tabindex', '-1');
200 tab.setAttribute(_constants.ARIA_EXPANDED, 'false');
201 tabpanel.setAttribute('hidden', '');
202 tabpanel.setAttribute(_constants.ARIA_HIDDEN, 'true');
203 };
204
205 var enableTab = function enableTab() {
206 if (!tab.hasAttribute(_constants.ARIA_EXPANDED)) {
207 tab.setAttribute(_constants.ARIA_EXPANDED, 'false');
208 }
209
210 tab.setAttribute('tabindex', '0');
211
212 if (tab.getAttribute(_constants.ARIA_EXPANDED).toLowerCase() === 'true') {
213 panel.classList.add(_constants.IS_EXPANDED);
214 tabpanel.removeAttribute('hidden');
215 tabpanel.setAttribute(_constants.ARIA_HIDDEN, 'false');
216 } else {
217 panel.classList.remove(_constants.IS_EXPANDED);
218 tabpanel.setAttribute('hidden', '');
219 tabpanel.setAttribute(_constants.ARIA_HIDDEN, 'true');
220 }
221 };
222
223 // In horizontal layout, caption must have a max-width defined to prevent pushing elements to the right of the caption out of view.
224 // In JsDom, offsetWidth and offsetHeight properties do not work, so this function is not testable.
225 /* istanbul ignore next */
226 var calcMaxTabCaptionWidth = function calcMaxTabCaptionWidth() {
227
228 var tabCaption = tab.querySelector('.' + TAB_CAPTION);
229 if (tabCaption !== null) {
230 var w = [].concat((0, _toConsumableArray3.default)(tab.children)).filter(function (el) {
231 return el.classList && !el.classList.contains(TAB_CAPTION);
232 }).reduce(function (v, el) {
233 return v + el.offsetWidth;
234 }, 0);
235
236 var maxWidth = tab.clientHeight - w;
237 if (maxWidth > 0) {
238 tabCaption.style['max-width'] = maxWidth + 'px';
239 }
240 }
241 };
242
243 var selectTab = function selectTab() {
244 if (!tab.hasAttribute(_constants.ARIA_SELECTED)) {
245 [].concat((0, _toConsumableArray3.default)(_this3.element_.querySelectorAll('.' + TAB + '[aria-selected="true"]'))).forEach(function (selectedTab) {
246 return selectedTab.removeAttribute(_constants.ARIA_SELECTED);
247 });
248 tab.setAttribute(_constants.ARIA_SELECTED, 'true');
249 }
250 };
251
252 var tabClickHandler = function tabClickHandler() {
253 _this3.toggleTab_(panel, tab, tabpanel);
254 selectTab();
255 };
256
257 var tabFocusHandler = function tabFocusHandler() {
258 selectTab();
259 };
260
261 var tabpanelClickHandler = function tabpanelClickHandler() {
262 selectTab();
263 };
264
265 var tabpanelFocusHandler = function tabpanelFocusHandler() {
266 selectTab();
267 };
268
269 var tabKeydownHandler = function tabKeydownHandler(e) {
270
271 if (_this3.element_.hasAttribute('disabled')) {
272 return;
273 }
274
275 if (e.keyCode === _constants.VK_END || e.keyCode === _constants.VK_HOME || e.keyCode === _constants.VK_ARROW_UP || e.keyCode === _constants.VK_ARROW_LEFT || e.keyCode === _constants.VK_ARROW_DOWN || e.keyCode === _constants.VK_ARROW_RIGHT) {
276
277 var nextTab = null;
278 var keyCode = e.keyCode;
279
280 if (keyCode === _constants.VK_HOME) {
281 nextTab = _this3.element_.querySelector('.' + PANEL + ':first-child > .' + TAB);
282 if (nextTab && nextTab.hasAttribute('disabled')) {
283 nextTab = null;
284 keyCode = _constants.VK_ARROW_DOWN;
285 }
286 } else if (keyCode === _constants.VK_END) {
287 nextTab = _this3.element_.querySelector('.' + PANEL + ':last-child > .' + TAB);
288 if (nextTab && nextTab.hasAttribute('disabled')) {
289 nextTab = null;
290 keyCode = _constants.VK_ARROW_UP;
291 }
292 }
293
294 if (!nextTab) {
295 var nextPanel = panel;
296
297 do {
298 if (keyCode === _constants.VK_ARROW_UP || keyCode === _constants.VK_ARROW_LEFT) {
299 nextPanel = nextPanel.previousElementSibling;
300 if (!nextPanel) {
301 nextPanel = _this3.element_.querySelector('.' + PANEL + ':last-child');
302 }
303 if (nextPanel) {
304 nextTab = nextPanel.querySelector('.' + PANEL + ' > .' + TAB);
305 }
306 } else if (keyCode === _constants.VK_ARROW_DOWN || keyCode === _constants.VK_ARROW_RIGHT) {
307 nextPanel = nextPanel.nextElementSibling;
308 if (!nextPanel) {
309 nextPanel = _this3.element_.querySelector('.' + PANEL + ':first-child');
310 }
311 if (nextPanel) {
312 nextTab = nextPanel.querySelector('.' + PANEL + ' > .' + TAB);
313 }
314 }
315
316 if (nextTab && nextTab.hasAttribute('disabled')) {
317 nextTab = null;
318 } else {
319 break;
320 }
321 } while (nextPanel !== panel);
322 }
323
324 if (nextTab) {
325 e.preventDefault();
326 e.stopPropagation();
327 nextTab.focus();
328
329 // Workaround for JSDom testing:
330 // In JsDom 'element.focus()' does not trigger any focus event
331 if (!nextTab.hasAttribute(_constants.ARIA_SELECTED)) {
332
333 [].concat((0, _toConsumableArray3.default)(_this3.element_.querySelectorAll('.' + TAB + '[aria-selected="true"]'))).forEach(function (selectedTab) {
334 return selectedTab.removeAttribute(_constants.ARIA_SELECTED);
335 });
336
337 nextTab.setAttribute(_constants.ARIA_SELECTED, 'true');
338 }
339 }
340 } else if (e.keyCode === _constants.VK_ENTER || e.keyCode === _constants.VK_SPACE) {
341 e.preventDefault();
342 e.stopPropagation();
343 _this3.toggleTab_(panel, tab, tabpanel);
344 }
345 };
346
347 if (tab === null) {
348 throw new Error('There must be a tab element for each accordion panel.');
349 }
350
351 if (tabpanel === null) {
352 throw new Error('There must be a tabpanel element for each accordion panel.');
353 }
354
355 panel.setAttribute('role', PANEL_ROLE);
356 tab.setAttribute('role', TAB_ROLE);
357 tabpanel.setAttribute('role', TABPANEL_ROLE);
358
359 if (tab.hasAttribute('disabled')) {
360 disableTab();
361 } else {
362 enableTab();
363 }
364
365 if (this.element_.classList.contains(ACCORDION_HORIZONTAL)) {
366 calcMaxTabCaptionWidth();
367 }
368
369 if (this.element_.classList.contains(RIPPLE_EFFECT)) {
370 tab.classList.add(RIPPLE);
371 }
372
373 if (this.element_.classList.contains(ANIMATION_EFFECT)) {
374 tabpanel.classList.add(ANIMATION);
375 }
376
377 // Remove listeners, just in case ...
378 tab.removeEventListener('click', tabClickHandler);
379 tab.removeEventListener('focus', tabFocusHandler);
380 tab.removeEventListener('keydown', tabKeydownHandler);
381 tabpanel.removeEventListener('click', tabpanelClickHandler);
382 tabpanel.removeEventListener('focus', tabpanelFocusHandler);
383
384 tab.addEventListener('click', tabClickHandler);
385 tab.addEventListener('focus', tabFocusHandler);
386 tab.addEventListener('keydown', tabKeydownHandler);
387 tabpanel.addEventListener('click', tabpanelClickHandler, true);
388 tabpanel.addEventListener('focus', tabpanelFocusHandler, true);
389 };
390 MaterialExtAccordion.prototype['upgradeTab'] = MaterialExtAccordion.prototype.upgradeTab;
391
392 /**
393 * Execute command
394 * @param detail
395 */
396 MaterialExtAccordion.prototype.command = function (detail) {
397 var _this4 = this;
398
399 var openTab = function openTab(tabElement) {
400
401 if (tabElement === undefined) {
402 _this4.openTabs_();
403 } else if (tabElement !== null) {
404 var _accordionPanelElemen2 = accordionPanelElements(tabElement),
405 panel = _accordionPanelElemen2.panel,
406 tab = _accordionPanelElemen2.tab,
407 tabpanel = _accordionPanelElemen2.tabpanel;
408
409 if (tab.getAttribute(_constants.ARIA_EXPANDED).toLowerCase() !== 'true') {
410 _this4.toggleTab_(panel, tab, tabpanel);
411 }
412 }
413 };
414
415 var closeTab = function closeTab(tabElement) {
416 if (tabElement === undefined) {
417 _this4.closeTabs_();
418 } else if (tabElement !== null) {
419 var _accordionPanelElemen3 = accordionPanelElements(tabElement),
420 panel = _accordionPanelElemen3.panel,
421 tab = _accordionPanelElemen3.tab,
422 tabpanel = _accordionPanelElemen3.tabpanel;
423
424 if (tab.getAttribute(_constants.ARIA_EXPANDED).toLowerCase() === 'true') {
425 _this4.toggleTab_(panel, tab, tabpanel);
426 }
427 }
428 };
429
430 var toggleTab = function toggleTab(tabElement) {
431 if (tabElement) {
432 var _accordionPanelElemen4 = accordionPanelElements(tabElement),
433 panel = _accordionPanelElemen4.panel,
434 tab = _accordionPanelElemen4.tab,
435 tabpanel = _accordionPanelElemen4.tabpanel;
436
437 _this4.toggleTab_(panel, tab, tabpanel);
438 }
439 };
440
441 if (detail && detail.action) {
442 var action = detail.action,
443 target = detail.target;
444
445
446 switch (action.toLowerCase()) {
447 case 'open':
448 openTab(target);
449 break;
450 case 'close':
451 closeTab(target);
452 break;
453 case 'toggle':
454 toggleTab(target);
455 break;
456 case 'upgrade':
457 if (target) {
458 this.upgradeTab(target);
459 }
460 break;
461 default:
462 throw new Error('Unknown action "' + action + '". Action must be one of "open", "close", "toggle" or "upgrade"');
463 }
464 }
465 };
466 MaterialExtAccordion.prototype['command'] = MaterialExtAccordion.prototype.command;
467
468 /**
469 * Initialize component
470 */
471 MaterialExtAccordion.prototype.init = function () {
472 var _this5 = this;
473
474 if (this.element_) {
475 // Do the init required for this component to work
476 if (!(this.element_.classList.contains(ACCORDION_HORIZONTAL) || this.element_.classList.contains(ACCORDION_VERTICAL))) {
477 throw new Error('Accordion must have one of the classes "' + ACCORDION_HORIZONTAL + '" or "' + ACCORDION_VERTICAL + '"');
478 }
479
480 this.element_.setAttribute('role', 'tablist');
481
482 if (!this.element_.hasAttribute(_constants.ARIA_MULTISELECTABLE)) {
483 this.element_.setAttribute(_constants.ARIA_MULTISELECTABLE, 'false');
484 }
485
486 this.element_.removeEventListener('command', this.commandHandler_);
487 this.element_.addEventListener('command', this.commandHandler_.bind(this), false);
488
489 [].concat((0, _toConsumableArray3.default)(this.element_.querySelectorAll('.' + ACCORDION + ' > .' + PANEL))).forEach(function (panel) {
490 return _this5.upgradeTab(panel);
491 });
492
493 // Set upgraded flag
494 this.element_.classList.add(_constants.IS_UPGRADED);
495 }
496 };
497
498 /*
499 * Downgrade component
500 * E.g remove listeners and clean up resources
501 *
502 * Nothing to downgrade
503 *
504 MaterialExtAccordion.prototype.mdlDowngrade_ = function() {
505 'use strict';
506 console.log('***** MaterialExtAccordion.mdlDowngrade');
507 };
508 */
509
510 // The component registers itself. It can assume componentHandler is available
511 // in the global scope.
512 /* eslint no-undef: 0 */
513 componentHandler.register({
514 constructor: MaterialExtAccordion,
515 classAsString: 'MaterialExtAccordion',
516 cssClass: 'mdlext-js-accordion',
517 widget: true
518 });
519})(); /**
520 * @license
521 * Copyright 2016 Leif Olsen. All Rights Reserved.
522 *
523 * Licensed under the Apache License, Version 2.0 (the "License");
524 * you may not use this file except in compliance with the License.
525 * You may obtain a copy of the License at
526 *
527 * http://www.apache.org/licenses/LICENSE-2.0
528 *
529 * Unless required by applicable law or agreed to in writing, software
530 * distributed under the License is distributed on an "AS IS" BASIS,
531 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
532 * See the License for the specific language governing permissions and
533 * limitations under the License.
534 *
535 * This code is built with Google Material Design Lite,
536 * which is Licensed under the Apache License, Version 2.0
537 */
538
539/**
540 * A WAI-ARIA friendly accordion component.
541 * An accordion is a collection of expandable panels associated with a common outer container. Panels consist
542 * of a header and an associated content region or tabpanel. The primary use of an Accordion is to present multiple sections
543 * of content on a single page without scrolling, where all of the sections are peers in the application or object hierarchy.
544 * The general look is similar to a tree where each root tree node is an expandable accordion header. The user navigates
545 * and makes the contents of each panel visible (or not) by interacting with the Accordion Header
546 */