@nrk/core-toggle
Advanced tools
Comparing version 2.2.3 to 3.0.0
'use strict'; | ||
var name = "@nrk/core-toggle"; | ||
var version = "2.2.2"; | ||
function _classCallCheck(instance, Constructor) { | ||
if (!(instance instanceof Constructor)) { | ||
throw new TypeError("Cannot call a class as a function"); | ||
} | ||
} | ||
function _defineProperties(target, props) { | ||
for (var i = 0; i < props.length; i++) { | ||
var descriptor = props[i]; | ||
descriptor.enumerable = descriptor.enumerable || false; | ||
descriptor.configurable = true; | ||
if ("value" in descriptor) descriptor.writable = true; | ||
Object.defineProperty(target, descriptor.key, descriptor); | ||
} | ||
} | ||
function _createClass(Constructor, protoProps, staticProps) { | ||
if (protoProps) _defineProperties(Constructor.prototype, protoProps); | ||
if (staticProps) _defineProperties(Constructor, staticProps); | ||
return Constructor; | ||
} | ||
function _inherits(subClass, superClass) { | ||
if (typeof superClass !== "function" && superClass !== null) { | ||
throw new TypeError("Super expression must either be null or a function"); | ||
} | ||
subClass.prototype = Object.create(superClass && superClass.prototype, { | ||
constructor: { | ||
value: subClass, | ||
writable: true, | ||
configurable: true | ||
} | ||
}); | ||
if (superClass) _setPrototypeOf(subClass, superClass); | ||
} | ||
function _getPrototypeOf(o) { | ||
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { | ||
return o.__proto__ || Object.getPrototypeOf(o); | ||
}; | ||
return _getPrototypeOf(o); | ||
} | ||
function _setPrototypeOf(o, p) { | ||
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { | ||
o.__proto__ = p; | ||
return o; | ||
}; | ||
return _setPrototypeOf(o, p); | ||
} | ||
function isNativeReflectConstruct() { | ||
if (typeof Reflect === "undefined" || !Reflect.construct) return false; | ||
if (Reflect.construct.sham) return false; | ||
if (typeof Proxy === "function") return true; | ||
try { | ||
Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); | ||
return true; | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
function _construct(Parent, args, Class) { | ||
if (isNativeReflectConstruct()) { | ||
_construct = Reflect.construct; | ||
} else { | ||
_construct = function _construct(Parent, args, Class) { | ||
var a = [null]; | ||
a.push.apply(a, args); | ||
var Constructor = Function.bind.apply(Parent, a); | ||
var instance = new Constructor(); | ||
if (Class) _setPrototypeOf(instance, Class.prototype); | ||
return instance; | ||
}; | ||
} | ||
return _construct.apply(null, arguments); | ||
} | ||
function _isNativeFunction(fn) { | ||
return Function.toString.call(fn).indexOf("[native code]") !== -1; | ||
} | ||
function _wrapNativeSuper(Class) { | ||
var _cache = typeof Map === "function" ? new Map() : undefined; | ||
_wrapNativeSuper = function _wrapNativeSuper(Class) { | ||
if (Class === null || !_isNativeFunction(Class)) return Class; | ||
if (typeof Class !== "function") { | ||
throw new TypeError("Super expression must either be null or a function"); | ||
} | ||
if (typeof _cache !== "undefined") { | ||
if (_cache.has(Class)) return _cache.get(Class); | ||
_cache.set(Class, Wrapper); | ||
} | ||
function Wrapper() { | ||
return _construct(Class, arguments, _getPrototypeOf(this).constructor); | ||
} | ||
Wrapper.prototype = Object.create(Class.prototype, { | ||
constructor: { | ||
value: Wrapper, | ||
enumerable: false, | ||
writable: true, | ||
configurable: true | ||
} | ||
}); | ||
return _setPrototypeOf(Wrapper, Class); | ||
}; | ||
return _wrapNativeSuper(Class); | ||
} | ||
function _assertThisInitialized(self) { | ||
if (self === void 0) { | ||
throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); | ||
} | ||
return self; | ||
} | ||
function _possibleConstructorReturn(self, call) { | ||
if (call && (typeof call === "object" || typeof call === "function")) { | ||
return call; | ||
} | ||
return _assertThisInitialized(self); | ||
} | ||
var IS_BROWSER = typeof window !== 'undefined'; | ||
var IS_ANDROID = IS_BROWSER && /(android)/i.test(navigator.userAgent); // Bad, but needed | ||
var IS_IOS = IS_BROWSER && /iPad|iPhone|iPod/.test(String(navigator.platform)); | ||
var HAS_EVENT_OPTIONS = (function (has) { | ||
if ( has === void 0 ) has = false; | ||
try { window.addEventListener('test', null, { get passive () { has = true; } }); } catch (e) {} | ||
return has | ||
})(); | ||
var IS_IOS = IS_BROWSER && /iPad|iPhone|iPod/.test(String(navigator.platform)); // Polyfill toggleAttribute for IE | ||
if (IS_BROWSER && !window.Element.prototype.toggleAttribute) { | ||
window.Element.prototype.toggleAttribute = function (name) { | ||
var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : !this.hasAttribute(name); | ||
if (!force === this.hasAttribute(name)) this[force ? 'setAttribute' : 'removeAttribute'](name, ''); | ||
return force; | ||
}; | ||
} | ||
/** | ||
* addEvent | ||
* @param {String} uuid An unique ID of the event to bind - ensurnes single instance | ||
* @param {String} type The type of event to bind | ||
* @param {Function} handler The function to call on event | ||
* @param {Boolean|Object} options useCapture or options object for addEventListener. Defaults to false | ||
* closest | ||
* @param {Element} element Element to traverse up from | ||
* @param {String} selector A selector to search for matching parents or element itself | ||
* @return {Element|null} Element which is the closest ancestor matching selector | ||
*/ | ||
function addEvent (uuid, type, handler, options) { | ||
if ( options === void 0 ) options = false; | ||
if (typeof window === 'undefined' || window[uuid = uuid + "-" + type]) { return } // Ensure single instance | ||
if (!HAS_EVENT_OPTIONS && typeof options === 'object') { options = Boolean(options.capture); } // Fix unsupported options | ||
var node = (type === 'resize' || type === 'load') ? window : document; | ||
node.addEventListener(window[uuid] = type, handler, options); | ||
} | ||
var closest = function () { | ||
var proto = typeof window === 'undefined' ? {} : window.Element.prototype; | ||
var match = proto.matches || proto.msMatchesSelector || proto.webkitMatchesSelector; | ||
return proto.closest ? function (el, css) { | ||
return el.closest(css); | ||
} : function (el, css) { | ||
for (; el; el = el.parentElement) { | ||
if (match.call(el, css)) return el; | ||
} | ||
return null; | ||
}; | ||
}(); | ||
/** | ||
@@ -39,26 +179,28 @@ * dispatchEvent - with infinite loop prevention | ||
*/ | ||
var IGNORE = 'prevent_recursive_dispatch_maximum_callstack'; | ||
function dispatchEvent (element, name, detail) { | ||
if ( detail === void 0 ) detail = {}; | ||
var ignore = "" + IGNORE + name; | ||
function dispatchEvent(element, name) { | ||
var detail = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; | ||
var ignore = "prevent_recursive_dispatch_maximum_callstack".concat(name); | ||
var event; | ||
if (element[ignore]) return true; // We are already processing this event, so skip sending a new one | ||
else element[ignore] = true; // Add name to dispatching ignore | ||
if (element[ignore]) { return true } // We are already processing this event, so skip sending a new one | ||
else { element[ignore] = true; } // Add name to dispatching ignore | ||
if (typeof window.CustomEvent === 'function') { | ||
event = new window.CustomEvent(name, { bubbles: true, cancelable: true, detail: detail }); | ||
event = new window.CustomEvent(name, { | ||
bubbles: true, | ||
cancelable: true, | ||
detail: detail | ||
}); | ||
} else { | ||
event = document.createEvent('CustomEvent'); | ||
event.initCustomEvent(name, true, true, detail); | ||
} | ||
// IE reports incorrect event.defaultPrevented | ||
} // IE reports incorrect event.defaultPrevented | ||
// but correct return value on element.dispatchEvent | ||
var result = element.dispatchEvent(event); | ||
element[ignore] = null; // Remove name from dispatching ignore | ||
return result // Follow W3C standard for return value | ||
return result; // Follow W3C standard for return value | ||
} | ||
/** | ||
@@ -68,107 +210,134 @@ * getUUID | ||
*/ | ||
function getUUID (el) { | ||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 5) | ||
function getUUID() { | ||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 5); | ||
} | ||
/** | ||
* queryAll | ||
* @param {String|NodeList|Array|Element} elements A CSS selector string, nodeList, element array, or single element | ||
* @return {Element[]} Array of elements | ||
*/ | ||
function queryAll (elements, context) { | ||
if ( context === void 0 ) context = document; | ||
var CoreToggle = | ||
/*#__PURE__*/ | ||
function (_HTMLElement) { | ||
_inherits(CoreToggle, _HTMLElement); | ||
if (elements) { | ||
if (elements.nodeType) { return [elements] } | ||
if (typeof elements === 'string') { return [].slice.call(context.querySelectorAll(elements)) } | ||
if (elements.length) { return [].slice.call(elements) } | ||
function CoreToggle() { | ||
_classCallCheck(this, CoreToggle); | ||
return _possibleConstructorReturn(this, _getPrototypeOf(CoreToggle).apply(this, arguments)); | ||
} | ||
return [] | ||
} | ||
var UUID = ("data-" + name + "-" + version).replace(/\W+/g, '-'); // Strip invalid attribute characters | ||
var ARIA = IS_ANDROID ? 'data' : 'aria'; // Andriod has a bug and reads only label instead of content | ||
var KEYS = { ESC: 27 }; | ||
_createClass(CoreToggle, [{ | ||
key: "connectedCallback", | ||
value: function connectedCallback() { | ||
if (IS_IOS) document.documentElement.style.cursor = 'pointer'; // Fix iOS events for closing popups (https://stackoverflow.com/a/16006333/8819615) | ||
function toggle (toggles, open) { | ||
var options = typeof open === 'object' ? open : { open: open }; | ||
if (IS_IOS) { document.documentElement.style.cursor = 'pointer'; } // Fix iOS events for closing popups (https://stackoverflow.com/a/16006333/8819615) | ||
if (!IS_ANDROID) this.setAttribute('aria-labelledby', this.button.id = this.button.id || getUUID()); // Andriod reads only label instead of content | ||
return queryAll(toggles).map(function (toggle) { | ||
var content = getContentElement(toggle); | ||
var isOpen = toggle.getAttribute('aria-expanded') === 'true'; | ||
var open = typeof options.open === 'boolean' ? options.open : (options.open === 'toggle' ? !isOpen : isOpen); | ||
var popup = String((options.hasOwnProperty('popup') ? options.popup : toggle.getAttribute(UUID)) || false); | ||
this.value = this.button.textContent; // Set up aria-label | ||
if (options.value) { toggle.innerHTML = options.value; } // Set innerHTML before updating aria-label | ||
if (popup !== 'false' && popup !== 'true') { toggle.setAttribute('aria-label', ((toggle.textContent) + ", " + popup)); } // Only update aria-label if popup-mode | ||
this.setAttribute('role', 'group'); // Help Edge | ||
toggle.setAttribute(UUID, popup); // aria-haspopup triggers forms mode in JAWS, therefore store in uuid | ||
toggle.setAttribute('aria-controls', content.id = content.id || getUUID()); | ||
content.setAttribute((ARIA + "-labelledby"), toggle.id = toggle.id || getUUID()); | ||
setOpen(toggle, open); | ||
return toggle | ||
}) | ||
} | ||
this.button.setAttribute('aria-expanded', this._open = !this.hidden); | ||
this.button.setAttribute('aria-controls', this.id = this.id || getUUID()); | ||
document.addEventListener('keydown', this, true); // Use capture to enable checking defaultPrevented (from ESC key) in parents | ||
function getContentElement (toggle) { | ||
return document.getElementById(toggle.getAttribute('aria-controls')) || toggle.nextElementSibling | ||
} | ||
document.addEventListener('click', this); | ||
} | ||
}, { | ||
key: "disconnectedCallback", | ||
value: function disconnectedCallback() { | ||
this._button = null; | ||
document.removeEventListener('keydown', this, true); | ||
document.removeEventListener('click', this); | ||
} | ||
}, { | ||
key: "attributeChangedCallback", | ||
value: function attributeChangedCallback() { | ||
if (this._open === this.hidden) { | ||
// this._open comparison ensures actual change | ||
this.button.setAttribute('aria-expanded', this._open = !this.hidden); | ||
addEvent(UUID, 'keydown', function (event) { | ||
if (event.keyCode !== KEYS.ESC) { return } | ||
for (var el = event.target; el; el = el.parentElement) { | ||
var toggle = (el.id && document.querySelector(("[aria-controls=\"" + (el.id) + "\"]"))) || el; | ||
try { | ||
this.querySelector('[autofocus]').focus(); | ||
} catch (err) {} | ||
if (toggle.getAttribute(UUID) !== 'false' && toggle.getAttribute('aria-expanded') === 'true') { | ||
event.preventDefault(); // Prevent leaving maximized window in Safari | ||
toggle.focus(); | ||
return setOpen(toggle, false) | ||
dispatchEvent(this, 'toggle'); | ||
} | ||
} | ||
} | ||
}, true); // Use capture to enable checking defaultPrevented (from ESC key) in parents | ||
}, { | ||
key: "handleEvent", | ||
value: function handleEvent(event) { | ||
if (event.defaultPrevented) return; | ||
addEvent(UUID, 'click', function (ref) { | ||
var target = ref.target; | ||
var defaultPrevented = ref.defaultPrevented; | ||
if (event.type === 'keydown' && event.keyCode === 27) { | ||
var isButton = event.target.getAttribute && event.target.getAttribute('aria-expanded') === 'true'; | ||
var isHiding = isButton ? event.target === this.button : closest(event.target, this.nodeName) === this; | ||
if (defaultPrevented) { return false } // Do not toggle if someone run event.preventDefault() | ||
if (isHiding) { | ||
this.hidden = true; | ||
this.button.focus(); // Move focus back to button | ||
for (var el = target, item = (void 0); el; el = el.parentElement) { | ||
var toggle = item && el.id && document.querySelector(("[" + UUID + "][aria-controls=\"" + (el.id) + "\"]")); | ||
if ((el.nodeName === 'BUTTON' || el.nodeName === 'A') && !el.hasAttribute(UUID)) { item = el; } // interactive element clicked | ||
if (toggle) { | ||
dispatchEvent(toggle, 'toggle.select', { | ||
relatedTarget: getContentElement(toggle), | ||
currentTarget: item, | ||
value: item.textContent.trim() | ||
}); | ||
break | ||
return event.preventDefault(); // Prevent closing maximized Safari and other coreToggles | ||
} | ||
} | ||
if (event.type === 'click') { | ||
var btn = closest(event.target, 'a,button'); | ||
if (btn && !btn.hasAttribute('aria-expanded') && closest(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 | ||
} | ||
} | ||
} | ||
}, { | ||
key: "button", | ||
get: function get() { | ||
if (this._button && this._button.getAttribute('for') === this.id) return this._button; // Speed up | ||
queryAll(("[" + UUID + "]")).forEach(function (toggle) { | ||
var open = toggle.getAttribute('aria-expanded') === 'true'; | ||
var popup = toggle.getAttribute(UUID) !== 'false'; | ||
var content = getContentElement(toggle); | ||
return (this._button = this.id && document.querySelector("[for=\"".concat(this.id, "\"]"))) || this.previousElementSibling; | ||
} // aria-haspopup triggers forms mode in JAWS, therefore store as custom attr | ||
if (toggle.contains(target)) { setOpen(toggle, !open); } // Click on toggle | ||
else if (popup && open) { setOpen(toggle, content.contains(target)); } // Click in content or outside | ||
}); | ||
}); | ||
}, { | ||
key: "popup", | ||
get: function get() { | ||
return this.getAttribute('popup') === 'true' || this.getAttribute('popup') || this.hasAttribute('popup'); | ||
}, | ||
set: function set(val) { | ||
this[val === false ? 'removeAttribute' : 'setAttribute']('popup', val); | ||
} // Must set attribute for IE11 | ||
function setOpen (toggle, open) { | ||
var content = getContentElement(toggle); | ||
var isOpen = toggle.getAttribute('aria-expanded') === 'true'; | ||
var willOpen = typeof open === 'boolean' ? open : (open === 'toggle' ? !isOpen : isOpen); | ||
var isUpdate = isOpen === willOpen || dispatchEvent(toggle, 'toggle', { relatedTarget: content, isOpen: isOpen, willOpen: willOpen }); | ||
var nextOpen = isUpdate ? willOpen : toggle.getAttribute('aria-expanded') === 'true'; // dispatchEvent can change attributes | ||
var focus = !isOpen && nextOpen && content.querySelector('[autofocus]'); | ||
}, { | ||
key: "hidden", | ||
get: function get() { | ||
return this.hasAttribute('hidden'); | ||
}, | ||
set: function set(val) { | ||
this.toggleAttribute('hidden', val); | ||
} // Sets this.button aria-label, so visible button text can be augmentet with intension of button | ||
// Example: Button text: "01.02.2019", aria-label: "01.02.2019, Choose date" | ||
// Does not updates aria-label if not allready set to something else than this.popup | ||
if (focus) { setTimeout(function () { return focus && focus.focus(); }); } // Move focus on next render (if element stil exists) | ||
}, { | ||
key: "value", | ||
get: function get() { | ||
return this.button.value || this.button.textContent; | ||
}, | ||
set: function set() { | ||
var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; | ||
if (!this.button || !this.popup.length) return; | ||
var button = this.button; | ||
var popup = (button.getAttribute('aria-label') || ",".concat(this.popup)).split(',')[1]; | ||
var label = data.textContent || data || ''; // data can be Element, Object or String | ||
toggle.setAttribute('aria-expanded', nextOpen); | ||
content[nextOpen ? 'removeAttribute' : 'setAttribute']('hidden', ''); | ||
} | ||
if (popup === this.popup) { | ||
button.value = data.value || label; | ||
button[data.innerHTML ? 'innerHTML' : 'textContent'] = data.innerHTML || label; | ||
button.setAttribute('aria-label', "".concat(button.textContent, ",").concat(this.popup)); | ||
} | ||
} | ||
}], [{ | ||
key: "observedAttributes", | ||
get: function get() { | ||
return ['hidden']; | ||
} | ||
}]); | ||
module.exports = toggle; | ||
return CoreToggle; | ||
}(_wrapNativeSuper(HTMLElement)); | ||
module.exports = CoreToggle; |
@@ -1,84 +0,76 @@ | ||
import { name, version } from './package.json' | ||
import { IS_ANDROID, IS_IOS, addEvent, dispatchEvent, getUUID, queryAll } from '../utils' | ||
import { IS_ANDROID, IS_IOS, closest, dispatchEvent, getUUID } from '../utils' | ||
const UUID = `data-${name}-${version}`.replace(/\W+/g, '-') // Strip invalid attribute characters | ||
const ARIA = IS_ANDROID ? 'data' : 'aria' // Andriod has a bug and reads only label instead of content | ||
const KEYS = { ESC: 27 } | ||
export default class CoreToggle extends HTMLElement { | ||
static get observedAttributes () { return ['hidden'] } | ||
export default function toggle (toggles, open) { | ||
const options = typeof open === 'object' ? open : { open } | ||
if (IS_IOS) document.documentElement.style.cursor = 'pointer' // Fix iOS events for closing popups (https://stackoverflow.com/a/16006333/8819615) | ||
connectedCallback () { | ||
if (IS_IOS) document.documentElement.style.cursor = 'pointer' // Fix iOS events for closing popups (https://stackoverflow.com/a/16006333/8819615) | ||
if (!IS_ANDROID) this.setAttribute('aria-labelledby', this.button.id = this.button.id || getUUID()) // Andriod reads only label instead of content | ||
return queryAll(toggles).map((toggle) => { | ||
const content = getContentElement(toggle) | ||
const isOpen = toggle.getAttribute('aria-expanded') === 'true' | ||
const open = typeof options.open === 'boolean' ? options.open : (options.open === 'toggle' ? !isOpen : isOpen) | ||
const popup = String((options.hasOwnProperty('popup') ? options.popup : toggle.getAttribute(UUID)) || false) | ||
if (options.value) toggle.innerHTML = options.value // Set innerHTML before updating aria-label | ||
if (popup !== 'false' && popup !== 'true') toggle.setAttribute('aria-label', `${toggle.textContent}, ${popup}`) // Only update aria-label if popup-mode | ||
toggle.setAttribute(UUID, popup) // aria-haspopup triggers forms mode in JAWS, therefore store in uuid | ||
toggle.setAttribute('aria-controls', content.id = content.id || getUUID()) | ||
content.setAttribute(`${ARIA}-labelledby`, toggle.id = toggle.id || getUUID()) | ||
setOpen(toggle, open) | ||
return toggle | ||
}) | ||
} | ||
function getContentElement (toggle) { | ||
return document.getElementById(toggle.getAttribute('aria-controls')) || toggle.nextElementSibling | ||
} | ||
addEvent(UUID, 'keydown', (event) => { | ||
if (event.keyCode !== KEYS.ESC) return | ||
for (let el = event.target; el; el = el.parentElement) { | ||
const toggle = (el.id && document.querySelector(`[aria-controls="${el.id}"]`)) || el | ||
if (toggle.getAttribute(UUID) !== 'false' && toggle.getAttribute('aria-expanded') === 'true') { | ||
event.preventDefault() // Prevent leaving maximized window in Safari | ||
toggle.focus() | ||
return setOpen(toggle, false) | ||
this.value = this.button.textContent // Set up aria-label | ||
this.setAttribute('role', 'group') // Help Edge | ||
this.button.setAttribute('aria-expanded', this._open = !this.hidden) | ||
this.button.setAttribute('aria-controls', this.id = this.id || getUUID()) | ||
document.addEventListener('keydown', this, true) // Use capture to enable checking defaultPrevented (from ESC key) in parents | ||
document.addEventListener('click', this) | ||
} | ||
disconnectedCallback () { | ||
this._button = null | ||
document.removeEventListener('keydown', this, true) | ||
document.removeEventListener('click', this) | ||
} | ||
attributeChangedCallback () { | ||
if (this._open === this.hidden) { // this._open comparison ensures actual change | ||
this.button.setAttribute('aria-expanded', this._open = !this.hidden) | ||
try { this.querySelector('[autofocus]').focus() } catch (err) {} | ||
dispatchEvent(this, 'toggle') | ||
} | ||
} | ||
}, true) // Use capture to enable checking defaultPrevented (from ESC key) in parents | ||
addEvent(UUID, 'click', ({ target, defaultPrevented }) => { | ||
if (defaultPrevented) return false // Do not toggle if someone run event.preventDefault() | ||
for (let el = target, item; el; el = el.parentElement) { | ||
const toggle = item && el.id && document.querySelector(`[${UUID}][aria-controls="${el.id}"]`) | ||
if ((el.nodeName === 'BUTTON' || el.nodeName === 'A') && !el.hasAttribute(UUID)) item = el // interactive element clicked | ||
if (toggle) { | ||
dispatchEvent(toggle, 'toggle.select', { | ||
relatedTarget: getContentElement(toggle), | ||
currentTarget: item, | ||
value: item.textContent.trim() | ||
}) | ||
break | ||
handleEvent (event) { | ||
if (event.defaultPrevented) return | ||
if (event.type === 'keydown' && event.keyCode === 27) { | ||
const isButton = event.target.getAttribute && event.target.getAttribute('aria-expanded') === 'true' | ||
const isHiding = isButton ? event.target === this.button : closest(event.target, this.nodeName) === this | ||
if (isHiding) { | ||
this.hidden = true | ||
this.button.focus() // Move focus back to button | ||
return event.preventDefault() // Prevent closing maximized Safari and other coreToggles | ||
} | ||
} | ||
if (event.type === 'click') { | ||
const btn = closest(event.target, 'a,button') | ||
if (btn && !btn.hasAttribute('aria-expanded') && closest(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 | ||
} | ||
} | ||
get button () { | ||
if (this._button && this._button.getAttribute('for') === this.id) return this._button // Speed up | ||
return (this._button = this.id && document.querySelector(`[for="${this.id}"]`)) || this.previousElementSibling | ||
} | ||
queryAll(`[${UUID}]`).forEach((toggle) => { | ||
const open = toggle.getAttribute('aria-expanded') === 'true' | ||
const popup = toggle.getAttribute(UUID) !== 'false' | ||
const content = getContentElement(toggle) | ||
// aria-haspopup triggers forms mode in JAWS, therefore store as custom attr | ||
get popup () { return this.getAttribute('popup') === 'true' || this.getAttribute('popup') || this.hasAttribute('popup') } | ||
set popup (val) { this[val === false ? 'removeAttribute' : 'setAttribute']('popup', val) } | ||
if (toggle.contains(target)) setOpen(toggle, !open) // Click on toggle | ||
else if (popup && open) setOpen(toggle, content.contains(target)) // Click in content or outside | ||
}) | ||
}) | ||
// Must set attribute for IE11 | ||
get hidden () { return this.hasAttribute('hidden') } | ||
set hidden (val) { this.toggleAttribute('hidden', val) } | ||
function setOpen (toggle, open) { | ||
const content = getContentElement(toggle) | ||
const isOpen = toggle.getAttribute('aria-expanded') === 'true' | ||
const willOpen = typeof open === 'boolean' ? open : (open === 'toggle' ? !isOpen : isOpen) | ||
const isUpdate = isOpen === willOpen || dispatchEvent(toggle, 'toggle', { relatedTarget: content, isOpen, willOpen }) | ||
const nextOpen = isUpdate ? willOpen : toggle.getAttribute('aria-expanded') === 'true' // dispatchEvent can change attributes | ||
const focus = !isOpen && nextOpen && content.querySelector('[autofocus]') | ||
// Sets this.button aria-label, so visible button text can be augmentet with intension of button | ||
// Example: Button text: "01.02.2019", aria-label: "01.02.2019, Choose date" | ||
// Does not updates aria-label if not allready set to something else than this.popup | ||
get value () { return this.button.value || this.button.textContent } | ||
set value (data = false) { | ||
if (!this.button || !this.popup.length) return | ||
const button = this.button | ||
const popup = (button.getAttribute('aria-label') || `,${this.popup}`).split(',')[1] | ||
const label = data.textContent || data || '' // data can be Element, Object or String | ||
if (focus) setTimeout(() => focus && focus.focus()) // Move focus on next render (if element stil exists) | ||
toggle.setAttribute('aria-expanded', nextOpen) | ||
content[nextOpen ? 'removeAttribute' : 'setAttribute']('hidden', '') | ||
if (popup === this.popup) { | ||
button.value = data.value || label | ||
button[data.innerHTML ? 'innerHTML' : 'textContent'] = data.innerHTML || label | ||
button.setAttribute('aria-label', `${button.textContent},${this.popup}`) | ||
} | ||
} | ||
} |
@@ -1,54 +0,4 @@ | ||
import React from 'react' | ||
import PropTypes from 'prop-types' | ||
import coreToggle from './core-toggle' | ||
import { exclude } from '../utils' | ||
import CoreToggle from './core-toggle.js' | ||
import { elementToReact } from '../utils.js' | ||
export default class Toggle extends React.Component { | ||
static get defaultProps () { return { open: null, popup: null, onToggle: null, onToggleSelect: null } } | ||
constructor (props) { | ||
super(props) | ||
this.onToggle = this.onToggle.bind(this) | ||
this.onToggleSelect = this.onToggleSelect.bind(this) | ||
} | ||
update () { | ||
coreToggle(this.el.firstElementChild, { | ||
popup: this.props.popup, | ||
open: this.props.open | ||
}) | ||
} | ||
componentDidMount () { | ||
this.update() | ||
this.el.addEventListener('toggle', this.onToggle) | ||
this.el.addEventListener('toggle.select', this.onToggleSelect) | ||
} | ||
componentDidUpdate () { this.update() } | ||
componentWillUnmount () { | ||
this.el.removeEventListener('toggle', this.onToggle) | ||
this.el.removeEventListener('toggle.select', this.onToggleSelect) | ||
} | ||
onToggle (event) { | ||
this.props.onToggle && this.props.onToggle(event) | ||
} | ||
onToggleSelect (event) { | ||
this.props.onToggleSelect && this.props.onToggleSelect(event) | ||
} | ||
render () { | ||
return React.createElement('div', exclude(this.props, Toggle.defaultProps, { ref: (el) => (this.el = el) }), | ||
React.Children.map(this.props.children, (child, adjacent) => { | ||
if (adjacent === 0) { | ||
return React.cloneElement(child, { | ||
'aria-expanded': String(Boolean(this.props.open)) | ||
}) | ||
} | ||
if (adjacent === 1) return React.cloneElement(child, { 'hidden': !this.props.open }) | ||
return child | ||
}) | ||
) | ||
} | ||
} | ||
Toggle.propTypes = { | ||
open: PropTypes.bool, | ||
popup: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), | ||
onToggle: PropTypes.func | ||
} | ||
export default elementToReact(CoreToggle, 'toggle', 'toggle.select') |
@@ -1,3 +0,437 @@ | ||
/*! @nrk/core-toggle v2.2.2 - Copyright (c) 2017-2019 NRK */ | ||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("react"),require("prop-types")):"function"==typeof define&&define.amd?define(["react","prop-types"],t):(e=e||self).CoreToggle=t(e.React,e.PropTypes)}(this,function(u,e){"use strict";u=u&&u.hasOwnProperty("default")?u.default:u,e=e&&e.hasOwnProperty("default")?e.default:e;var t="undefined"!=typeof window,n=t&&/(android)/i.test(navigator.userAgent),o=t&&/iPad|iPhone|iPod/.test(String(navigator.platform)),r=function(e){void 0===e&&(e=!1);try{window.addEventListener("test",null,{get passive(){e=!0}})}catch(e){}return e}();function i(e,t,n,o){(void 0===o&&(o=!1),"undefined"==typeof window||window[e=e+"-"+t])||(r||"object"!=typeof o||(o=Boolean(o.capture)),("resize"===t||"load"===t?window:document).addEventListener(window[e]=t,n,o))}var a="prevent_recursive_dispatch_maximum_callstack";function l(e,t,n){void 0===n&&(n={});var o,r=""+a+t;if(e[r])return!0;e[r]=!0,"function"==typeof window.CustomEvent?o=new window.CustomEvent(t,{bubbles:!0,cancelable:!0,detail:n}):(o=document.createEvent("CustomEvent")).initCustomEvent(t,!0,!0,n);var i=e.dispatchEvent(o);return e[r]=null,i}function p(e){return Date.now().toString(36)+Math.random().toString(36).slice(2,5)}function d(e,t){if(void 0===t&&(t=document),e){if(e.nodeType)return[e];if("string"==typeof e)return[].slice.call(t.querySelectorAll(e));if(e.length)return[].slice.call(e)}return[]}var s="data-@nrk/core-toggle-2.2.2".replace(/\W+/g,"-"),c=n?"data":"aria",g=27;function f(e){return document.getElementById(e.getAttribute("aria-controls"))||e.nextElementSibling}function v(e,t){var n=f(e),o="true"===e.getAttribute("aria-expanded"),r="boolean"==typeof t?t:"toggle"===t?!o:o,i=o===r||l(e,"toggle",{relatedTarget:n,isOpen:o,willOpen:r})?r:"true"===e.getAttribute("aria-expanded"),u=!o&&i&&n.querySelector("[autofocus]");u&&setTimeout(function(){return u&&u.focus()}),e.setAttribute("aria-expanded",i),n[i?"removeAttribute":"setAttribute"]("hidden","")}i(s,"keydown",function(e){if(e.keyCode===g)for(var t=e.target;t;t=t.parentElement){var n=t.id&&document.querySelector('[aria-controls="'+t.id+'"]')||t;if("false"!==n.getAttribute(s)&&"true"===n.getAttribute("aria-expanded"))return e.preventDefault(),n.focus(),v(n,!1)}},!0),i(s,"click",function(e){var r=e.target;if(e.defaultPrevented)return!1;for(var t=r,n=void 0;t;t=t.parentElement){var o=n&&t.id&&document.querySelector("["+s+'][aria-controls="'+t.id+'"]');if("BUTTON"!==t.nodeName&&"A"!==t.nodeName||t.hasAttribute(s)||(n=t),o){l(o,"toggle.select",{relatedTarget:f(o),currentTarget:n,value:n.textContent.trim()});break}}d("["+s+"]").forEach(function(e){var t="true"===e.getAttribute("aria-expanded"),n="false"!==e.getAttribute(s),o=f(e);e.contains(r)?v(e,!t):n&&t&&v(e,o.contains(r))})});var h=function(t){function i(e){t.call(this,e),this.onToggle=this.onToggle.bind(this),this.onToggleSelect=this.onToggleSelect.bind(this)}t&&(i.__proto__=t),(i.prototype=Object.create(t&&t.prototype)).constructor=i;var e={defaultProps:{configurable:!0}};return e.defaultProps.get=function(){return{open:null,popup:null,onToggle:null,onToggleSelect:null}},i.prototype.update=function(){var e,t,i;e=this.el.firstElementChild,t={popup:this.props.popup,open:this.props.open},i="object"==typeof t?t:{open:t},o&&(document.documentElement.style.cursor="pointer"),d(e).map(function(e){var t=f(e),n="true"===e.getAttribute("aria-expanded"),o="boolean"==typeof i.open?i.open:"toggle"===i.open?!n:n,r=String((i.hasOwnProperty("popup")?i.popup:e.getAttribute(s))||!1);return i.value&&(e.innerHTML=i.value),"false"!==r&&"true"!==r&&e.setAttribute("aria-label",e.textContent+", "+r),e.setAttribute(s,r),e.setAttribute("aria-controls",t.id=t.id||p()),t.setAttribute(c+"-labelledby",e.id=e.id||p()),v(e,o),e})},i.prototype.componentDidMount=function(){this.update(),this.el.addEventListener("toggle",this.onToggle),this.el.addEventListener("toggle.select",this.onToggleSelect)},i.prototype.componentDidUpdate=function(){this.update()},i.prototype.componentWillUnmount=function(){this.el.removeEventListener("toggle",this.onToggle),this.el.removeEventListener("toggle.select",this.onToggleSelect)},i.prototype.onToggle=function(e){this.props.onToggle&&this.props.onToggle(e)},i.prototype.onToggleSelect=function(e){this.props.onToggleSelect&&this.props.onToggleSelect(e)},i.prototype.render=function(){var n,o,e,r=this;return u.createElement("div",(n=this.props,o=i.defaultProps,void 0===(e={ref:function(e){return r.el=e}})&&(e={}),Object.keys(n).reduce(function(e,t){return o.hasOwnProperty(t)||(e[t]=n[t]),e},e)),u.Children.map(this.props.children,function(e,t){return 0===t?u.cloneElement(e,{"aria-expanded":String(Boolean(r.props.open))}):1===t?u.cloneElement(e,{hidden:!r.props.open}):e}))},Object.defineProperties(i,e),i}(u.Component);return h.propTypes={open:e.bool,popup:e.oneOfType([e.bool,e.string]),onToggle:e.func},h}); | ||
(function (global, factory) { | ||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('react')) : | ||
typeof define === 'function' && define.amd ? define(['react'], factory) : | ||
(global = global || self, global.CoreToggle = factory(global.React)); | ||
}(this, function (React) { 'use strict'; | ||
React = React && React.hasOwnProperty('default') ? React['default'] : React; | ||
function _classCallCheck(instance, Constructor) { | ||
if (!(instance instanceof Constructor)) { | ||
throw new TypeError("Cannot call a class as a function"); | ||
} | ||
} | ||
function _defineProperties(target, props) { | ||
for (var i = 0; i < props.length; i++) { | ||
var descriptor = props[i]; | ||
descriptor.enumerable = descriptor.enumerable || false; | ||
descriptor.configurable = true; | ||
if ("value" in descriptor) descriptor.writable = true; | ||
Object.defineProperty(target, descriptor.key, descriptor); | ||
} | ||
} | ||
function _createClass(Constructor, protoProps, staticProps) { | ||
if (protoProps) _defineProperties(Constructor.prototype, protoProps); | ||
if (staticProps) _defineProperties(Constructor, staticProps); | ||
return Constructor; | ||
} | ||
function _inherits(subClass, superClass) { | ||
if (typeof superClass !== "function" && superClass !== null) { | ||
throw new TypeError("Super expression must either be null or a function"); | ||
} | ||
subClass.prototype = Object.create(superClass && superClass.prototype, { | ||
constructor: { | ||
value: subClass, | ||
writable: true, | ||
configurable: true | ||
} | ||
}); | ||
if (superClass) _setPrototypeOf(subClass, superClass); | ||
} | ||
function _getPrototypeOf(o) { | ||
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { | ||
return o.__proto__ || Object.getPrototypeOf(o); | ||
}; | ||
return _getPrototypeOf(o); | ||
} | ||
function _setPrototypeOf(o, p) { | ||
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { | ||
o.__proto__ = p; | ||
return o; | ||
}; | ||
return _setPrototypeOf(o, p); | ||
} | ||
function isNativeReflectConstruct() { | ||
if (typeof Reflect === "undefined" || !Reflect.construct) return false; | ||
if (Reflect.construct.sham) return false; | ||
if (typeof Proxy === "function") return true; | ||
try { | ||
Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); | ||
return true; | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
function _construct(Parent, args, Class) { | ||
if (isNativeReflectConstruct()) { | ||
_construct = Reflect.construct; | ||
} else { | ||
_construct = function _construct(Parent, args, Class) { | ||
var a = [null]; | ||
a.push.apply(a, args); | ||
var Constructor = Function.bind.apply(Parent, a); | ||
var instance = new Constructor(); | ||
if (Class) _setPrototypeOf(instance, Class.prototype); | ||
return instance; | ||
}; | ||
} | ||
return _construct.apply(null, arguments); | ||
} | ||
function _isNativeFunction(fn) { | ||
return Function.toString.call(fn).indexOf("[native code]") !== -1; | ||
} | ||
function _wrapNativeSuper(Class) { | ||
var _cache = typeof Map === "function" ? new Map() : undefined; | ||
_wrapNativeSuper = function _wrapNativeSuper(Class) { | ||
if (Class === null || !_isNativeFunction(Class)) return Class; | ||
if (typeof Class !== "function") { | ||
throw new TypeError("Super expression must either be null or a function"); | ||
} | ||
if (typeof _cache !== "undefined") { | ||
if (_cache.has(Class)) return _cache.get(Class); | ||
_cache.set(Class, Wrapper); | ||
} | ||
function Wrapper() { | ||
return _construct(Class, arguments, _getPrototypeOf(this).constructor); | ||
} | ||
Wrapper.prototype = Object.create(Class.prototype, { | ||
constructor: { | ||
value: Wrapper, | ||
enumerable: false, | ||
writable: true, | ||
configurable: true | ||
} | ||
}); | ||
return _setPrototypeOf(Wrapper, Class); | ||
}; | ||
return _wrapNativeSuper(Class); | ||
} | ||
function _assertThisInitialized(self) { | ||
if (self === void 0) { | ||
throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); | ||
} | ||
return self; | ||
} | ||
function _possibleConstructorReturn(self, call) { | ||
if (call && (typeof call === "object" || typeof call === "function")) { | ||
return call; | ||
} | ||
return _assertThisInitialized(self); | ||
} | ||
var IS_BROWSER = typeof window !== 'undefined'; | ||
var IS_ANDROID = IS_BROWSER && /(android)/i.test(navigator.userAgent); // Bad, but needed | ||
var IS_IOS = IS_BROWSER && /iPad|iPhone|iPod/.test(String(navigator.platform)); // Polyfill toggleAttribute for IE | ||
if (IS_BROWSER && !window.Element.prototype.toggleAttribute) { | ||
window.Element.prototype.toggleAttribute = function (name) { | ||
var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : !this.hasAttribute(name); | ||
if (!force === this.hasAttribute(name)) this[force ? 'setAttribute' : 'removeAttribute'](name, ''); | ||
return force; | ||
}; | ||
} | ||
/** | ||
* closest | ||
* @param {Element} element Element to traverse up from | ||
* @param {String} selector A selector to search for matching parents or element itself | ||
* @return {Element|null} Element which is the closest ancestor matching selector | ||
*/ | ||
var closest = function () { | ||
var proto = typeof window === 'undefined' ? {} : window.Element.prototype; | ||
var match = proto.matches || proto.msMatchesSelector || proto.webkitMatchesSelector; | ||
return proto.closest ? function (el, css) { | ||
return el.closest(css); | ||
} : function (el, css) { | ||
for (; el; el = el.parentElement) { | ||
if (match.call(el, css)) return el; | ||
} | ||
return null; | ||
}; | ||
}(); | ||
/** | ||
* dispatchEvent - with infinite loop prevention | ||
* @param {Element} elem The target object | ||
* @param {String} name The source object(s) | ||
* @param {Object} detail Detail object (bubbles and cancelable is set to true) | ||
* @return {Boolean} Whether the event was canceled | ||
*/ | ||
function dispatchEvent(element, name) { | ||
var detail = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; | ||
var ignore = "prevent_recursive_dispatch_maximum_callstack".concat(name); | ||
var event; | ||
if (element[ignore]) return true; // We are already processing this event, so skip sending a new one | ||
else element[ignore] = true; // Add name to dispatching ignore | ||
if (typeof window.CustomEvent === 'function') { | ||
event = new window.CustomEvent(name, { | ||
bubbles: true, | ||
cancelable: true, | ||
detail: detail | ||
}); | ||
} else { | ||
event = document.createEvent('CustomEvent'); | ||
event.initCustomEvent(name, true, true, detail); | ||
} // IE reports incorrect event.defaultPrevented | ||
// but correct return value on element.dispatchEvent | ||
var result = element.dispatchEvent(event); | ||
element[ignore] = null; // Remove name from dispatching ignore | ||
return result; // Follow W3C standard for return value | ||
} | ||
function elementToReact(elementClass) { | ||
for (var _len = arguments.length, attr = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
attr[_key - 1] = arguments[_key]; | ||
} | ||
var name = elementClass.name || String(elementClass).match(/function ([^(]+)/)[1]; // String match for IE11 | ||
var tag = "".concat(name.replace(/\W+/, '-'), "-").concat(getUUID()).toLowerCase(); | ||
if (IS_BROWSER && !window.customElements.get(tag)) window.customElements.define(tag, elementClass); | ||
return ( | ||
/*#__PURE__*/ | ||
function (_React$Component) { | ||
_inherits(_class, _React$Component); | ||
function _class(props) { | ||
var _this; | ||
_classCallCheck(this, _class); | ||
_this = _possibleConstructorReturn(this, _getPrototypeOf(_class).call(this, props)); | ||
_this.ref = function (el) { | ||
return _this.el = el; | ||
}; | ||
attr.forEach(function (k) { | ||
var on = "on".concat(k.replace(/(^|\.)./g, function (m) { | ||
return m.slice(-1).toUpperCase(); | ||
})); // input.filter => onInputFilter | ||
_this[k] = function (event) { | ||
return _this.props[on] && _this.props[on](event); | ||
}; | ||
}); | ||
return _this; | ||
} | ||
_createClass(_class, [{ | ||
key: "componentDidMount", | ||
value: function componentDidMount() { | ||
var _this2 = this; | ||
attr.forEach(function (k) { | ||
return _this2.props[k] ? _this2.el[k] = _this2.props[k] : _this2.el.addEventListener(k, _this2[k]); | ||
}); | ||
} | ||
}, { | ||
key: "componentDidUpdate", | ||
value: function componentDidUpdate(prev) { | ||
var _this3 = this; | ||
attr.forEach(function (k) { | ||
return prev[k] !== _this3.props[k] && (_this3.el[k] = _this3.props[k]); | ||
}); | ||
} | ||
}, { | ||
key: "componentWillUnmount", | ||
value: function componentWillUnmount() { | ||
var _this4 = this; | ||
attr.forEach(function (k) { | ||
return _this4.el.removeEventListener(k, _this4[k]); | ||
}); | ||
} | ||
}, { | ||
key: "render", | ||
value: function render() { | ||
var _this5 = this; | ||
// Convert React props to CustomElement props https://github.com/facebook/react/issues/12810 | ||
return React.createElement(tag, Object.keys(this.props).reduce(function (props, k) { | ||
if (k === 'className') props["class"] = _this5.props[k]; // Fixes className for custom elements | ||
else if (_this5.props[k] === true) props[k] = ''; // Fixes boolean attributes | ||
else if (_this5.props[k] !== false) props[k] = _this5.props[k]; | ||
return props; | ||
}, { | ||
ref: this.ref | ||
})); | ||
} | ||
}]); | ||
return _class; | ||
}(React.Component) | ||
); | ||
} | ||
/** | ||
* getUUID | ||
* @return {String} A generated unique ID | ||
*/ | ||
function getUUID() { | ||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 5); | ||
} | ||
var CoreToggle = | ||
/*#__PURE__*/ | ||
function (_HTMLElement) { | ||
_inherits(CoreToggle, _HTMLElement); | ||
function CoreToggle() { | ||
_classCallCheck(this, CoreToggle); | ||
return _possibleConstructorReturn(this, _getPrototypeOf(CoreToggle).apply(this, arguments)); | ||
} | ||
_createClass(CoreToggle, [{ | ||
key: "connectedCallback", | ||
value: function connectedCallback() { | ||
if (IS_IOS) document.documentElement.style.cursor = 'pointer'; // Fix iOS events for closing popups (https://stackoverflow.com/a/16006333/8819615) | ||
if (!IS_ANDROID) this.setAttribute('aria-labelledby', this.button.id = this.button.id || getUUID()); // Andriod reads only label instead of content | ||
this.value = this.button.textContent; // Set up aria-label | ||
this.setAttribute('role', 'group'); // Help Edge | ||
this.button.setAttribute('aria-expanded', this._open = !this.hidden); | ||
this.button.setAttribute('aria-controls', this.id = this.id || getUUID()); | ||
document.addEventListener('keydown', this, true); // Use capture to enable checking defaultPrevented (from ESC key) in parents | ||
document.addEventListener('click', this); | ||
} | ||
}, { | ||
key: "disconnectedCallback", | ||
value: function disconnectedCallback() { | ||
this._button = null; | ||
document.removeEventListener('keydown', this, true); | ||
document.removeEventListener('click', this); | ||
} | ||
}, { | ||
key: "attributeChangedCallback", | ||
value: function attributeChangedCallback() { | ||
if (this._open === this.hidden) { | ||
// this._open comparison ensures actual change | ||
this.button.setAttribute('aria-expanded', this._open = !this.hidden); | ||
try { | ||
this.querySelector('[autofocus]').focus(); | ||
} catch (err) {} | ||
dispatchEvent(this, 'toggle'); | ||
} | ||
} | ||
}, { | ||
key: "handleEvent", | ||
value: function handleEvent(event) { | ||
if (event.defaultPrevented) return; | ||
if (event.type === 'keydown' && event.keyCode === 27) { | ||
var isButton = event.target.getAttribute && event.target.getAttribute('aria-expanded') === 'true'; | ||
var isHiding = isButton ? event.target === this.button : closest(event.target, this.nodeName) === this; | ||
if (isHiding) { | ||
this.hidden = true; | ||
this.button.focus(); // Move focus back to button | ||
return event.preventDefault(); // Prevent closing maximized Safari and other coreToggles | ||
} | ||
} | ||
if (event.type === 'click') { | ||
var btn = closest(event.target, 'a,button'); | ||
if (btn && !btn.hasAttribute('aria-expanded') && closest(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 | ||
} | ||
} | ||
}, { | ||
key: "button", | ||
get: function get() { | ||
if (this._button && this._button.getAttribute('for') === this.id) return this._button; // Speed up | ||
return (this._button = this.id && document.querySelector("[for=\"".concat(this.id, "\"]"))) || this.previousElementSibling; | ||
} // aria-haspopup triggers forms mode in JAWS, therefore store as custom attr | ||
}, { | ||
key: "popup", | ||
get: function get() { | ||
return this.getAttribute('popup') === 'true' || this.getAttribute('popup') || this.hasAttribute('popup'); | ||
}, | ||
set: function set(val) { | ||
this[val === false ? 'removeAttribute' : 'setAttribute']('popup', val); | ||
} // Must set attribute for IE11 | ||
}, { | ||
key: "hidden", | ||
get: function get() { | ||
return this.hasAttribute('hidden'); | ||
}, | ||
set: function set(val) { | ||
this.toggleAttribute('hidden', val); | ||
} // Sets this.button aria-label, so visible button text can be augmentet with intension of button | ||
// Example: Button text: "01.02.2019", aria-label: "01.02.2019, Choose date" | ||
// Does not updates aria-label if not allready set to something else than this.popup | ||
}, { | ||
key: "value", | ||
get: function get() { | ||
return this.button.value || this.button.textContent; | ||
}, | ||
set: function set() { | ||
var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; | ||
if (!this.button || !this.popup.length) return; | ||
var button = this.button; | ||
var popup = (button.getAttribute('aria-label') || ",".concat(this.popup)).split(',')[1]; | ||
var label = data.textContent || data || ''; // data can be Element, Object or String | ||
if (popup === this.popup) { | ||
button.value = data.value || label; | ||
button[data.innerHTML ? 'innerHTML' : 'textContent'] = data.innerHTML || label; | ||
button.setAttribute('aria-label', "".concat(button.textContent, ",").concat(this.popup)); | ||
} | ||
} | ||
}], [{ | ||
key: "observedAttributes", | ||
get: function get() { | ||
return ['hidden']; | ||
} | ||
}]); | ||
return CoreToggle; | ||
}(_wrapNativeSuper(HTMLElement)); | ||
var coreToggle = elementToReact(CoreToggle, 'toggle', 'toggle.select'); | ||
return coreToggle; | ||
})); | ||
//# sourceMappingURL=core-toggle.jsx.js.map |
@@ -1,3 +0,3 @@ | ||
/*! @nrk/core-toggle v2.2.2 - Copyright (c) 2017-2019 NRK */ | ||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).coreToggle=t()}(this,function(){"use strict";var e="undefined"!=typeof window,t=e&&/(android)/i.test(navigator.userAgent),n=e&&/iPad|iPhone|iPod/.test(String(navigator.platform)),o=function(e){void 0===e&&(e=!1);try{window.addEventListener("test",null,{get passive(){e=!0}})}catch(e){}return e}();function r(e,t,n,r){(void 0===r&&(r=!1),"undefined"==typeof window||window[e=e+"-"+t])||(o||"object"!=typeof r||(r=Boolean(r.capture)),("resize"===t||"load"===t?window:document).addEventListener(window[e]=t,n,r))}var a="prevent_recursive_dispatch_maximum_callstack";function u(e,t,n){void 0===n&&(n={});var r,o=""+a+t;if(e[o])return!0;e[o]=!0,"function"==typeof window.CustomEvent?r=new window.CustomEvent(t,{bubbles:!0,cancelable:!0,detail:n}):(r=document.createEvent("CustomEvent")).initCustomEvent(t,!0,!0,n);var i=e.dispatchEvent(r);return e[o]=null,i}function d(e){return Date.now().toString(36)+Math.random().toString(36).slice(2,5)}function l(e,t){if(void 0===t&&(t=document),e){if(e.nodeType)return[e];if("string"==typeof e)return[].slice.call(t.querySelectorAll(e));if(e.length)return[].slice.call(e)}return[]}var c="data-@nrk/core-toggle-2.2.2".replace(/\W+/g,"-"),f=t?"data":"aria",i=27;function s(e){return document.getElementById(e.getAttribute("aria-controls"))||e.nextElementSibling}function p(e,t){var n=s(e),r="true"===e.getAttribute("aria-expanded"),o="boolean"==typeof t?t:"toggle"===t?!r:r,i=r===o||u(e,"toggle",{relatedTarget:n,isOpen:r,willOpen:o})?o:"true"===e.getAttribute("aria-expanded"),a=!r&&i&&n.querySelector("[autofocus]");a&&setTimeout(function(){return a&&a.focus()}),e.setAttribute("aria-expanded",i),n[i?"removeAttribute":"setAttribute"]("hidden","")}return r(c,"keydown",function(e){if(e.keyCode===i)for(var t=e.target;t;t=t.parentElement){var n=t.id&&document.querySelector('[aria-controls="'+t.id+'"]')||t;if("false"!==n.getAttribute(c)&&"true"===n.getAttribute("aria-expanded"))return e.preventDefault(),n.focus(),p(n,!1)}},!0),r(c,"click",function(e){var o=e.target;if(e.defaultPrevented)return!1;for(var t=o,n=void 0;t;t=t.parentElement){var r=n&&t.id&&document.querySelector("["+c+'][aria-controls="'+t.id+'"]');if("BUTTON"!==t.nodeName&&"A"!==t.nodeName||t.hasAttribute(c)||(n=t),r){u(r,"toggle.select",{relatedTarget:s(r),currentTarget:n,value:n.textContent.trim()});break}}l("["+c+"]").forEach(function(e){var t="true"===e.getAttribute("aria-expanded"),n="false"!==e.getAttribute(c),r=s(e);e.contains(o)?p(e,!t):n&&t&&p(e,r.contains(o))})}),function(e,t){var i="object"==typeof t?t:{open:t};return n&&(document.documentElement.style.cursor="pointer"),l(e).map(function(e){var t=s(e),n="true"===e.getAttribute("aria-expanded"),r="boolean"==typeof i.open?i.open:"toggle"===i.open?!n:n,o=String((i.hasOwnProperty("popup")?i.popup:e.getAttribute(c))||!1);return i.value&&(e.innerHTML=i.value),"false"!==o&&"true"!==o&&e.setAttribute("aria-label",e.textContent+", "+o),e.setAttribute(c,o),e.setAttribute("aria-controls",t.id=t.id||d()),t.setAttribute(f+"-labelledby",e.id=e.id||d()),p(e,r),e})}}); | ||
/*! @nrk/core-toggle v3.0.0 - Copyright (c) 2017-2019 NRK */ | ||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).coreToggle=e()}(this,function(){"use strict";function r(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(t,i.key,i)}}function u(t){return(u=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}function s(t,e){return(s=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function o(t,e,n){return(o=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(t){return!1}}()?Reflect.construct:function(t,e,n){var i=[null];i.push.apply(i,e);var o=new(Function.bind.apply(t,i));return n&&s(o,n.prototype),o}).apply(null,arguments)}function c(t){var i="function"==typeof Map?new Map:void 0;return(c=function(t){if(null===t||(e=t,-1===Function.toString.call(e).indexOf("[native code]")))return t;var e;if("function"!=typeof t)throw new TypeError("Super expression must either be null or a function");if(void 0!==i){if(i.has(t))return i.get(t);i.set(t,n)}function n(){return o(t,arguments,u(this).constructor)}return n.prototype=Object.create(t.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),s(n,t)})(t)}function a(t,e){return!e||"object"!=typeof e&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}var t="undefined"!=typeof window,l=t&&/(android)/i.test(navigator.userAgent),f=t&&/iPad|iPhone|iPod/.test(String(navigator.platform));t&&!window.Element.prototype.toggleAttribute&&(window.Element.prototype.toggleAttribute=function(t){var e=1<arguments.length&&void 0!==arguments[1]?arguments[1]:!this.hasAttribute(t);return!e===this.hasAttribute(t)&&this[e?"setAttribute":"removeAttribute"](t,""),e});var e,n,h=(e="undefined"==typeof window?{}:window.Element.prototype,n=e.matches||e.msMatchesSelector||e.webkitMatchesSelector,e.closest?function(t,e){return t.closest(e)}:function(t,e){for(;t;t=t.parentElement)if(n.call(t,e))return t;return null});function p(t,e){var n,i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{},o="prevent_recursive_dispatch_maximum_callstack".concat(e);if(t[o])return!0;t[o]=!0,"function"==typeof window.CustomEvent?n=new window.CustomEvent(e,{bubbles:!0,cancelable:!0,detail:i}):(n=document.createEvent("CustomEvent")).initCustomEvent(e,!0,!0,i);var r=t.dispatchEvent(n);return t[o]=null,r}function d(){return Date.now().toString(36)+Math.random().toString(36).slice(2,5)}return function(t){function e(){return function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,e),a(this,u(e).apply(this,arguments))}var n,i,o;return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&s(t,e)}(e,c(HTMLElement)),n=e,o=[{key:"observedAttributes",get:function(){return["hidden"]}}],(i=[{key:"connectedCallback",value:function(){f&&(document.documentElement.style.cursor="pointer"),l||this.setAttribute("aria-labelledby",this.button.id=this.button.id||d()),this.value=this.button.textContent,this.setAttribute("role","group"),this.button.setAttribute("aria-expanded",this._open=!this.hidden),this.button.setAttribute("aria-controls",this.id=this.id||d()),document.addEventListener("keydown",this,!0),document.addEventListener("click",this)}},{key:"disconnectedCallback",value:function(){this._button=null,document.removeEventListener("keydown",this,!0),document.removeEventListener("click",this)}},{key:"attributeChangedCallback",value:function(){if(this._open===this.hidden){this.button.setAttribute("aria-expanded",this._open=!this.hidden);try{this.querySelector("[autofocus]").focus()}catch(t){}p(this,"toggle")}}},{key:"handleEvent",value:function(t){if(!t.defaultPrevented){if("keydown"===t.type&&27===t.keyCode)if(t.target.getAttribute&&"true"===t.target.getAttribute("aria-expanded")?t.target===this.button:h(t.target,this.nodeName)===this)return this.hidden=!0,this.button.focus(),t.preventDefault();if("click"===t.type){var e=h(t.target,"a,button");e&&!e.hasAttribute("aria-expanded")&&h(t.target,this.nodeName)===this?p(this,"toggle.select",e):e&&e.getAttribute("aria-controls")===this.id?this.hidden=!this.hidden:this.popup&&!this.contains(t.target)&&(this.hidden=!0)}}}},{key:"button",get:function(){return this._button&&this._button.getAttribute("for")===this.id?this._button:(this._button=this.id&&document.querySelector('[for="'.concat(this.id,'"]')))||this.previousElementSibling}},{key:"popup",get:function(){return"true"===this.getAttribute("popup")||this.getAttribute("popup")||this.hasAttribute("popup")},set:function(t){this[!1===t?"removeAttribute":"setAttribute"]("popup",t)}},{key:"hidden",get:function(){return this.hasAttribute("hidden")},set:function(t){this.toggleAttribute("hidden",t)}},{key:"value",get:function(){return this.button.value||this.button.textContent},set:function(){var t=0<arguments.length&&void 0!==arguments[0]&&arguments[0];if(this.button&&this.popup.length){var e=this.button,n=(e.getAttribute("aria-label")||",".concat(this.popup)).split(",")[1],i=t.textContent||t||"";n===this.popup&&(e.value=t.value||i,e[t.innerHTML?"innerHTML":"textContent"]=t.innerHTML||i,e.setAttribute("aria-label","".concat(e.textContent,",").concat(this.popup)))}}}])&&r(n.prototype,i),o&&r(n,o),e}()}),window.customElements.define("core-toggle",coreToggle); | ||
//# sourceMappingURL=core-toggle.min.js.map |
@@ -1,85 +0,167 @@ | ||
/* global expect, describe, it */ | ||
const { name, version } = require('./package.json') | ||
const coreToggle = require('./core-toggle.min') | ||
const UUID = `data-${name}-${version}`.replace(/\W+/g, '-') | ||
import test from 'ava' | ||
import path from 'path' | ||
import puppeteer from 'puppeteer' | ||
const standardHTML = ` | ||
<button class="my-toggle">Toggle VanillaJS</button> | ||
<div hidden>Content</div> | ||
` | ||
async function withPage (t, run) { | ||
const browser = await puppeteer.launch() | ||
const page = await browser.newPage() | ||
page.on('console', msg => console.log(msg._text)) | ||
await page.addScriptTag({ path: path.join(__dirname, 'core-toggle.min.js') }) | ||
try { | ||
await run(t, page) | ||
} finally { | ||
await page.close() | ||
await browser.close() | ||
} | ||
} | ||
describe('toggle', () => { | ||
it('should exists', () => { | ||
expect(coreToggle).toBeInstanceOf(Function) | ||
}) | ||
test('sets up all properties', withPage, async (t, page) => { | ||
await page.setContent(` | ||
<button>Toggle</button> | ||
<core-toggle hidden></core-toggle> | ||
`) | ||
t.is(await page.$eval('button', el => el.getAttribute('aria-expanded')), 'false') | ||
t.true(await page.$eval('button', el => el.getAttribute('aria-controls') === document.querySelector('core-toggle').id)) | ||
t.true(await page.$eval('core-toggle', el => el.hasAttribute('hidden'))) | ||
t.true(await page.$eval('core-toggle', el => el.getAttribute('aria-labelledby') === document.querySelector('button').id)) | ||
}) | ||
it('should initialize button and container', () => { | ||
document.body.innerHTML = standardHTML | ||
const button = document.querySelector('.my-toggle') | ||
const container = document.querySelector('.my-toggle + *') | ||
coreToggle(button) | ||
expect(button.getAttribute(UUID)).toEqual('false') | ||
expect(button.getAttribute('aria-expanded')).toEqual('false') | ||
expect(button.getAttribute('aria-controls')).toEqual(container.id) | ||
expect(container.hasAttribute('hidden')).toEqual(true) | ||
expect(container.getAttribute('aria-labelledby')).toEqual(button.id) | ||
}) | ||
test('opens and closes toggle', withPage, async (t, page) => { | ||
await page.setContent(` | ||
<button>Toggle</button> | ||
<core-toggle hidden></core-toggle> | ||
`) | ||
await page.click('button') | ||
t.is(await page.$eval('button', el => el.getAttribute('aria-expanded')), 'true') | ||
await page.click('button') | ||
t.is(await page.$eval('button', el => el.getAttribute('aria-expanded')), 'false') | ||
await page.click('button') | ||
t.is(await page.$eval('button', el => el.getAttribute('aria-expanded')), 'true') | ||
await page.click('button') | ||
t.is(await page.$eval('button', el => el.getAttribute('aria-expanded')), 'false') | ||
}) | ||
it('should open with open attribute', () => { | ||
document.body.innerHTML = standardHTML | ||
const button = document.querySelector('.my-toggle') | ||
const container = document.querySelector('.my-toggle + *') | ||
coreToggle(button, { open: true }) | ||
expect(container.hasAttribute('hidden')).toEqual(false) | ||
expect(button.getAttribute('aria-expanded')).toEqual('true') | ||
}) | ||
test('opens and closes nested toggle', withPage, async (t, page) => { | ||
await page.setContent(` | ||
<button id="outer">Toggle outer</button> | ||
<core-toggle hidden> | ||
<button id="inner">Toggle inner</button> | ||
<core-toggle hidden> | ||
<div>Inner content</div> | ||
</core-toggle> | ||
</core-toggle> | ||
`) | ||
await page.click('button#outer') | ||
await page.click('button#inner') | ||
t.false(await page.$eval('button#outer + core-toggle', el => el.hasAttribute('hidden'))) | ||
t.false(await page.$eval('button#outer + core-toggle', el => el.hasAttribute('hidden'))) | ||
await page.click('button#inner') | ||
t.true(await page.$eval('button#inner + core-toggle', el => el.hasAttribute('hidden'))) | ||
t.false(await page.$eval('button#outer + core-toggle', el => el.hasAttribute('hidden'))) | ||
await page.click('button#outer') | ||
t.true(await page.$eval('button#outer + core-toggle', el => el.hasAttribute('hidden'))) | ||
}) | ||
it('should close an opened toggle', () => { | ||
document.body.innerHTML = standardHTML | ||
const button = document.querySelector('.my-toggle') | ||
const container = document.querySelector('.my-toggle + *') | ||
coreToggle(button, { open: true }) | ||
coreToggle(button, { open: false }) | ||
expect(container.hasAttribute('hidden')).toEqual(true) | ||
}) | ||
test('closes nested toggle with esc', withPage, async (t, page) => { | ||
await page.setContent(` | ||
<button id="outer">Toggle outer</button> | ||
<core-toggle hidden> | ||
<button id="inner">Toggle inner</button> | ||
<core-toggle hidden> | ||
<div>Inner content</div> | ||
</core-toggle> | ||
</core-toggle> | ||
`) | ||
await page.click('button#outer') | ||
await page.click('button#inner') | ||
t.false(await page.$eval('button#outer + core-toggle', el => el.hasAttribute('hidden'))) | ||
t.false(await page.$eval('button#outer + core-toggle', el => el.hasAttribute('hidden'))) | ||
await page.keyboard.press('Escape') | ||
t.true(await page.$eval('button#inner + core-toggle', el => el.hasAttribute('hidden'))) | ||
t.false(await page.$eval('button#outer + core-toggle', el => el.hasAttribute('hidden'))) | ||
await page.keyboard.press('Escape') | ||
t.true(await page.$eval('button#outer + core-toggle', el => el.hasAttribute('hidden'))) | ||
}) | ||
it('should initialize as popup', () => { | ||
document.body.innerHTML = standardHTML | ||
const button = document.querySelector('.my-toggle') | ||
coreToggle(button, { popup: 'Test' }) | ||
expect(button.getAttribute(UUID)).toEqual('Test') | ||
}) | ||
test('closes on outside click with popup', withPage, async (t, page) => { | ||
await page.setContent(` | ||
<button>Toggle</button> | ||
<core-toggle popup hidden></core-toggle> | ||
`) | ||
await page.click('button') | ||
t.false(await page.$eval('core-toggle', el => el.hasAttribute('hidden'))) | ||
await page.click('body') | ||
t.true(await page.$eval('core-toggle', el => el.hasAttribute('hidden'))) | ||
}) | ||
it('should open popup with open', () => { | ||
document.body.innerHTML = standardHTML | ||
const button = document.querySelector('.my-toggle') | ||
const container = document.querySelector('.my-toggle + *') | ||
coreToggle(button, { popup: 'Tekst', open: true }) | ||
expect(button.getAttribute(UUID)).toEqual('Tekst') | ||
expect(container.hasAttribute('hidden')).toEqual(false) | ||
}) | ||
test('respects "for" attribute', withPage, async (t, page) => { | ||
await page.setContent(` | ||
<div><button for="content">Toggle</button></div> | ||
<core-toggle id="content" hidden></core-toggle> | ||
`) | ||
t.true(await page.$eval('core-toggle', el => el.button.getAttribute('for') === el.id)) | ||
t.true(await page.$eval('core-toggle', el => el.button.getAttribute('aria-controls') === el.id)) | ||
}) | ||
it('should close popup', () => { | ||
document.body.innerHTML = standardHTML | ||
const button = document.querySelector('.my-toggle') | ||
const container = document.querySelector('.my-toggle + *') | ||
coreToggle(button, { popup: 'Tekst', open: true }) | ||
coreToggle(button, { open: false }) | ||
expect(container.hasAttribute('hidden')).toEqual(true) | ||
test('respects exisiting aria-label with popup and value', withPage, async (t, page) => { | ||
await page.setContent(` | ||
<button aria-label="Label">Toggle</button> | ||
<core-toggle popup="Another label" hidden></core-toggle> | ||
`) | ||
await page.$eval('core-toggle', el => (el.value = 'Button text')) | ||
t.is(await page.$eval('button', el => el.textContent), await page.$eval('core-toggle', el => el.value)) | ||
t.is(await page.$eval('button', el => el.getAttribute('aria-label')), 'Label') | ||
}) | ||
test('sets aria-label with popup attr and value', withPage, async (t, page) => { | ||
await page.setContent(` | ||
<button>Toggle</button> | ||
<core-toggle popup="Some label" hidden></core-toggle> | ||
`) | ||
await page.$eval('core-toggle', el => (el.value = 'Button text')) | ||
t.is(await page.$eval('button', el => el.textContent), await page.$eval('core-toggle', el => el.value)) | ||
t.is(await page.$eval('button', el => el.getAttribute('aria-label')), 'Button text,Some label') | ||
}) | ||
test('sets aria-label with popup prop and value', withPage, async (t, page) => { | ||
await page.setContent(` | ||
<button>Toggle</button> | ||
<core-toggle hidden></core-toggle> | ||
`) | ||
await page.$eval('core-toggle', el => (el.popup = 'Some label')) | ||
await page.$eval('core-toggle', el => (el.value = 'Button text')) | ||
t.is(await page.$eval('button', el => el.textContent), await page.$eval('core-toggle', el => el.value)) | ||
t.is(await page.$eval('button', el => el.getAttribute('aria-label')), 'Button text,Some label') | ||
}) | ||
test('triggers toggle event', withPage, async (t, page) => { | ||
await page.setContent(` | ||
<button>Toggle</button> | ||
<core-toggle hidden></core-toggle> | ||
`) | ||
await page.evaluate(() => { | ||
return new Promise((resolve, reject) => { | ||
window.addEventListener('toggle', resolve) | ||
document.querySelector('core-toggle').hidden = false | ||
}) | ||
}) | ||
t.pass() | ||
}) | ||
it('should respect existing aria-controls', () => { | ||
document.body.innerHTML = ` | ||
<div><button class="my-toggle" aria-controls="content">Toggle VanillaJS</button></div> | ||
<div id="content" hidden>Content</div>` | ||
const button = document.querySelector('.my-toggle') | ||
const container = document.querySelector('#content') | ||
coreToggle(button, { open: false }) | ||
expect(container.hasAttribute('hidden')).toEqual(true) | ||
coreToggle(button, { open: true }) | ||
expect(container.hasAttribute('hidden')).toEqual(false) | ||
expect(button.getAttribute('aria-expanded')).toEqual('true') | ||
expect(button.getAttribute('aria-controls')).toEqual(container.id) | ||
expect(container.getAttribute('aria-labelledby')).toEqual(button.id) | ||
test('triggers select event', withPage, async (t, page) => { | ||
await page.setContent(` | ||
<button>Toggle</button> | ||
<core-toggle hidden> | ||
<button id="item">Select me</button> | ||
</core-toggle> | ||
`) | ||
const selected = await page.evaluate(() => { | ||
return new Promise((resolve, reject) => { | ||
window.addEventListener('toggle.select', ({ detail }) => resolve(detail.id)) | ||
const toggle = document.querySelector('core-toggle') | ||
toggle.hidden = false | ||
toggle.children[0].click() | ||
}) | ||
}) | ||
t.is(selected, 'item') | ||
}) |
553
jsx.js
@@ -6,48 +6,172 @@ 'use strict'; | ||
var React = _interopDefault(require('react')); | ||
var PropTypes = _interopDefault(require('prop-types')); | ||
var name = "@nrk/core-toggle"; | ||
var version = "2.2.2"; | ||
function _classCallCheck(instance, Constructor) { | ||
if (!(instance instanceof Constructor)) { | ||
throw new TypeError("Cannot call a class as a function"); | ||
} | ||
} | ||
function _defineProperties(target, props) { | ||
for (var i = 0; i < props.length; i++) { | ||
var descriptor = props[i]; | ||
descriptor.enumerable = descriptor.enumerable || false; | ||
descriptor.configurable = true; | ||
if ("value" in descriptor) descriptor.writable = true; | ||
Object.defineProperty(target, descriptor.key, descriptor); | ||
} | ||
} | ||
function _createClass(Constructor, protoProps, staticProps) { | ||
if (protoProps) _defineProperties(Constructor.prototype, protoProps); | ||
if (staticProps) _defineProperties(Constructor, staticProps); | ||
return Constructor; | ||
} | ||
function _inherits(subClass, superClass) { | ||
if (typeof superClass !== "function" && superClass !== null) { | ||
throw new TypeError("Super expression must either be null or a function"); | ||
} | ||
subClass.prototype = Object.create(superClass && superClass.prototype, { | ||
constructor: { | ||
value: subClass, | ||
writable: true, | ||
configurable: true | ||
} | ||
}); | ||
if (superClass) _setPrototypeOf(subClass, superClass); | ||
} | ||
function _getPrototypeOf(o) { | ||
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { | ||
return o.__proto__ || Object.getPrototypeOf(o); | ||
}; | ||
return _getPrototypeOf(o); | ||
} | ||
function _setPrototypeOf(o, p) { | ||
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { | ||
o.__proto__ = p; | ||
return o; | ||
}; | ||
return _setPrototypeOf(o, p); | ||
} | ||
function isNativeReflectConstruct() { | ||
if (typeof Reflect === "undefined" || !Reflect.construct) return false; | ||
if (Reflect.construct.sham) return false; | ||
if (typeof Proxy === "function") return true; | ||
try { | ||
Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); | ||
return true; | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
function _construct(Parent, args, Class) { | ||
if (isNativeReflectConstruct()) { | ||
_construct = Reflect.construct; | ||
} else { | ||
_construct = function _construct(Parent, args, Class) { | ||
var a = [null]; | ||
a.push.apply(a, args); | ||
var Constructor = Function.bind.apply(Parent, a); | ||
var instance = new Constructor(); | ||
if (Class) _setPrototypeOf(instance, Class.prototype); | ||
return instance; | ||
}; | ||
} | ||
return _construct.apply(null, arguments); | ||
} | ||
function _isNativeFunction(fn) { | ||
return Function.toString.call(fn).indexOf("[native code]") !== -1; | ||
} | ||
function _wrapNativeSuper(Class) { | ||
var _cache = typeof Map === "function" ? new Map() : undefined; | ||
_wrapNativeSuper = function _wrapNativeSuper(Class) { | ||
if (Class === null || !_isNativeFunction(Class)) return Class; | ||
if (typeof Class !== "function") { | ||
throw new TypeError("Super expression must either be null or a function"); | ||
} | ||
if (typeof _cache !== "undefined") { | ||
if (_cache.has(Class)) return _cache.get(Class); | ||
_cache.set(Class, Wrapper); | ||
} | ||
function Wrapper() { | ||
return _construct(Class, arguments, _getPrototypeOf(this).constructor); | ||
} | ||
Wrapper.prototype = Object.create(Class.prototype, { | ||
constructor: { | ||
value: Wrapper, | ||
enumerable: false, | ||
writable: true, | ||
configurable: true | ||
} | ||
}); | ||
return _setPrototypeOf(Wrapper, Class); | ||
}; | ||
return _wrapNativeSuper(Class); | ||
} | ||
function _assertThisInitialized(self) { | ||
if (self === void 0) { | ||
throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); | ||
} | ||
return self; | ||
} | ||
function _possibleConstructorReturn(self, call) { | ||
if (call && (typeof call === "object" || typeof call === "function")) { | ||
return call; | ||
} | ||
return _assertThisInitialized(self); | ||
} | ||
var IS_BROWSER = typeof window !== 'undefined'; | ||
var IS_ANDROID = IS_BROWSER && /(android)/i.test(navigator.userAgent); // Bad, but needed | ||
var IS_IOS = IS_BROWSER && /iPad|iPhone|iPod/.test(String(navigator.platform)); | ||
var HAS_EVENT_OPTIONS = (function (has) { | ||
if ( has === void 0 ) has = false; | ||
try { window.addEventListener('test', null, { get passive () { has = true; } }); } catch (e) {} | ||
return has | ||
})(); | ||
var IS_IOS = IS_BROWSER && /iPad|iPhone|iPod/.test(String(navigator.platform)); // Polyfill toggleAttribute for IE | ||
/** | ||
* addEvent | ||
* @param {String} uuid An unique ID of the event to bind - ensurnes single instance | ||
* @param {String} type The type of event to bind | ||
* @param {Function} handler The function to call on event | ||
* @param {Boolean|Object} options useCapture or options object for addEventListener. Defaults to false | ||
*/ | ||
function addEvent (uuid, type, handler, options) { | ||
if ( options === void 0 ) options = false; | ||
if (typeof window === 'undefined' || window[uuid = uuid + "-" + type]) { return } // Ensure single instance | ||
if (!HAS_EVENT_OPTIONS && typeof options === 'object') { options = Boolean(options.capture); } // Fix unsupported options | ||
var node = (type === 'resize' || type === 'load') ? window : document; | ||
node.addEventListener(window[uuid] = type, handler, options); | ||
if (IS_BROWSER && !window.Element.prototype.toggleAttribute) { | ||
window.Element.prototype.toggleAttribute = function (name) { | ||
var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : !this.hasAttribute(name); | ||
if (!force === this.hasAttribute(name)) this[force ? 'setAttribute' : 'removeAttribute'](name, ''); | ||
return force; | ||
}; | ||
} | ||
/** | ||
* exclude | ||
* @param {Object} target The target object | ||
* @param {Object} exclude The source to exclude keys from | ||
* @return {Object} The target object without keys found in source | ||
* closest | ||
* @param {Element} element Element to traverse up from | ||
* @param {String} selector A selector to search for matching parents or element itself | ||
* @return {Element|null} Element which is the closest ancestor matching selector | ||
*/ | ||
function exclude (target, exclude, include) { | ||
if ( include === void 0 ) include = {}; | ||
return Object.keys(target).reduce(function (acc, key) { | ||
if (!exclude.hasOwnProperty(key)) { acc[key] = target[key]; } | ||
return acc | ||
}, include) | ||
} | ||
var closest = function () { | ||
var proto = typeof window === 'undefined' ? {} : window.Element.prototype; | ||
var match = proto.matches || proto.msMatchesSelector || proto.webkitMatchesSelector; | ||
return proto.closest ? function (el, css) { | ||
return el.closest(css); | ||
} : function (el, css) { | ||
for (; el; el = el.parentElement) { | ||
if (match.call(el, css)) return el; | ||
} | ||
return null; | ||
}; | ||
}(); | ||
/** | ||
@@ -60,198 +184,251 @@ * dispatchEvent - with infinite loop prevention | ||
*/ | ||
var IGNORE = 'prevent_recursive_dispatch_maximum_callstack'; | ||
function dispatchEvent (element, name, detail) { | ||
if ( detail === void 0 ) detail = {}; | ||
var ignore = "" + IGNORE + name; | ||
function dispatchEvent(element, name) { | ||
var detail = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; | ||
var ignore = "prevent_recursive_dispatch_maximum_callstack".concat(name); | ||
var event; | ||
if (element[ignore]) return true; // We are already processing this event, so skip sending a new one | ||
else element[ignore] = true; // Add name to dispatching ignore | ||
if (element[ignore]) { return true } // We are already processing this event, so skip sending a new one | ||
else { element[ignore] = true; } // Add name to dispatching ignore | ||
if (typeof window.CustomEvent === 'function') { | ||
event = new window.CustomEvent(name, { bubbles: true, cancelable: true, detail: detail }); | ||
event = new window.CustomEvent(name, { | ||
bubbles: true, | ||
cancelable: true, | ||
detail: detail | ||
}); | ||
} else { | ||
event = document.createEvent('CustomEvent'); | ||
event.initCustomEvent(name, true, true, detail); | ||
} | ||
// IE reports incorrect event.defaultPrevented | ||
} // IE reports incorrect event.defaultPrevented | ||
// but correct return value on element.dispatchEvent | ||
var result = element.dispatchEvent(event); | ||
element[ignore] = null; // Remove name from dispatching ignore | ||
return result // Follow W3C standard for return value | ||
return result; // Follow W3C standard for return value | ||
} | ||
function elementToReact(elementClass) { | ||
for (var _len = arguments.length, attr = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
attr[_key - 1] = arguments[_key]; | ||
} | ||
/** | ||
* getUUID | ||
* @return {String} A generated unique ID | ||
*/ | ||
function getUUID (el) { | ||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 5) | ||
} | ||
var name = elementClass.name || String(elementClass).match(/function ([^(]+)/)[1]; // String match for IE11 | ||
/** | ||
* queryAll | ||
* @param {String|NodeList|Array|Element} elements A CSS selector string, nodeList, element array, or single element | ||
* @return {Element[]} Array of elements | ||
*/ | ||
function queryAll (elements, context) { | ||
if ( context === void 0 ) context = document; | ||
var tag = "".concat(name.replace(/\W+/, '-'), "-").concat(getUUID()).toLowerCase(); | ||
if (IS_BROWSER && !window.customElements.get(tag)) window.customElements.define(tag, elementClass); | ||
return ( | ||
/*#__PURE__*/ | ||
function (_React$Component) { | ||
_inherits(_class, _React$Component); | ||
if (elements) { | ||
if (elements.nodeType) { return [elements] } | ||
if (typeof elements === 'string') { return [].slice.call(context.querySelectorAll(elements)) } | ||
if (elements.length) { return [].slice.call(elements) } | ||
} | ||
return [] | ||
} | ||
function _class(props) { | ||
var _this; | ||
var UUID = ("data-" + name + "-" + version).replace(/\W+/g, '-'); // Strip invalid attribute characters | ||
var ARIA = IS_ANDROID ? 'data' : 'aria'; // Andriod has a bug and reads only label instead of content | ||
var KEYS = { ESC: 27 }; | ||
_classCallCheck(this, _class); | ||
function toggle (toggles, open) { | ||
var options = typeof open === 'object' ? open : { open: open }; | ||
if (IS_IOS) { document.documentElement.style.cursor = 'pointer'; } // Fix iOS events for closing popups (https://stackoverflow.com/a/16006333/8819615) | ||
_this = _possibleConstructorReturn(this, _getPrototypeOf(_class).call(this, props)); | ||
return queryAll(toggles).map(function (toggle) { | ||
var content = getContentElement(toggle); | ||
var isOpen = toggle.getAttribute('aria-expanded') === 'true'; | ||
var open = typeof options.open === 'boolean' ? options.open : (options.open === 'toggle' ? !isOpen : isOpen); | ||
var popup = String((options.hasOwnProperty('popup') ? options.popup : toggle.getAttribute(UUID)) || false); | ||
_this.ref = function (el) { | ||
return _this.el = el; | ||
}; | ||
if (options.value) { toggle.innerHTML = options.value; } // Set innerHTML before updating aria-label | ||
if (popup !== 'false' && popup !== 'true') { toggle.setAttribute('aria-label', ((toggle.textContent) + ", " + popup)); } // Only update aria-label if popup-mode | ||
attr.forEach(function (k) { | ||
var on = "on".concat(k.replace(/(^|\.)./g, function (m) { | ||
return m.slice(-1).toUpperCase(); | ||
})); // input.filter => onInputFilter | ||
toggle.setAttribute(UUID, popup); // aria-haspopup triggers forms mode in JAWS, therefore store in uuid | ||
toggle.setAttribute('aria-controls', content.id = content.id || getUUID()); | ||
content.setAttribute((ARIA + "-labelledby"), toggle.id = toggle.id || getUUID()); | ||
setOpen(toggle, open); | ||
return toggle | ||
}) | ||
_this[k] = function (event) { | ||
return _this.props[on] && _this.props[on](event); | ||
}; | ||
}); | ||
return _this; | ||
} | ||
_createClass(_class, [{ | ||
key: "componentDidMount", | ||
value: function componentDidMount() { | ||
var _this2 = this; | ||
attr.forEach(function (k) { | ||
return _this2.props[k] ? _this2.el[k] = _this2.props[k] : _this2.el.addEventListener(k, _this2[k]); | ||
}); | ||
} | ||
}, { | ||
key: "componentDidUpdate", | ||
value: function componentDidUpdate(prev) { | ||
var _this3 = this; | ||
attr.forEach(function (k) { | ||
return prev[k] !== _this3.props[k] && (_this3.el[k] = _this3.props[k]); | ||
}); | ||
} | ||
}, { | ||
key: "componentWillUnmount", | ||
value: function componentWillUnmount() { | ||
var _this4 = this; | ||
attr.forEach(function (k) { | ||
return _this4.el.removeEventListener(k, _this4[k]); | ||
}); | ||
} | ||
}, { | ||
key: "render", | ||
value: function render() { | ||
var _this5 = this; | ||
// Convert React props to CustomElement props https://github.com/facebook/react/issues/12810 | ||
return React.createElement(tag, Object.keys(this.props).reduce(function (props, k) { | ||
if (k === 'className') props["class"] = _this5.props[k]; // Fixes className for custom elements | ||
else if (_this5.props[k] === true) props[k] = ''; // Fixes boolean attributes | ||
else if (_this5.props[k] !== false) props[k] = _this5.props[k]; | ||
return props; | ||
}, { | ||
ref: this.ref | ||
})); | ||
} | ||
}]); | ||
return _class; | ||
}(React.Component) | ||
); | ||
} | ||
/** | ||
* getUUID | ||
* @return {String} A generated unique ID | ||
*/ | ||
function getContentElement (toggle) { | ||
return document.getElementById(toggle.getAttribute('aria-controls')) || toggle.nextElementSibling | ||
function getUUID() { | ||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 5); | ||
} | ||
addEvent(UUID, 'keydown', function (event) { | ||
if (event.keyCode !== KEYS.ESC) { return } | ||
for (var el = event.target; el; el = el.parentElement) { | ||
var toggle = (el.id && document.querySelector(("[aria-controls=\"" + (el.id) + "\"]"))) || el; | ||
var CoreToggle = | ||
/*#__PURE__*/ | ||
function (_HTMLElement) { | ||
_inherits(CoreToggle, _HTMLElement); | ||
if (toggle.getAttribute(UUID) !== 'false' && toggle.getAttribute('aria-expanded') === 'true') { | ||
event.preventDefault(); // Prevent leaving maximized window in Safari | ||
toggle.focus(); | ||
return setOpen(toggle, false) | ||
} | ||
function CoreToggle() { | ||
_classCallCheck(this, CoreToggle); | ||
return _possibleConstructorReturn(this, _getPrototypeOf(CoreToggle).apply(this, arguments)); | ||
} | ||
}, true); // Use capture to enable checking defaultPrevented (from ESC key) in parents | ||
addEvent(UUID, 'click', function (ref) { | ||
var target = ref.target; | ||
var defaultPrevented = ref.defaultPrevented; | ||
_createClass(CoreToggle, [{ | ||
key: "connectedCallback", | ||
value: function connectedCallback() { | ||
if (IS_IOS) document.documentElement.style.cursor = 'pointer'; // Fix iOS events for closing popups (https://stackoverflow.com/a/16006333/8819615) | ||
if (defaultPrevented) { return false } // Do not toggle if someone run event.preventDefault() | ||
if (!IS_ANDROID) this.setAttribute('aria-labelledby', this.button.id = this.button.id || getUUID()); // Andriod reads only label instead of content | ||
for (var el = target, item = (void 0); el; el = el.parentElement) { | ||
var toggle = item && el.id && document.querySelector(("[" + UUID + "][aria-controls=\"" + (el.id) + "\"]")); | ||
if ((el.nodeName === 'BUTTON' || el.nodeName === 'A') && !el.hasAttribute(UUID)) { item = el; } // interactive element clicked | ||
if (toggle) { | ||
dispatchEvent(toggle, 'toggle.select', { | ||
relatedTarget: getContentElement(toggle), | ||
currentTarget: item, | ||
value: item.textContent.trim() | ||
}); | ||
break | ||
this.value = this.button.textContent; // Set up aria-label | ||
this.setAttribute('role', 'group'); // Help Edge | ||
this.button.setAttribute('aria-expanded', this._open = !this.hidden); | ||
this.button.setAttribute('aria-controls', this.id = this.id || getUUID()); | ||
document.addEventListener('keydown', this, true); // Use capture to enable checking defaultPrevented (from ESC key) in parents | ||
document.addEventListener('click', this); | ||
} | ||
} | ||
}, { | ||
key: "disconnectedCallback", | ||
value: function disconnectedCallback() { | ||
this._button = null; | ||
document.removeEventListener('keydown', this, true); | ||
document.removeEventListener('click', this); | ||
} | ||
}, { | ||
key: "attributeChangedCallback", | ||
value: function attributeChangedCallback() { | ||
if (this._open === this.hidden) { | ||
// this._open comparison ensures actual change | ||
this.button.setAttribute('aria-expanded', this._open = !this.hidden); | ||
queryAll(("[" + UUID + "]")).forEach(function (toggle) { | ||
var open = toggle.getAttribute('aria-expanded') === 'true'; | ||
var popup = toggle.getAttribute(UUID) !== 'false'; | ||
var content = getContentElement(toggle); | ||
try { | ||
this.querySelector('[autofocus]').focus(); | ||
} catch (err) {} | ||
if (toggle.contains(target)) { setOpen(toggle, !open); } // Click on toggle | ||
else if (popup && open) { setOpen(toggle, content.contains(target)); } // Click in content or outside | ||
}); | ||
}); | ||
dispatchEvent(this, 'toggle'); | ||
} | ||
} | ||
}, { | ||
key: "handleEvent", | ||
value: function handleEvent(event) { | ||
if (event.defaultPrevented) return; | ||
function setOpen (toggle, open) { | ||
var content = getContentElement(toggle); | ||
var isOpen = toggle.getAttribute('aria-expanded') === 'true'; | ||
var willOpen = typeof open === 'boolean' ? open : (open === 'toggle' ? !isOpen : isOpen); | ||
var isUpdate = isOpen === willOpen || dispatchEvent(toggle, 'toggle', { relatedTarget: content, isOpen: isOpen, willOpen: willOpen }); | ||
var nextOpen = isUpdate ? willOpen : toggle.getAttribute('aria-expanded') === 'true'; // dispatchEvent can change attributes | ||
var focus = !isOpen && nextOpen && content.querySelector('[autofocus]'); | ||
if (event.type === 'keydown' && event.keyCode === 27) { | ||
var isButton = event.target.getAttribute && event.target.getAttribute('aria-expanded') === 'true'; | ||
var isHiding = isButton ? event.target === this.button : closest(event.target, this.nodeName) === this; | ||
if (focus) { setTimeout(function () { return focus && focus.focus(); }); } // Move focus on next render (if element stil exists) | ||
if (isHiding) { | ||
this.hidden = true; | ||
this.button.focus(); // Move focus back to button | ||
toggle.setAttribute('aria-expanded', nextOpen); | ||
content[nextOpen ? 'removeAttribute' : 'setAttribute']('hidden', ''); | ||
} | ||
return event.preventDefault(); // Prevent closing maximized Safari and other coreToggles | ||
} | ||
} | ||
var Toggle = /*@__PURE__*/(function (superclass) { | ||
function Toggle (props) { | ||
superclass.call(this, props); | ||
this.onToggle = this.onToggle.bind(this); | ||
this.onToggleSelect = this.onToggleSelect.bind(this); | ||
} | ||
if (event.type === 'click') { | ||
var btn = closest(event.target, 'a,button'); | ||
if (btn && !btn.hasAttribute('aria-expanded') && closest(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 | ||
} | ||
} | ||
}, { | ||
key: "button", | ||
get: function get() { | ||
if (this._button && this._button.getAttribute('for') === this.id) return this._button; // Speed up | ||
if ( superclass ) Toggle.__proto__ = superclass; | ||
Toggle.prototype = Object.create( superclass && superclass.prototype ); | ||
Toggle.prototype.constructor = Toggle; | ||
return (this._button = this.id && document.querySelector("[for=\"".concat(this.id, "\"]"))) || this.previousElementSibling; | ||
} // aria-haspopup triggers forms mode in JAWS, therefore store as custom attr | ||
var staticAccessors = { defaultProps: { configurable: true } }; | ||
staticAccessors.defaultProps.get = function () { return { open: null, popup: null, onToggle: null, onToggleSelect: null } }; | ||
}, { | ||
key: "popup", | ||
get: function get() { | ||
return this.getAttribute('popup') === 'true' || this.getAttribute('popup') || this.hasAttribute('popup'); | ||
}, | ||
set: function set(val) { | ||
this[val === false ? 'removeAttribute' : 'setAttribute']('popup', val); | ||
} // Must set attribute for IE11 | ||
Toggle.prototype.update = function update () { | ||
toggle(this.el.firstElementChild, { | ||
popup: this.props.popup, | ||
open: this.props.open | ||
}); | ||
}; | ||
Toggle.prototype.componentDidMount = function componentDidMount () { | ||
this.update(); | ||
this.el.addEventListener('toggle', this.onToggle); | ||
this.el.addEventListener('toggle.select', this.onToggleSelect); | ||
}; | ||
Toggle.prototype.componentDidUpdate = function componentDidUpdate () { this.update(); }; | ||
Toggle.prototype.componentWillUnmount = function componentWillUnmount () { | ||
this.el.removeEventListener('toggle', this.onToggle); | ||
this.el.removeEventListener('toggle.select', this.onToggleSelect); | ||
}; | ||
Toggle.prototype.onToggle = function onToggle (event) { | ||
this.props.onToggle && this.props.onToggle(event); | ||
}; | ||
Toggle.prototype.onToggleSelect = function onToggleSelect (event) { | ||
this.props.onToggleSelect && this.props.onToggleSelect(event); | ||
}; | ||
Toggle.prototype.render = function render () { | ||
var this$1 = this; | ||
}, { | ||
key: "hidden", | ||
get: function get() { | ||
return this.hasAttribute('hidden'); | ||
}, | ||
set: function set(val) { | ||
this.toggleAttribute('hidden', val); | ||
} // Sets this.button aria-label, so visible button text can be augmentet with intension of button | ||
// Example: Button text: "01.02.2019", aria-label: "01.02.2019, Choose date" | ||
// Does not updates aria-label if not allready set to something else than this.popup | ||
return React.createElement('div', exclude(this.props, Toggle.defaultProps, { ref: function (el) { return (this$1.el = el); } }), | ||
React.Children.map(this.props.children, function (child, adjacent) { | ||
if (adjacent === 0) { | ||
return React.cloneElement(child, { | ||
'aria-expanded': String(Boolean(this$1.props.open)) | ||
}) | ||
} | ||
if (adjacent === 1) { return React.cloneElement(child, { 'hidden': !this$1.props.open }) } | ||
return child | ||
}) | ||
) | ||
}; | ||
}, { | ||
key: "value", | ||
get: function get() { | ||
return this.button.value || this.button.textContent; | ||
}, | ||
set: function set() { | ||
var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; | ||
if (!this.button || !this.popup.length) return; | ||
var button = this.button; | ||
var popup = (button.getAttribute('aria-label') || ",".concat(this.popup)).split(',')[1]; | ||
var label = data.textContent || data || ''; // data can be Element, Object or String | ||
Object.defineProperties( Toggle, staticAccessors ); | ||
if (popup === this.popup) { | ||
button.value = data.value || label; | ||
button[data.innerHTML ? 'innerHTML' : 'textContent'] = data.innerHTML || label; | ||
button.setAttribute('aria-label', "".concat(button.textContent, ",").concat(this.popup)); | ||
} | ||
} | ||
}], [{ | ||
key: "observedAttributes", | ||
get: function get() { | ||
return ['hidden']; | ||
} | ||
}]); | ||
return Toggle; | ||
}(React.Component)); | ||
return CoreToggle; | ||
}(_wrapNativeSuper(HTMLElement)); | ||
Toggle.propTypes = { | ||
open: PropTypes.bool, | ||
popup: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), | ||
onToggle: PropTypes.func | ||
}; | ||
var coreToggle = elementToReact(CoreToggle, 'toggle', 'toggle.select'); | ||
module.exports = Toggle; | ||
module.exports = coreToggle; |
@@ -5,3 +5,3 @@ { | ||
"author": "NRK <opensource@nrk.no> (https://www.nrk.no/)", | ||
"version": "2.2.3", | ||
"version": "3.0.0", | ||
"license": "MIT", | ||
@@ -12,6 +12,3 @@ "main": "core-toggle.cjs.js", | ||
"url": "git+https://github.com/nrkno/core-components.git" | ||
}, | ||
"dependencies": { | ||
"prop-types": "15.7.2" | ||
} | ||
} |
232
readme.md
# Core Toggle | ||
> `@nrk/core-toggle` simply makes a `<button>` toggle the visibility of next element sibling. Toggles can be nested and easily extended with custom animations or behavior through the [toggle event](#events). It has two modes: | ||
> `@nrk/core-toggle` makes a `<button>` toggle the visibility of next element sibling. Toggles can be nested and easily extended with custom animations or behavior through the [toggle event](#events). | ||
<!-- <script src="https://unpkg.com/preact"></script> | ||
<script src="https://unpkg.com/preact-compat"></script> | ||
<script> | ||
window.React = preactCompat | ||
window.ReactDOM = preactCompat | ||
</script> --> | ||
<!--demo | ||
<script src="https://unpkg.com/@webcomponents/custom-elements"></script> | ||
<script src="core-toggle/core-toggle.min.js"></script> | ||
<script src="core-toggle/core-toggle.jsx.js"></script> | ||
<style>core-toggle:not([hidden]){display:block}</style> | ||
demo--> | ||
## Installation | ||
## Example | ||
```bash | ||
npm install @nrk/core-toggle --save-exact | ||
``` | ||
```js | ||
import coreToggle from '@nrk/core-toggle' // Vanilla JS | ||
import CoreToggle from '@nrk/core-toggle/jsx' // React/Preact JSX | ||
``` | ||
## Demo | ||
```html | ||
<!--demo--> | ||
<button class="my-popup">Popup VanillaJS</button> | ||
<ul class="my-dropdown" hidden> | ||
<li><a>Link</a></li> | ||
<li> | ||
<button class="my-popup">Can also be nested</button> | ||
<ul class="my-dropdown" hidden> | ||
<li><a>Sub-link</a></li> | ||
<li><input type="text" autofocus aria-label="Skriv her"></li> | ||
</ul> | ||
</li> | ||
</ul> | ||
<script> | ||
coreToggle('.my-popup', { popup: 'Example picker' }) | ||
</script> | ||
<button>Popup VanillaJS</button> | ||
<core-toggle class="my-dropdown" popup hidden> | ||
<ul> | ||
<li><a>Link</a></li> | ||
<li> | ||
<button>Can also be nested</button> | ||
<core-toggle class="my-dropdown" popup hidden> | ||
<ul> | ||
<li><a>Sub-link</a></li> | ||
<li><input type="text" autofocus aria-label="Skriv her"></li> | ||
</ul> | ||
</core-toggle> | ||
</li> | ||
</ul> | ||
</core-toggle> | ||
``` | ||
@@ -46,19 +43,37 @@ | ||
<script type="text/jsx"> | ||
ReactDOM.render(<CoreToggle popup='Example picker'> | ||
ReactDOM.render(<> | ||
<button>Popup JSX</button> | ||
<ul className='my-dropdown'> | ||
<li><a href='#'>Link</a></li> | ||
<li> | ||
<CoreToggle popup='Example picker'> | ||
<CoreToggle className='my-dropdown' hidden popup onToggleSelect={console.warn}> | ||
<ul> | ||
<li><button>Select</button></li> | ||
<li><a href='#'>Link</a></li> | ||
<li> | ||
<button>Can also be nested</button> | ||
<ul className='my-dropdown'> | ||
<li><a href='#'>Sub-link</a></li> | ||
</ul> | ||
</CoreToggle> | ||
</li> | ||
</ul> | ||
</CoreToggle>, document.getElementById('jsx-toggle-popup')) | ||
<CoreToggle className='my-dropdown' hidden popup> | ||
<ul> | ||
<li><a href='#'>Sub-link</a></li> | ||
</ul> | ||
</CoreToggle> | ||
</li> | ||
</ul> | ||
</CoreToggle> | ||
</>, document.getElementById('jsx-toggle-popup')) | ||
</script> | ||
``` | ||
## Installation | ||
Using NPM provides own element namespace and extensibility. | ||
Recommended: | ||
```bash | ||
npm install @nrk/core-toggle # Using NPM | ||
``` | ||
Using static registers the custom element with default name automatically: | ||
```html | ||
<script src="https://static.nrk.no/core-components/major/5/core-toggle/core-toggle.min.js"></script> <!-- Using static --> | ||
``` | ||
## Usage | ||
@@ -69,14 +84,25 @@ | ||
```html | ||
<button class="my-toggle">Toggle VanillaJS</button> <!-- must be <button> --> | ||
<div hidden>Content</div> <!-- use hidden to prevent flash of unstyled content --> | ||
<button>Toggle VanillaJS</button> <!-- Must be <button> placed directly before <core-toggle> or use id + for attributes --> | ||
<core-toggle | ||
hidden <!-- Set hidden attribute to prevent FOUC --> | ||
popup="{Boolean|String}"> <!-- Optional. Defaults to false. Enable or disable if clicking outside toggle should close it. Provide a string to control the aria-label text on the toggle --> | ||
<div>Content</div> | ||
</core-toggle> | ||
``` | ||
```js | ||
import coreToggle from '@nrk/core-toggle' | ||
import CoreToggle from '@nrk/core-toggle' // Using NPM | ||
window.customElements.define('core-toggle', CoreToggle) // Using NPM. Replace 'core-toggle' with 'my-toggle' to namespace | ||
coreToggle(String|Element|Elements, { // Accepts a selector string, NodeList, Element or array of Elements | ||
open: Boolean // Optional. Defaults to aria-expanded or false. Set to force open state. | ||
popup: Boolean|String // Optional. Defaults to false. Enable or disable if clicking outside toggle should close it. Provide a string to control the aria-label text on the toggle. | ||
value: String // Optional. Defaults to button.innerHTML. Sets innerHTML of the button and safely updates aria-label for screen readers. | ||
}) | ||
const myToggle = document.querySelector('core-toggle') | ||
// Getters | ||
myToggle.button // Get toggle button element | ||
myToggle.popup // Get popup value | ||
myToggle.hidden // Get hidden value | ||
myToggle.value // Get toggle button text | ||
// Setters | ||
myToggle.popup = true // Enable or disable if clicking outside toggle should close it. Provide a string to control the aria-label text on the toggle | ||
myToggle.hidden = true // Set hidden attribute | ||
myToggle.value = 'Velg' // Sets innerHTML of the button and safely updates aria-label for screen readers. Defaults to button.innerHTML | ||
``` | ||
@@ -86,11 +112,11 @@ | ||
```jsx | ||
```js | ||
import CoreToggle from '@nrk/core-toggle/jsx' | ||
// All props are optional, and defaults are shown below | ||
// Props like className, style, etc. will be applied as actual attributes | ||
// <CoreToggle> will handle state itself unless you call event.preventDefault() in onToggle | ||
<CoreToggle open={false} popup={false} onToggle={(event) => {}}> | ||
<button>Use with JSX</button> // First element must result in a <button>-tag. Accepts both elements and components | ||
<CoreToggle | ||
hidden // Set hidden attribute to prevent FOUC | ||
popup={Boolean|String} // Optional. Defaults to false. Enable or disable if clicking outside toggle should close it. Provide a string to control the aria-label text on the toggle | ||
onToggle={Function} // Optional. Toggle event listener. See event 'toggle' | ||
onToggleSelect={Function}> // Optional. Toggle select event listener. See event 'toggle.select' | ||
<button>Use with JSX</button> // First element must result in a <button>. Accepts both elements and components | ||
<div>Content</div> // Next element will be toggled. Accepts both elements and components | ||
@@ -102,11 +128,11 @@ </CoreToggle> | ||
### With aria-controls | ||
### With for | ||
Putting the toggle button directly before the content is highly recommended, as this fulfills all accessibility requirements by default. There might be scenarios though, where styling makes this DOM structure impractical. In such cases, give the toggle button an `aria-controls` attribute, and the content an `id` with corresponding value. Make sure there is no text between the button and toggle content, as this will break the experience for screen reader users: | ||
Putting the toggle button directly before the content is highly recommended, as this fulfills all accessibility requirements by default. There might be scenarios though, where styling makes this DOM structure impractical. In such cases, give the `<button>` a `for` attribute, and the `core-toggle` a `id` with corresponding value. Make sure there is no text between the button and toggle content, as this will break the experience for screen reader users: | ||
```html | ||
<div> | ||
<button class="my-toggle" aria-controls="content">Toggle VanillaJS</button> | ||
<button for="my-toggle">Toggle VanillaJS</button> | ||
</div> | ||
<div id="content" hidden>Content</div> | ||
<core-toggle id="my-toggle" hidden>Content</core-toggle> | ||
``` | ||
@@ -116,7 +142,4 @@ | ||
If you have form elements inside a `@nrk/core-toggle`, you can optionally add a `autofocus` attribute to the most prominent form element. This helps the user navigate quickly when toggle is opened. | ||
If you have form elements inside a `core-toggle`, you can optionally add a `autofocus` attribute to the most prominent form element. This helps the user navigate quickly when toggle is opened. | ||
## Events | ||
@@ -126,3 +149,3 @@ | ||
Before a `@nrk/core-toggle` changes open state, a [toggle event](https://www.w3schools.com/jsref/event_ontoggle.asp) is fired (both for VanillaJS and React/Preact components). The toggle event is cancelable, meaning you can use [`event.preventDefault()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault) to cancel toggling. The event also [bubbles](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture), and can therefore be detected both from the button element itself, or any parent element (read [event delegation](https://stackoverflow.com/questions/1687296/what-is-dom-event-delegation)): | ||
Fired after open state changes: | ||
@@ -132,6 +155,3 @@ | ||
document.addEventListener('toggle', (event) => { | ||
event.target // The button element triggering toggle event | ||
event.detail.relatedTarget // The content element controlled by button | ||
event.detail.isOpen // The current toggle state (before toggle event has run) | ||
event.detail.willOpen // The wanted toggle state | ||
event.target // The toggle element | ||
}) | ||
@@ -142,3 +162,3 @@ ``` | ||
The `toggle.select` event is fired whenever an item is selected inside a toggle with the `popup` option enabled. | ||
Fired whenever an `<a>` or `<button>` element is selected inside a toggle with the `popup` option enabled. | ||
Useful for setting the value of the toggle button with the selected value. | ||
@@ -149,6 +169,5 @@ | ||
document.addEventListener('toggle.select', (event) => { | ||
event.target // The buttom element triggering the event | ||
event.detail.relatedTarget // The content element controlled by button | ||
event.detail.currentTarget // The item element selected | ||
event.detail.value // The selected item's value | ||
event.target // The toggle element | ||
event.detail // The selected element | ||
event.target.value = event.detail // Example: set value of toggle to selected element | ||
}) | ||
@@ -160,8 +179,8 @@ ``` | ||
All styling in documentation is example only. Both the `<button>` and content element receive attributes reflecting the current toggle state: | ||
**Note:** `core-toggle` is `display: inline` by default. Change this by for instance setting `core-tabs:not([hidden]) { display: block | flex | grid }` or similar in your app. Not needed when `position` or `float` is used. All styling in documentation is example only. Both the `<button>` and `core-toggle` element receive attributes reflecting the current toggle state: | ||
```css | ||
.my-toggle {} /* Target button in any state */ | ||
.my-toggle[aria-expanded="true"] {} /* Target only open button */ | ||
.my-toggle[aria-expanded="false"] {} /* Target only closed button */ | ||
.my-button {} /* Target button in any state */ | ||
.my-button[aria-expanded="true"] {} /* Target only open button */ | ||
.my-button[aria-expanded="false"] {} /* Target only closed button */ | ||
@@ -173,3 +192,3 @@ .my-toggle-content {} /* Target content in any state */ | ||
## Demo: Expand | ||
## Example: Expand | ||
@@ -180,7 +199,4 @@ Content is only toggled when clicking the button. Great for accordions and expand/collapse panels. | ||
<!--demo--> | ||
<button class="my-toggle">Toggle VanillaJS</button> <!-- must be <button> --> | ||
<div hidden>Content</div> <!-- hidden prevents flash of unstyled content --> | ||
<script> | ||
coreToggle('.my-toggle') // Optionally pass {open: true|false} as second argument to open/close | ||
</script> | ||
<button>Toggle VanillaJS</button> <!-- must be <button> --> | ||
<core-toggle hidden>Content</core-toggle> <!-- hidden prevents flash of unstyled content --> | ||
``` | ||
@@ -191,6 +207,6 @@ ```html | ||
<script type="text/jsx"> | ||
ReactDOM.render(<CoreToggle popup={false} open={false} onToggle={function(){}}> | ||
ReactDOM.render(<> | ||
<button>Toggle JSX</button> | ||
<div>Content</div> | ||
</CoreToggle>, document.getElementById('jsx-toggle-default')) | ||
<CoreToggle hidden onToggle={console.log}>Content</CoreToggle> | ||
</>, document.getElementById('jsx-toggle-default')) | ||
</script> | ||
@@ -200,3 +216,3 @@ ``` | ||
## Demo: Select | ||
## Example: Select | ||
@@ -208,14 +224,17 @@ Listen to the `toggle.select` event and update the button's value from the selected item | ||
<!--demo--> | ||
<button class="my-popup-value">Select number</button> | ||
<ul class="my-dropdown" hidden> | ||
<li><button>One</button></li> | ||
<li><button>Two</button></li> | ||
<li><button>Three</button></li> | ||
</ul> | ||
<button>Episode 1</button> | ||
<core-toggle class="my-select my-dropdown" hidden popup="Choose episode"> | ||
<ul> | ||
<li><button>Episode 1</button></li> | ||
<li><button>Episode 2</button></li> | ||
<li><button>Episode 3</button></li> | ||
</ul> | ||
</core-toggle> | ||
<script> | ||
document.addEventListener('toggle.select', (event) => { | ||
if (event.target.className !== 'my-popup-value') return | ||
coreToggle(event.target, { value: event.detail.value, open: false }) | ||
if (!event.target.classList.contains('my-select')) return | ||
event.target.value = event.detail | ||
event.target.hidden = true | ||
event.target.button.focus() | ||
}) | ||
coreToggle('.my-popup-value', { popup: 'Number picker'}) | ||
</script> | ||
@@ -235,13 +254,16 @@ ``` | ||
onSelect (event) { | ||
this.setState({ value: event.detail.value }) | ||
event.target.hidden = true | ||
this.setState({ value: event.detail.textContent }) | ||
} | ||
render () { | ||
return <CoreToggle popup='Example picker' open={false} onToggleSelect={this.onSelect}> | ||
return <> | ||
<button>{this.state.value}</button> | ||
<ul className='my-dropdown'> | ||
<li><button>One</button></li> | ||
<li><button>Two</button></li> | ||
<li><button>Three</button></li> | ||
</ul> | ||
</CoreToggle> | ||
<CoreToggle className='my-dropdown' popup='Example picker' hidden onToggleSelect={this.onSelect}> | ||
<ul> | ||
<li><button>One</button></li> | ||
<li><button>Two</button></li> | ||
<li><button>Three</button></li> | ||
</ul> | ||
</CoreToggle> | ||
</> | ||
} | ||
@@ -253,4 +275,2 @@ } | ||
## FAQ | ||
@@ -257,0 +277,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
109426
0
1257
0
275
2
- Removedprop-types@15.7.2
- Removedjs-tokens@4.0.0(transitive)
- Removedloose-envify@1.4.0(transitive)
- Removedobject-assign@4.1.1(transitive)
- Removedprop-types@15.7.2(transitive)
- Removedreact-is@16.13.1(transitive)