UNPKG

21.5 kBJavaScriptView Raw
1/*! @nrk/core-toggle v3.1.0 - Copyright (c) 2017-2021 NRK */
2'use strict';
3
4var React = require('react');
5
6function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
7
8var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
9
10function _classCallCheck(instance, Constructor) {
11 if (!(instance instanceof Constructor)) {
12 throw new TypeError("Cannot call a class as a function");
13 }
14}
15
16function _defineProperties(target, props) {
17 for (var i = 0; i < props.length; i++) {
18 var descriptor = props[i];
19 descriptor.enumerable = descriptor.enumerable || false;
20 descriptor.configurable = true;
21 if ("value" in descriptor) descriptor.writable = true;
22 Object.defineProperty(target, descriptor.key, descriptor);
23 }
24}
25
26function _createClass(Constructor, protoProps, staticProps) {
27 if (protoProps) _defineProperties(Constructor.prototype, protoProps);
28 if (staticProps) _defineProperties(Constructor, staticProps);
29 return Constructor;
30}
31
32function _inherits(subClass, superClass) {
33 if (typeof superClass !== "function" && superClass !== null) {
34 throw new TypeError("Super expression must either be null or a function");
35 }
36
37 subClass.prototype = Object.create(superClass && superClass.prototype, {
38 constructor: {
39 value: subClass,
40 writable: true,
41 configurable: true
42 }
43 });
44 if (superClass) _setPrototypeOf(subClass, superClass);
45}
46
47function _getPrototypeOf(o) {
48 _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
49 return o.__proto__ || Object.getPrototypeOf(o);
50 };
51 return _getPrototypeOf(o);
52}
53
54function _setPrototypeOf(o, p) {
55 _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
56 o.__proto__ = p;
57 return o;
58 };
59
60 return _setPrototypeOf(o, p);
61}
62
63function _isNativeReflectConstruct() {
64 if (typeof Reflect === "undefined" || !Reflect.construct) return false;
65 if (Reflect.construct.sham) return false;
66 if (typeof Proxy === "function") return true;
67
68 try {
69 Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
70 return true;
71 } catch (e) {
72 return false;
73 }
74}
75
76function _construct(Parent, args, Class) {
77 if (_isNativeReflectConstruct()) {
78 _construct = Reflect.construct;
79 } else {
80 _construct = function _construct(Parent, args, Class) {
81 var a = [null];
82 a.push.apply(a, args);
83 var Constructor = Function.bind.apply(Parent, a);
84 var instance = new Constructor();
85 if (Class) _setPrototypeOf(instance, Class.prototype);
86 return instance;
87 };
88 }
89
90 return _construct.apply(null, arguments);
91}
92
93function _isNativeFunction(fn) {
94 return Function.toString.call(fn).indexOf("[native code]") !== -1;
95}
96
97function _wrapNativeSuper(Class) {
98 var _cache = typeof Map === "function" ? new Map() : undefined;
99
100 _wrapNativeSuper = function _wrapNativeSuper(Class) {
101 if (Class === null || !_isNativeFunction(Class)) return Class;
102
103 if (typeof Class !== "function") {
104 throw new TypeError("Super expression must either be null or a function");
105 }
106
107 if (typeof _cache !== "undefined") {
108 if (_cache.has(Class)) return _cache.get(Class);
109
110 _cache.set(Class, Wrapper);
111 }
112
113 function Wrapper() {
114 return _construct(Class, arguments, _getPrototypeOf(this).constructor);
115 }
116
117 Wrapper.prototype = Object.create(Class.prototype, {
118 constructor: {
119 value: Wrapper,
120 enumerable: false,
121 writable: true,
122 configurable: true
123 }
124 });
125 return _setPrototypeOf(Wrapper, Class);
126 };
127
128 return _wrapNativeSuper(Class);
129}
130
131function _assertThisInitialized(self) {
132 if (self === void 0) {
133 throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
134 }
135
136 return self;
137}
138
139function _possibleConstructorReturn(self, call) {
140 if (call && (typeof call === "object" || typeof call === "function")) {
141 return call;
142 }
143
144 return _assertThisInitialized(self);
145}
146
147function _createSuper(Derived) {
148 var hasNativeReflectConstruct = _isNativeReflectConstruct();
149
150 return function _createSuperInternal() {
151 var Super = _getPrototypeOf(Derived),
152 result;
153
154 if (hasNativeReflectConstruct) {
155 var NewTarget = _getPrototypeOf(this).constructor;
156
157 result = Reflect.construct(Super, arguments, NewTarget);
158 } else {
159 result = Super.apply(this, arguments);
160 }
161
162 return _possibleConstructorReturn(this, result);
163 };
164}
165
166var IS_BROWSER = typeof window !== 'undefined';
167var IS_ANDROID = IS_BROWSER && /(android)/i.test(navigator.userAgent); // Bad, but needed
168
169var IS_IOS = IS_BROWSER && /iPad|iPhone|iPod/.test(String(navigator.platform));
170// Mock HTMLElement for Node
171
172if (!IS_BROWSER && !global.HTMLElement) {
173 global.HTMLElement = /*#__PURE__*/function () {
174 function _class() {
175 _classCallCheck(this, _class);
176 }
177
178 return _class;
179 }();
180}
181/**
182* closest
183* @param {Element} element Element to traverse up from
184* @param {String} selector A selector to search for matching parents or element itself
185* @return {Element|null} Element which is the closest ancestor matching selector
186*/
187
188var closest$1 = function () {
189 var proto = typeof window === 'undefined' ? {} : window.Element.prototype;
190 var match = proto.matches || proto.msMatchesSelector || proto.webkitMatchesSelector;
191 return proto.closest ? function (el, css) {
192 return el.closest(css);
193 } : function (el, css) {
194 // IE jumps to shadow SVG DOM on clicking an SVG defined by <use>.
195 // If so, jump back to <use> element and traverse real DOM
196 if (el.correspondingUseElement) el = el.correspondingUseElement;
197
198 for (; el; el = el.parentElement) {
199 if (match.call(el, css)) return el;
200 }
201
202 return null;
203 };
204}();
205/**
206* dispatchEvent - with infinite loop prevention
207* @param {Element} elem The target object
208* @param {String} name The source object(s)
209* @param {Object} detail Detail object (bubbles and cancelable is set to true)
210* @return {Boolean} Whether the event was canceled
211*/
212
213function dispatchEvent(element, name) {
214 var detail = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
215 var ignore = "prevent_recursive_dispatch_maximum_callstack".concat(name);
216 var event;
217 if (element[ignore]) return true; // We are already processing this event, so skip sending a new one
218 else element[ignore] = true; // Add name to dispatching ignore
219
220 if (typeof window.CustomEvent === 'function') {
221 event = new window.CustomEvent(name, {
222 bubbles: true,
223 cancelable: true,
224 detail: detail
225 });
226 } else {
227 event = document.createEvent('CustomEvent');
228 event.initCustomEvent(name, true, true, detail);
229 } // IE reports incorrect event.defaultPrevented
230 // but correct return value on element.dispatchEvent
231
232
233 var result = element.dispatchEvent(event);
234 element[ignore] = null; // Remove name from dispatching ignore
235
236 return result; // Follow W3C standard for return value
237}
238/**
239* getUUID
240* @return {String} A generated unique ID
241*/
242
243function getUUID() {
244 return Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
245}
246/**
247 * toggleAttribute (Ponyfill for IE and Edge, fixes #299)
248 * @link https://developer.mozilla.org/en-US/docs/Web/API/Element/toggleAttribute
249 * @param {Element} el Single DOM Element
250 * @param {String} name The name of the attribute to be toggled
251 * @param {Boolean} force Force attribute to be added or removed regardless of previous state
252 */
253
254function toggleAttribute(el, name) {
255 var force = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : !this.hasAttribute(name);
256 if (!force === el.hasAttribute(name)) el[force ? 'setAttribute' : 'removeAttribute'](name, '');
257 return force;
258}
259
260var SCROLLER = IS_BROWSER && document.createElement('div');
261
262var CoreToggle = /*#__PURE__*/function (_HTMLElement) {
263 _inherits(CoreToggle, _HTMLElement);
264
265 var _super = _createSuper(CoreToggle);
266
267 function CoreToggle() {
268 _classCallCheck(this, CoreToggle);
269
270 return _super.apply(this, arguments);
271 }
272
273 _createClass(CoreToggle, [{
274 key: "connectedCallback",
275 value: function connectedCallback() {
276 if (IS_IOS) document.documentElement.style.cursor = 'pointer'; // Fix iOS events for closing popups (https://stackoverflow.com/a/16006333/8819615)
277
278 if (!IS_ANDROID) this.setAttribute('aria-labelledby', this.button.id = this.button.id || getUUID()); // Andriod reads only label instead of content
279
280 this.value = this.button.textContent; // Set up aria-label
281
282 this.setAttribute('role', 'group'); // Help Edge
283
284 this.button.setAttribute('aria-expanded', this._open = !this.hidden);
285 this.button.setAttribute('aria-controls', this.id = this.id || getUUID());
286 document.addEventListener('keydown', this, true); // Use capture to enable checking defaultPrevented (from ESC key) in parents
287
288 document.addEventListener('click', this);
289 }
290 }, {
291 key: "disconnectedCallback",
292 value: function disconnectedCallback() {
293 this._button = null;
294 document.removeEventListener('keydown', this, true);
295 document.removeEventListener('click', this);
296 handleAutoposition(this, true);
297 }
298 }, {
299 key: "attributeChangedCallback",
300 value: function attributeChangedCallback() {
301 if (this._open === this.hidden) {
302 // this._open comparison ensures actual change
303 this.button.setAttribute('aria-expanded', this._open = !this.hidden);
304
305 try {
306 this.querySelector('[autofocus]').focus();
307 } catch (err) {}
308
309 handleAutoposition(this, this.hidden);
310 dispatchEvent(this, 'toggle');
311 }
312 }
313 }, {
314 key: "handleEvent",
315 value: function handleEvent(event) {
316 if (event.defaultPrevented) return;
317 if (event.type === 'resize' || event.type === 'scroll') return this.updatePosition();
318
319 if (event.type === 'keydown' && event.keyCode === 27) {
320 var isButton = event.target.getAttribute && event.target.getAttribute('aria-expanded') === 'true';
321 var isHiding = isButton ? event.target === this.button : closest$1(event.target, this.nodeName) === this;
322
323 if (isHiding) {
324 this.hidden = true;
325 this.button.focus(); // Move focus back to button
326
327 return event.preventDefault(); // Prevent closing maximized Safari and other coreToggles
328 }
329 }
330
331 if (event.type === 'click') {
332 var btn = closest$1(event.target, 'a,button');
333 if (btn && !btn.hasAttribute('aria-expanded') && closest$1(event.target, this.nodeName) === this) dispatchEvent(this, 'toggle.select', btn);else if (btn && btn.getAttribute('aria-controls') === this.id) this.hidden = !this.hidden;else if (this.popup && !this.contains(event.target)) this.hidden = true; // Click in content or outside
334 }
335 }
336 /**
337 * updatePosition Exposed for _very_ niche situations, use sparingly
338 * @param {HTMLElement} contentEl Reference to the core-toggle element
339 */
340
341 }, {
342 key: "updatePosition",
343 value: function updatePosition() {
344 var _this = this;
345
346 if (this._skipPosition || !this.button) return; // Avoid infinite loops for mutationObserver
347
348 this._skipPosition = true;
349 this.style.position = 'fixed'; // Set viewModel before reading dimensions
350
351 var triggerRect = this.button.getBoundingClientRect();
352 var contentRect = this.getBoundingClientRect();
353 var hasSpaceRight = triggerRect.left + contentRect.width < window.innerWidth;
354 var hasSpaceUnder = triggerRect.bottom + contentRect.height < window.innerHeight;
355 var hasSpaceOver = triggerRect.top - contentRect.height > 0; // Always place under when no hasSpaceOver, as no OS can scroll further up than window.scrollY = 0
356
357 var placeUnder = hasSpaceUnder || !hasSpaceOver;
358 var scroll = placeUnder ? window.pageYOffset + triggerRect.bottom + contentRect.height + 30 : 0;
359 this.style.left = "".concat(Math.round(hasSpaceRight ? triggerRect.left : triggerRect.right - contentRect.width), "px");
360 this.style.top = "".concat(Math.round(placeUnder ? triggerRect.bottom : triggerRect.top - contentRect.height), "px");
361 SCROLLER.style.cssText = "position:absolute;padding:1px;top:".concat(Math.round(scroll), "px");
362 setTimeout(function () {
363 return _this._skipPosition = null;
364 }); // Timeout to flush event queue before we can resume acting on mutations
365 }
366 }, {
367 key: "button",
368 get: function get() {
369 if (this._button && (this._button.getAttribute('data-for') || this._button.getAttribute('for')) === this.id) return this._button; // Speed up
370
371 return (this._button = this.id && document.querySelector("[for=\"".concat(this.id, "\"],[data-for=\"").concat(this.id, "\"]"))) || this.previousElementSibling;
372 } // aria-haspopup triggers forms mode in JAWS, therefore store as custom attr
373
374 }, {
375 key: "popup",
376 get: function get() {
377 return this.getAttribute('popup') === 'true' || this.getAttribute('popup') || this.hasAttribute('popup');
378 },
379 set: function set(val) {
380 this[val === false ? 'removeAttribute' : 'setAttribute']('popup', val);
381 }
382 }, {
383 key: "autoposition",
384 get: function get() {
385 return this.hasAttribute('autoposition');
386 },
387 set: function set(val) {
388 toggleAttribute(this, 'autoposition', val);
389 } // Must set attribute for IE11
390
391 }, {
392 key: "hidden",
393 get: function get() {
394 return this.hasAttribute('hidden');
395 },
396 set: function set(val) {
397 toggleAttribute(this, 'hidden', val);
398 } // Set this.button aria-label, so that visible button text can be augmentet with intention of button
399 // Example: Button text: "01.02.2019", aria-label: "01.02.2019, Choose date"
400 // Does not update aria-label if not already set to something else than this.popup
401
402 }, {
403 key: "value",
404 get: function get() {
405 return this.button.value || this.button.textContent;
406 },
407 set: function set(data) {
408 if (data === void 0) {
409 data = false;
410 }
411
412 if (!this.button || !this.popup.length) return;
413 var button = this.button;
414 var popup = (button.getAttribute('aria-label') || ",".concat(this.popup)).split(',')[1];
415 var label = data.textContent || data || ''; // data can be Element, Object or String
416
417 if (popup === this.popup) {
418 var target = button.querySelector('span') || button; // Use span to preserve embedded HTML and SVG
419
420 button.value = data.value || label;
421 target[data.innerHTML ? 'innerHTML' : 'textContent'] = data.innerHTML || label;
422 button.setAttribute('aria-label', "".concat(button.textContent, ",").concat(this.popup));
423 }
424 }
425 }], [{
426 key: "observedAttributes",
427 get: function get() {
428 return ['hidden', 'autoposition'];
429 }
430 }]);
431
432 return CoreToggle;
433}( /*#__PURE__*/_wrapNativeSuper(HTMLElement));
434
435function handleAutoposition(self, teardown) {
436 if (teardown) {
437 if (self._positionObserver) self._positionObserver.disconnect();
438 if (SCROLLER.parentNode) SCROLLER.parentNode.removeChild(SCROLLER);
439 self.style.position = self._positionObserver = null;
440 window.removeEventListener('scroll', self, true); // Use capture to also listen for elements with overflow
441
442 window.removeEventListener('resize', self);
443 } else if (self.autoposition) {
444 // Attach MutationObserver if supported
445 if (!self._positionObserver) self._positionObserver = window.MutationObserver && new window.MutationObserver(self.updatePosition.bind(self));
446 if (self._positionObserver) self._positionObserver.observe(self, {
447 childList: true,
448 subtree: true,
449 attributes: true
450 });
451 document.body.appendChild(SCROLLER);
452 window.addEventListener('scroll', self, true); // Use capture to also listen for elements with overflow
453
454 window.addEventListener('resize', self);
455 self.updatePosition(); // Initial trigger
456 }
457}
458
459var version = "3.1.0";
460
461/**
462* closest
463* @param {Element} el Element to traverse up from
464* @param {String} css A selector to search for matching parents or element itself
465* @return {Element|null} Element which is the closest ancestor matching selector
466*/
467
468var closest = function () {
469 var proto = typeof window === 'undefined' ? {} : window.Element.prototype;
470 var match = proto.matches || proto.msMatchesSelector || proto.webkitMatchesSelector;
471 return proto.closest ? function (el, css) {
472 return el.closest(css);
473 } : function (el, css) {
474 for (; el; el = el.parentElement) {
475 if (match.call(el, css)) {
476 return el;
477 }
478 }
479
480 return null;
481 };
482}();
483/**
484* customElementToReact
485* @param {Class|Function} elementClass A custom element definition.
486* @param {Array} options Props and custom events
487* @return {Object} A React component
488*/
489
490
491function customElementToReact(elementClass, options) {
492 if (options === void 0) options = {};
493 var name = elementClass.name || String(elementClass).match(/function ([^(]+)/)[1]; // String match for IE11
494
495 var dashCase = name.replace(/.[A-Z]/g, function (ref) {
496 var a = ref[0];
497 var b = ref[1];
498 return a + "-" + b;
499 }); // NameName -> name-name
500
501 var customProps = options.props || [];
502 var customEvents = options.customEvents || [];
503 var eventMap = customEvents.reduce(function (map, eventName) {
504 map[eventName] = "on" + eventName.replace(/(^|\.)./g, function (m) {
505 return m.slice(-1).toUpperCase();
506 }); // input.filter => onInputFilter
507
508 return map;
509 }, {});
510 var skipProps = customProps.concat('forwardRef', Object.keys(eventMap).map(function (onEventName) {
511 return eventMap[onEventName];
512 }));
513 var tagName = (dashCase + "-" + (options.suffix || 'react')).replace(/\W+/g, '-').toLowerCase();
514 return function (superclass) {
515 function anonymous(props) {
516 var this$1$1 = this;
517 superclass.call(this, props); // Register ref prop for accessing custom element https://reactjs.org/docs/refs-and-the-dom.html#callback-refs
518
519 this.ref = function (el) {
520 if (typeof this$1$1.props.forwardRef === 'function') {
521 this$1$1.props.forwardRef(el);
522 } else if (this$1$1.props.forwardRef) {
523 this$1$1.props.forwardRef.current = el;
524 }
525
526 return this$1$1.el = el;
527 }; // Register event handler on component for each custom event
528
529
530 Object.keys(eventMap).forEach(function (eventName) {
531 var onEventName = eventMap[eventName];
532
533 this$1$1[eventName] = function (event) {
534 if (this$1$1.props[onEventName] && closest(event.target, this$1$1.el.nodeName) === this$1$1.el) {
535 this$1$1.props[onEventName](event);
536 }
537 };
538 });
539 }
540
541 if (superclass) anonymous.__proto__ = superclass;
542 anonymous.prototype = Object.create(superclass && superclass.prototype);
543 anonymous.prototype.constructor = anonymous;
544
545 anonymous.prototype.componentDidMount = function componentDidMount() {
546 var this$1$1 = this; // Run connectedCallback() after React componentDidMount() to allow React hydration to run first
547
548 if (!window.customElements.get(tagName)) {
549 window.customElements.define(tagName, elementClass);
550 } // Populate properties on custom element
551
552
553 customProps.forEach(function (propName) {
554 if (propName in this$1$1.props) {
555 this$1$1.el[propName] = this$1$1.props[propName];
556 }
557 }); // Register events on custom element
558
559 customEvents.forEach(function (eventName) {
560 this$1$1.el.addEventListener(eventName, this$1$1[eventName]);
561 });
562 };
563
564 anonymous.prototype.componentDidUpdate = function componentDidUpdate(prev) {
565 var this$1$1 = this; // Sync prop changes to custom element
566
567 customProps.forEach(function (propName) {
568 if (prev[propName] !== this$1$1.props[propName]) {
569 this$1$1.el[propName] = this$1$1.props[propName];
570 }
571 });
572 };
573
574 anonymous.prototype.componentWillUnmount = function componentWillUnmount() {
575 var this$1$1 = this; // Remove event handlers on custom element on unmount
576
577 customEvents.forEach(function (eventName) {
578 this$1$1.el.removeEventListener(eventName, this$1$1[eventName]);
579 });
580 };
581
582 anonymous.prototype.render = function render() {
583 var this$1$1 = this; // Convert React props to CustomElement props https://github.com/facebook/react/issues/12810
584
585 return React__default['default'].createElement(tagName, Object.keys(this.props).reduce(function (thisProps, propName) {
586 if (skipProps.indexOf(propName) === -1) {
587 // Do not render customEvents and custom props as attributes
588 if (propName === 'className') {
589 thisProps["class"] = this$1$1.props[propName];
590 } // Fixes className for custom elements
591 else if (this$1$1.props[propName] === true) {
592 thisProps[propName] = '';
593 } // Fixes boolean attributes
594 else if (this$1$1.props[propName] !== false) {
595 thisProps[propName] = this$1$1.props[propName];
596 } // Pass only truthy, non-function props
597
598 }
599
600 return thisProps;
601 }, {
602 ref: this.ref
603 }));
604 };
605
606 return anonymous;
607 }(React__default['default'].Component);
608}
609
610var coreToggle = customElementToReact(CoreToggle, {
611 props: ['popup'],
612 customEvents: ['toggle', 'toggle.select'],
613 suffix: version
614});
615
616module.exports = coreToggle;