UNPKG

16.7 kBJavaScriptView Raw
1function _typeof(obj) {
2 if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
3 _typeof = function _typeof(obj) {
4 return typeof obj;
5 };
6 } else {
7 _typeof = function _typeof(obj) {
8 return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
9 };
10 }
11
12 return _typeof(obj);
13}
14
15function _toConsumableArray(arr) {
16 return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
17}
18
19function _nonIterableSpread() {
20 throw new TypeError("Invalid attempt to spread non-iterable instance");
21}
22
23function _iterableToArray(iter) {
24 if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
25}
26
27function _arrayWithoutHoles(arr) {
28 if (Array.isArray(arr)) {
29 for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) {
30 arr2[i] = arr[i];
31 }
32
33 return arr2;
34 }
35}
36
37function _classCallCheck(instance, Constructor) {
38 if (!(instance instanceof Constructor)) {
39 throw new TypeError("Cannot call a class as a function");
40 }
41}
42
43function _defineProperties(target, props) {
44 for (var i = 0; i < props.length; i++) {
45 var descriptor = props[i];
46 descriptor.enumerable = descriptor.enumerable || false;
47 descriptor.configurable = true;
48 if ("value" in descriptor) descriptor.writable = true;
49 Object.defineProperty(target, descriptor.key, descriptor);
50 }
51}
52
53function _createClass(Constructor, protoProps, staticProps) {
54 if (protoProps) _defineProperties(Constructor.prototype, protoProps);
55 if (staticProps) _defineProperties(Constructor, staticProps);
56 return Constructor;
57}
58
59function _possibleConstructorReturn(self, call) {
60 if (call && (_typeof(call) === "object" || typeof call === "function")) {
61 return call;
62 }
63
64 return _assertThisInitialized(self);
65}
66
67function _assertThisInitialized(self) {
68 if (self === void 0) {
69 throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
70 }
71
72 return self;
73}
74
75function _getPrototypeOf(o) {
76 _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
77 return o.__proto__ || Object.getPrototypeOf(o);
78 };
79 return _getPrototypeOf(o);
80}
81
82function _inherits(subClass, superClass) {
83 if (typeof superClass !== "function" && superClass !== null) {
84 throw new TypeError("Super expression must either be null or a function");
85 }
86
87 subClass.prototype = Object.create(superClass && superClass.prototype, {
88 constructor: {
89 value: subClass,
90 writable: true,
91 configurable: true
92 }
93 });
94 if (superClass) _setPrototypeOf(subClass, superClass);
95}
96
97function _setPrototypeOf(o, p) {
98 _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
99 o.__proto__ = p;
100 return o;
101 };
102
103 return _setPrototypeOf(o, p);
104}
105
106function _objectSpread(target) {
107 for (var i = 1; i < arguments.length; i++) {
108 var source = arguments[i] != null ? arguments[i] : {};
109 var ownKeys = Object.keys(source);
110
111 if (typeof Object.getOwnPropertySymbols === 'function') {
112 ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {
113 return Object.getOwnPropertyDescriptor(source, sym).enumerable;
114 }));
115 }
116
117 ownKeys.forEach(function (key) {
118 _defineProperty(target, key, source[key]);
119 });
120 }
121
122 return target;
123}
124
125function _defineProperty(obj, key, value) {
126 if (key in obj) {
127 Object.defineProperty(obj, key, {
128 value: value,
129 enumerable: true,
130 configurable: true,
131 writable: true
132 });
133 } else {
134 obj[key] = value;
135 }
136
137 return obj;
138}
139/**
140 * Copyright IBM Corp. 2016, 2018
141 *
142 * This source code is licensed under the Apache-2.0 license found in the
143 * LICENSE file in the root directory of this source tree.
144 */
145
146
147import settings from '../../globals/js/settings';
148import eventMatches from '../../globals/js/misc/event-matches';
149import mixin from '../../globals/js/misc/mixin';
150import createComponent from '../../globals/js/mixins/create-component';
151import initComponentBySearch from '../../globals/js/mixins/init-component-by-search';
152import eventedShowHideState from '../../globals/js/mixins/evented-show-hide-state';
153import handles from '../../globals/js/mixins/handles';
154import FloatingMenu, { DIRECTION_TOP, DIRECTION_BOTTOM, DIRECTION_LEFT, DIRECTION_RIGHT } from '../floating-menu/floating-menu';
155import getLaunchingDetails from '../../globals/js/misc/get-launching-details';
156import on from '../../globals/js/misc/on';
157import { componentsX } from '../../globals/js/feature-flags';
158/**
159 * The CSS property names of the arrow keyed by the floating menu direction.
160 * @type {Object<string, string>}
161 */
162
163var triggerButtonPositionProps =
164/* #__PURE__ */
165function () {
166 var _ref;
167
168 return _ref = {}, _defineProperty(_ref, DIRECTION_TOP, 'bottom'), _defineProperty(_ref, DIRECTION_BOTTOM, 'top'), _defineProperty(_ref, DIRECTION_LEFT, 'left'), _defineProperty(_ref, DIRECTION_RIGHT, 'right'), _ref;
169}();
170/**
171 * Determines how the position of arrow should affect the floating menu position.
172 * @type {Object<string, number>}
173 */
174
175
176var triggerButtonPositionFactors =
177/* #__PURE__ */
178function () {
179 var _ref2;
180
181 return _ref2 = {}, _defineProperty(_ref2, DIRECTION_TOP, -2), _defineProperty(_ref2, DIRECTION_BOTTOM, -1), _defineProperty(_ref2, DIRECTION_LEFT, -2), _defineProperty(_ref2, DIRECTION_RIGHT, -1), _ref2;
182}();
183/**
184 * @param {Element} menuBody The menu body with the menu arrow.
185 * @param {string} direction The floating menu direction.
186 * @param {Element} trigger The trigger button.
187 * @returns {FloatingMenu~offset} The adjustment of the floating menu position, upon the position of the menu arrow.
188 * @private
189 */
190
191
192export var getMenuOffset = function getMenuOffset(menuBody, direction, trigger) {
193 var triggerButtonPositionProp = triggerButtonPositionProps[direction];
194 var triggerButtonPositionFactor = triggerButtonPositionFactors[direction];
195
196 if (!triggerButtonPositionProp || !triggerButtonPositionFactor) {
197 console.warn('Wrong floating menu direction:', direction); // eslint-disable-line no-console
198 }
199
200 var menuWidth = menuBody.offsetWidth;
201 var menuHeight = menuBody.offsetHeight;
202 var arrowStyle = menuBody.ownerDocument.defaultView.getComputedStyle(menuBody, ':before');
203 var values = [triggerButtonPositionProp, 'left', 'width', 'height', 'border-top-width'].reduce(function (o, name) {
204 return _objectSpread({}, o, _defineProperty({}, name, Number((/^([\d-.]+)px$/.exec(arrowStyle.getPropertyValue(name)) || [])[1])));
205 }, {});
206
207 if (Object.keys(values).every(function (name) {
208 return !isNaN(values[name]);
209 })) {
210 var left = values.left,
211 width = values.width,
212 height = values.height,
213 borderTopWidth = values['border-top-width'];
214 return {
215 left: menuWidth / 2 - (left + Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)) / 2),
216 top: Math.sqrt(Math.pow(borderTopWidth, 2) * 2) + triggerButtonPositionFactor * values[triggerButtonPositionProp]
217 };
218 }
219
220 if (componentsX) {
221 // eslint-disable-next-line no-use-before-define
222 var menu = OverflowMenu.components.get(trigger);
223
224 if (!menu) {
225 throw new TypeError('Overflow menu instance cannot be found.');
226 }
227
228 var flip = menuBody.classList.contains(menu.options.classMenuFlip);
229
230 if (triggerButtonPositionProp === 'top' || triggerButtonPositionProp === 'bottom') {
231 var triggerWidth = trigger.offsetWidth;
232 return {
233 left: (!flip ? 1 : -1) * (menuWidth / 2 - triggerWidth / 2),
234 top: 0
235 };
236 }
237
238 if (triggerButtonPositionProp === 'left' || triggerButtonPositionProp === 'right') {
239 var triggerHeight = trigger.offsetHeight;
240 return {
241 left: 0,
242 top: (!flip ? 1 : -1) * (menuHeight / 2 - triggerHeight / 2)
243 };
244 }
245 }
246
247 return undefined;
248};
249
250var OverflowMenu =
251/*#__PURE__*/
252function (_mixin) {
253 _inherits(OverflowMenu, _mixin);
254 /**
255 * Overflow menu.
256 * @extends CreateComponent
257 * @extends InitComponentBySearch
258 * @extends Handles
259 * @param {HTMLElement} element The element working as a modal dialog.
260 * @param {Object} [options] The component options.
261 * @param {string} [options.selectorOptionMenu] The CSS selector to find the menu.
262 * @param {string} [options.classShown] The CSS class for the shown state, for the trigger UI.
263 * @param {string} [options.classMenuShown] The CSS class for the shown state, for the menu.
264 * @param {string} [options.classMenuFlip] The CSS class for the flipped state of the menu.
265 * @param {Object} [options.objMenuOffset] The offset locating the menu for the non-flipped state.
266 * @param {Object} [options.objMenuOffsetFlip] The offset locating the menu for the flipped state.
267 */
268
269
270 function OverflowMenu(element, options) {
271 var _this;
272
273 _classCallCheck(this, OverflowMenu);
274
275 _this = _possibleConstructorReturn(this, _getPrototypeOf(OverflowMenu).call(this, element, options));
276
277 _this.getCurrentNavigation = function () {
278 var focused = _this.element.ownerDocument.activeElement;
279 return focused.nodeType === Node.ELEMENT_NODE && focused.matches(_this.options.selectorItem) ? focused : null;
280 };
281
282 _this.navigate = function (direction) {
283 var items = _toConsumableArray(_this.element.ownerDocument.querySelectorAll(_this.options.selectorItem));
284
285 var start = _this.getCurrentNavigation() || _this.element.querySelector(_this.options.selectorItemSelected);
286
287 var getNextItem = function getNextItem(old) {
288 var handleUnderflow = function handleUnderflow(index, length) {
289 return index + (index >= 0 ? 0 : length);
290 };
291
292 var handleOverflow = function handleOverflow(index, length) {
293 return index - (index < length ? 0 : length);
294 }; // `items.indexOf(old)` may be -1 (Scenario of no previous focus)
295
296
297 var index = Math.max(items.indexOf(old) + direction, -1);
298 return items[handleUnderflow(handleOverflow(index, items.length), items.length)];
299 };
300
301 for (var current = getNextItem(start); current && current !== start; current = getNextItem(current)) {
302 if (!current.matches(_this.options.selectorItemHidden) && !current.parentNode.matches(_this.options.selectorItemHidden) && !current.matches(_this.options.selectorItemSelected)) {
303 current.focus();
304 break;
305 }
306 }
307 };
308
309 _this.manage(on(_this.element.ownerDocument, 'click', function (event) {
310 _this._handleDocumentClick(event);
311
312 _this.wasOpenBeforeClick = undefined;
313 }));
314
315 _this.manage(on(_this.element.ownerDocument, 'keydown', function (event) {
316 _this._handleKeyPress(event);
317 }));
318
319 _this.manage(on(_this.element, 'mousedown', function () {
320 _this.wasOpenBeforeClick = element.classList.contains(_this.options.classShown);
321 }));
322
323 return _this;
324 }
325 /**
326 * Changes the shown/hidden state.
327 * @param {string} state The new state.
328 * @param {Object} detail The detail of the event trigging this action.
329 * @param {Function} callback Callback called when change in state completes.
330 */
331
332
333 _createClass(OverflowMenu, [{
334 key: "changeState",
335 value: function changeState(state, detail, callback) {
336 if (state === 'hidden') {
337 this.element.setAttribute('aria-expanded', 'false');
338 } else {
339 this.element.setAttribute('aria-expanded', 'true');
340 }
341
342 if (!this.optionMenu) {
343 var optionMenu = this.element.querySelector(this.options.selectorOptionMenu);
344
345 if (!optionMenu) {
346 throw new Error('Cannot find the target menu.');
347 } // Lazily create a component instance for menu
348
349
350 this.optionMenu = FloatingMenu.create(optionMenu, {
351 refNode: this.element,
352 classShown: this.options.classMenuShown,
353 classRefShown: this.options.classShown,
354 offset: this.options.objMenuOffset
355 });
356 this.children.push(this.optionMenu);
357 }
358
359 if (this.optionMenu.element.classList.contains(this.options.classMenuFlip)) {
360 this.optionMenu.options.offset = this.options.objMenuOffsetFlip;
361 } // Delegates the action of changing state to the menu.
362 // (And thus the before/after shown/hidden events are fired from the menu)
363
364
365 this.optionMenu.changeState(state, Object.assign(detail, {
366 delegatorNode: this.element
367 }), callback);
368 }
369 /**
370 * Handles click on document.
371 * @param {Event} event The triggering event.
372 * @private
373 */
374
375 }, {
376 key: "_handleDocumentClick",
377 value: function _handleDocumentClick(event) {
378 var element = this.element,
379 optionMenu = this.optionMenu,
380 wasOpenBeforeClick = this.wasOpenBeforeClick;
381 var isOfSelf = element.contains(event.target);
382 var isOfMenu = optionMenu && optionMenu.element.contains(event.target);
383 var shouldBeOpen = isOfSelf && !wasOpenBeforeClick;
384 var state = shouldBeOpen ? 'shown' : 'hidden';
385
386 if (isOfSelf) {
387 if (element.tagName === 'A') {
388 event.preventDefault();
389 }
390
391 event.delegateTarget = element; // eslint-disable-line no-param-reassign
392 }
393
394 if (!isOfMenu || eventMatches(event, this.options.selectorItem)) {
395 this.changeState(state, getLaunchingDetails(event), function () {
396 if (state === 'hidden' && isOfMenu) {
397 element.focus();
398 }
399 });
400 }
401 }
402 /**
403 * Provides the element to move focus from
404 * @returns {Element} Currently highlighted element.
405 */
406
407 }, {
408 key: "_handleKeyPress",
409
410 /**
411 * Handles key press on document.
412 * @param {Event} event The triggering event.
413 * @private
414 */
415 value: function _handleKeyPress(event) {
416 var key = event.which;
417 var element = this.element,
418 optionMenu = this.optionMenu,
419 options = this.options;
420 var isOfMenu = optionMenu && optionMenu.element.contains(event.target);
421 var isExpanded = this.element.classList.contains(this.options.classShown);
422
423 switch (key) {
424 // Esc
425 case 27:
426 this.changeState('hidden', getLaunchingDetails(event), function () {
427 if (isOfMenu) {
428 element.focus();
429 }
430 });
431 break;
432 // Enter || Space bar
433
434 case 13:
435 case 32:
436 {
437 if (!isExpanded && this.element.ownerDocument.activeElement !== this.element) {
438 return;
439 }
440
441 var isOfSelf = element.contains(event.target);
442 var shouldBeOpen = isOfSelf && !element.classList.contains(options.classShown);
443 var state = shouldBeOpen ? 'shown' : 'hidden';
444
445 if (isOfSelf) {
446 event.delegateTarget = element; // eslint-disable-line no-param-reassign
447
448 event.preventDefault(); // prevent scrolling
449
450 this.changeState(state, getLaunchingDetails(event), function () {
451 if (state === 'hidden' && isOfMenu) {
452 element.focus();
453 }
454 });
455 }
456
457 break;
458 }
459
460 case 38: // up arrow
461
462 case 40:
463 // down arrow
464 {
465 if (!isExpanded) {
466 return;
467 }
468
469 event.preventDefault(); // prevent scrolling
470
471 var direction = {
472 38: -1,
473 40: 1
474 }[event.which];
475 this.navigate(direction);
476 }
477 break;
478
479 default:
480 break;
481 }
482 }
483 }], [{
484 key: "options",
485 get: function get() {
486 var prefix = settings.prefix;
487 return {
488 selectorInit: '[data-overflow-menu]',
489 selectorOptionMenu: ".".concat(prefix, "--overflow-menu-options"),
490 selectorItem: "\n .".concat(prefix, "--overflow-menu-options--open >\n .").concat(prefix, "--overflow-menu-options__option:not(.").concat(prefix, "--overflow-menu-options__option--disabled) >\n .").concat(prefix, "--overflow-menu-options__btn\n "),
491 classShown: "".concat(prefix, "--overflow-menu--open"),
492 classMenuShown: "".concat(prefix, "--overflow-menu-options--open"),
493 classMenuFlip: "".concat(prefix, "--overflow-menu--flip"),
494 objMenuOffset: getMenuOffset,
495 objMenuOffsetFlip: getMenuOffset
496 };
497 }
498 }]);
499
500 OverflowMenu.components = new WeakMap();
501 return OverflowMenu;
502}(mixin(createComponent, initComponentBySearch, eventedShowHideState, handles));
503
504export default OverflowMenu;
\No newline at end of file