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 |
|
8 | import settings from '../../globals/js/settings';
|
9 | import eventMatches from '../../globals/js/misc/event-matches';
|
10 | import ContentSwitcher from '../content-switcher/content-switcher';
|
11 | import on from '../../globals/js/misc/on';
|
12 |
|
13 | const toArray = arrayLike => Array.prototype.slice.call(arrayLike);
|
14 |
|
15 | class 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 |
|
231 | export default Tab;
|