UNPKG

9.06 kBJavaScriptView Raw
1/**
2 * Copyright IBM Corp. 2016, 2018
3 *
4 * This source code is licensed under the Apache-2.0 license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import settings from '../../globals/js/settings';
9import eventMatches from '../../globals/js/misc/event-matches';
10import ContentSwitcher from '../content-switcher/content-switcher';
11import on from '../../globals/js/misc/on';
12
13const toArray = arrayLike => Array.prototype.slice.call(arrayLike);
14
15class Tab extends ContentSwitcher {
16 /**
17 * Container of tabs.
18 * @extends ContentSwitcher
19 * @param {HTMLElement} element The element working as a container of tabs.
20 * @param {Object} [options] The component options.
21 * @param {string} [options.selectorMenu] The CSS selector to find the drop down menu used in narrow mode.
22 * @param {string} [options.selectorTrigger] The CSS selector to find the button to open the drop down menu used in narrow mode.
23 * @param {string} [options.selectorTriggerText]
24 * The CSS selector to find the element used in narrow mode showing the selected tab item.
25 * @param {string} [options.selectorButton] The CSS selector to find tab containers.
26 * @param {string} [options.selectorButtonSelected] The CSS selector to find the selected tab.
27 * @param {string} [options.selectorLink] The CSS selector to find the links in tabs.
28 * @param {string} [options.classActive] The CSS class for tab's selected state.
29 * @param {string} [options.classHidden] The CSS class for the drop down menu's hidden state used in narrow mode.
30 * @param {string} [options.eventBeforeSelected]
31 * The name of the custom event fired before a tab is selected.
32 * Cancellation of this event stops selection of tab.
33 * @param {string} [options.eventAfterSelected] The name of the custom event fired after a tab is selected.
34 */
35 constructor(element, options) {
36 super(element, options);
37
38 this.manage(
39 on(this.element, 'keydown', event => {
40 this._handleKeyDown(event);
41 })
42 );
43 this.manage(
44 on(this.element.ownerDocument, 'click', event => {
45 this._handleDocumentClick(event);
46 })
47 );
48
49 const selected = this.element.querySelector(this.options.selectorButtonSelected);
50 if (selected) {
51 this._updateTriggerText(selected);
52 }
53 }
54
55 /**
56 * Internal method of {@linkcode Tab#setActive .setActive()}, to select a tab item.
57 * @private
58 * @param {Object} detail The detail of the event trigging this action.
59 * @param {HTMLElement} detail.item The tab item to be selected.
60 * @param {Function} callback Callback called when change in state completes.
61 */
62 _changeState(detail, callback) {
63 super._changeState(detail, (error, ...data) => {
64 if (!error) {
65 this._updateTriggerText(detail.item);
66 }
67 callback(error, ...data);
68 });
69 }
70
71 /**
72 * Handles click on tab container.
73 * * If the click is on a tab, activates it.
74 * * If the click is on the button to open the drop down menu, does so.
75 * @param {Event} event The event triggering this method.
76 */
77 _handleClick(event) {
78 const button = eventMatches(event, this.options.selectorButton);
79 const trigger = eventMatches(event, this.options.selectorTrigger);
80 if (button && !button.classList.contains(this.options.classButtonDisabled)) {
81 super._handleClick(event);
82 this._updateMenuState(false);
83 }
84 if (trigger) {
85 this._updateMenuState();
86 }
87 }
88
89 /**
90 * Handles click on document.
91 * @param {Event} event The triggering event.
92 * @private
93 */
94 _handleDocumentClick(event) {
95 const { element } = this;
96 const isOfSelf = element.contains(event.target);
97 if (isOfSelf) {
98 return;
99 }
100 this._updateMenuState(false);
101 }
102
103 /**
104 * Handles arrow keys on tab container.
105 * * Left keys are used to go to previous tab.
106 * * Right keys are used to go to next tab.
107 * @param {Event} event The event triggering this method.
108 */
109 _handleKeyDown(event) {
110 const triggerNode = eventMatches(event, this.options.selectorTrigger);
111 if (triggerNode) {
112 if (event.which === 13) {
113 this._updateMenuState();
114 }
115 return;
116 }
117
118 const direction = {
119 37: this.constructor.NAVIGATE.BACKWARD,
120 39: this.constructor.NAVIGATE.FORWARD,
121 }[event.which];
122
123 if (direction) {
124 const buttons = toArray(this.element.querySelectorAll(this.options.selectorButtonEnabled));
125 const button = this.element.querySelector(this.options.selectorButtonSelected);
126 const nextIndex = Math.max(buttons.indexOf(button) + direction, -1 /* For `button` not found in `buttons` */);
127 const nextIndexLooped =
128 nextIndex >= 0 && nextIndex < buttons.length ? nextIndex : nextIndex - Math.sign(nextIndex) * buttons.length;
129 this.setActive(buttons[nextIndexLooped], (error, item) => {
130 if (item) {
131 const link = item.querySelector(this.options.selectorLink);
132 if (link) {
133 link.focus();
134 }
135 }
136 });
137 event.preventDefault();
138 }
139 }
140
141 /**
142 * Shows/hides the drop down menu used in narrow mode.
143 * @param {boolean} [force] `true` to show the menu, `false` to hide the menu, otherwise toggles the menu.
144 */
145 _updateMenuState(force) {
146 const menu = this.element.querySelector(this.options.selectorMenu);
147 const trigger = this.element.querySelector(this.options.selectorTrigger);
148 if (menu) {
149 menu.classList.toggle(this.options.classHidden, typeof force === 'undefined' ? force : !force);
150 if (menu.classList.contains(this.options.classHidden)) {
151 trigger.classList.remove(this.options.classOpen);
152 } else {
153 trigger.classList.add(this.options.classOpen);
154 }
155 }
156 }
157
158 /**
159 * Updates the text indicating the currently selected tab item.
160 * @param {HTMLElement} target The newly selected tab item.
161 */
162 _updateTriggerText(target) {
163 const triggerText = this.element.querySelector(this.options.selectorTriggerText);
164 if (triggerText) {
165 triggerText.textContent = target.textContent;
166 }
167 }
168
169 /**
170 * The map associating DOM element and tab container instance.
171 * @member Tab.components
172 * @type {WeakMap}
173 */
174 static components /* #__PURE_CLASS_PROPERTY__ */ = new WeakMap();
175
176 /**
177 * The component options.
178 * If `options` is specified in the constructor, {@linkcode ContentSwitcher.create .create()}, or {@linkcode Tab.init .init()},
179 * properties in this object are overriden for the instance being create and how {@linkcode Tab.init .init()} works.
180 * @member Tab.options
181 * @type {Object}
182 * @property {string} selectorInit The CSS selector to find tab containers.
183 * @property {string} [selectorMenu] The CSS selector to find the drop down menu used in narrow mode.
184 * @property {string} [selectorTrigger] The CSS selector to find the button to open the drop down menu used in narrow mode.
185 * @property {string} [selectorTriggerText]
186 * The CSS selector to find the element used in narrow mode showing the selected tab item.
187 * @property {string} [selectorButton] The CSS selector to find tab containers.
188 * @property {string} [selectorButtonSelected] The CSS selector to find the selected tab.
189 * @property {string} [selectorLink] The CSS selector to find the links in tabs.
190 * @property {string} [classActive] The CSS class for tab's selected state.
191 * @property {string} [classHidden] The CSS class for the drop down menu's hidden state used in narrow mode.
192 * @property {string} [eventBeforeSelected]
193 * The name of the custom event fired before a tab is selected.
194 * Cancellation of this event stops selection of tab.
195 * @property {string} [eventAfterSelected] The name of the custom event fired after a tab is selected.
196 */
197 static get options() {
198 const { prefix } = settings;
199 return Object.assign(Object.create(ContentSwitcher.options), {
200 selectorInit: '[data-tabs]',
201 selectorMenu: `.${prefix}--tabs__nav`,
202 selectorTrigger: `.${prefix}--tabs-trigger`,
203 selectorTriggerText: `.${prefix}--tabs-trigger-text`,
204 selectorButton: `.${prefix}--tabs__nav-item`,
205 selectorButtonEnabled: `.${prefix}--tabs__nav-item:not(.${prefix}--tabs__nav-item--disabled)`,
206 selectorButtonSelected: `.${prefix}--tabs__nav-item--selected`,
207 selectorLink: `.${prefix}--tabs__nav-link`,
208 classActive: `${prefix}--tabs__nav-item--selected`,
209 classHidden: `${prefix}--tabs__nav--hidden`,
210 classOpen: `${prefix}--tabs-trigger--open`,
211 classButtonDisabled: `${prefix}--tabs__nav-item--disabled`,
212 eventBeforeSelected: 'tab-beingselected',
213 eventAfterSelected: 'tab-selected',
214 });
215 }
216
217 /**
218 * Enum for navigating backward/forward.
219 * @readonly
220 * @member Tab.NAVIGATE
221 * @type {Object}
222 * @property {number} BACKWARD Navigating backward.
223 * @property {number} FORWARD Navigating forward.
224 */
225 static NAVIGATE /* #__PURE_CLASS_PROPERTY__ */ = {
226 BACKWARD: -1,
227 FORWARD: 1,
228 };
229}
230
231export default Tab;