UNPKG

@stormid/tabs

Version:

Accessible tabbed panelled content areas

1 lines 17.5 kB
{"version":3,"file":"index.modern.mjs","sources":["../src/lib/defaults.js","../src/lib/constants.js","../src/lib/store.js","../src/lib/dom.js","../src/lib/factory.js","../src/index.js","../src/lib/utils.js"],"sourcesContent":["import { MODES } from './constants';\n/* istanbul ignore file */\n/*\n * Default settings used by a Tabs instance if not otherwise overwritten with config\n *\n * @property tabSelector, String, selector for a tab link \n * @property activeClass, String, className added to active tab\n * @property updateURL, Boolean, to push tab fragment identifier to window location \n * @property activation, string, 'auto' or 'manual' describes tab activation method. \n * as per https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-2/tabs.html or https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html\n * @property activeIndex, Number, index of initially active tab\n * @property focusOnLoad, Boolean, sets whether the page should focus on the first tab on load\n */\nexport default {\n tabSelector: '[role=tab]',\n activeClass: 'is--active',\n updateURL: true,\n activation: MODES.AUTO,\n activeIndex: 0,\n focusOnLoad: false\n};","/* istanbul ignore file */\r\n// Event keycodes that initiate toggle for keyboard events\r\nexport const KEYCODES = {\r\n SPACE: 32,\r\n ENTER: 13,\r\n TAB: 9,\r\n LEFT: 37,\r\n RIGHT: 39,\r\n DOWN: 40\r\n};\r\n\r\nexport const ACCEPTED_TRIGGERS = ['button', 'a'];\r\n\r\n\r\n/* @property activation, string, 'auto' or 'manual' describes tab activation method. \r\n as per https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-2/tabs.html or https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html */\r\nexport const MODES = {\r\n MANUAL: 'manual',\r\n AUTO: 'auto'\r\n};\r\n\r\n//Array of focusable child elements\r\nexport const FOCUSABLE_ELEMENTS = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex=\"-1\"])'];","export const createStore = () => {\r\n let state = {};\r\n \r\n const getState = () => state;\r\n\r\n const update = (nextState, effects) => {\r\n state = nextState ?? state;\r\n if (!effects) return;\r\n effects.forEach(effect => effect(state));\r\n };\r\n \r\n return { update, getState };\r\n};","import { KEYCODES, MODES } from './constants';\r\n\r\n/*\r\n * Returns an Object composed of two Array of HTMLElements - tabs and panels\r\n * \r\n * @param node, HTMLElement, tab container\r\n * @param settings, Object, settings of the \r\n * @return Object, tabs (Array of HTMLElement tab links), panels (Array of HTMLElement panel links)\r\n */\r\nexport const findTabsAndPanels = (node, settings) => {\r\n const tabs = [].slice.call(node.querySelectorAll(settings.tabSelector));\r\n const panels = tabs.map(tab => document.getElementById(tab.getAttribute('href').substr(1)) || console.warn(`Tab panel not found for ${tab}`));\r\n return { tabs, panels };\r\n};\r\n \r\n/*\r\n * Sets aria attributes and adds eventListener on each tab\r\n * \r\n * @param store, Object, model or state of the current instance\r\n */\r\nexport const initUI = store => ({ tabs, panels }) => {\r\n tabs[0].parentNode.setAttribute('role', 'tablist');\r\n tabs.forEach((tab, i) => {\r\n tab.setAttribute('role', 'tab');\r\n tab.setAttribute('aria-selected', false);\r\n panels[i].setAttribute('aria-labelledby', tab.getAttribute('id'));\r\n tab.setAttribute('tabindex', '-1');\r\n panels[i].setAttribute('role', 'tabpanel');\r\n panels[i].setAttribute('hidden', 'hidden');\r\n panels[i].setAttribute('tabindex', '-1');\r\n initListeners(tab, i, store);\r\n if (!panels[i].firstElementChild || panels[i].firstElementChild.hasAttribute('tabindex')) return;\r\n panels[i].firstElementChild.setAttribute('tabindex', '-1');\r\n });\r\n};\r\n\r\nconst getPreviousTabIndex = ({ activeTabIndex, tabs }) => activeTabIndex === 0 ? tabs.length - 1 : activeTabIndex - 1;\r\n\r\nconst getNextTabIndex = ({ activeTabIndex, tabs }) => activeTabIndex === tabs.length - 1 ? 0 : activeTabIndex + 1;\r\n\r\nconst initListeners = (tab, nextIndex, store) => {\r\n const isManualActivation = store.getState().settings.activation === MODES.MANUAL;\r\n const onDirectionChangeFunction = (isManualActivation) ? () => focusTab(store) : changePanel;\r\n\r\n tab.addEventListener('keydown', e => {\r\n const previousIndex = store.getState().activeIndex;\r\n switch (e.keyCode) {\r\n case KEYCODES.LEFT:\r\n store.update({\r\n ...store.getState(),\r\n activeTabIndex: getPreviousTabIndex(store.getState()),\r\n activeIndex: (isManualActivation) ? store.getState().activeIndex : getPreviousTabIndex(store.getState())\r\n }, [() => onDirectionChangeFunction(store, previousIndex)]);\r\n break;\r\n case KEYCODES.RIGHT:\r\n store.update({\r\n ...store.getState(),\r\n activeTabIndex: getNextTabIndex(store.getState()) ,\r\n activeIndex: (isManualActivation) ? store.getState().activeIndex : getNextTabIndex(store.getState())\r\n }, [() => onDirectionChangeFunction(store, previousIndex)]);\r\n break;\r\n case KEYCODES.ENTER:\r\n (previousIndex !== nextIndex) && store.update({ ...store.getState(), activeIndex: nextIndex, activeTabIndex: nextIndex }, [() => changePanel(store, previousIndex)]);\r\n break;\r\n case KEYCODES.SPACE:\r\n e.preventDefault();\r\n (previousIndex !== nextIndex) && store.update({ ...store.getState(), activeIndex: nextIndex, activeTabIndex: nextIndex }, [() => changePanel(store, previousIndex)]);\r\n break;\r\n default:\r\n break;\r\n }\r\n });\r\n \r\n tab.addEventListener('click', e => {\r\n e.preventDefault();\r\n const previousActiveIndex = store.getState().activeTabIndex;\r\n store.getState().activeIndex !== nextIndex && store.update({\r\n ...store.getState(),\r\n activeIndex: nextIndex,\r\n activeTabIndex: nextIndex\r\n }, [() => changePanel(store, previousActiveIndex)]);\r\n }, false);\r\n};\r\n\r\nconst changePanel = (store, previousActiveIndex) => {\r\n const { activeIndex, settings, tabs } = store.getState();\r\n close(store.getState(), previousActiveIndex);\r\n open(store)(store.getState());\r\n focusTab(store);\r\n if (settings.updateURL && window.history) {\r\n window.history.replaceState({ URL: tabs[activeIndex].getAttribute('href') }, '', tabs[activeIndex].getAttribute('href'));\r\n }\r\n};\r\n\r\nconst close = ({ settings, tabs, panels }, previousActiveIndex) => {\r\n tabs[previousActiveIndex].classList.remove(settings.activeClass);\r\n tabs[previousActiveIndex].setAttribute('tabindex', '-1');\r\n tabs[previousActiveIndex].setAttribute('aria-selected', false);\r\n panels[previousActiveIndex].classList.remove(settings.activeClass);\r\n panels[previousActiveIndex].setAttribute('hidden', 'hidden');\r\n panels[previousActiveIndex].setAttribute('tabindex', '-1');\r\n};\r\n\r\nconst activateTab = ({ settings, tabs, activeTabIndex }) => {\r\n tabs[activeTabIndex].classList.add(settings.activeClass);\r\n tabs[activeTabIndex].setAttribute('tabindex', 0);\r\n};\r\n\r\nconst focusTab = store => {\r\n const { tabs, activeTabIndex } = store.getState();\r\n window.setTimeout(() => { tabs[activeTabIndex].focus(); }, 0);\r\n};\r\n\r\nexport const open = store => () => {\r\n const { settings, tabs, panels, activeIndex, activeTabIndex, loaded } = store.getState();\r\n activateTab({ settings, tabs, activeTabIndex });\r\n if (settings.focusOnLoad && !loaded) focusTab(store);\r\n tabs[activeTabIndex].setAttribute('aria-selected', true);\r\n panels[activeIndex].classList.add(settings.activeClass);\r\n panels[activeIndex].removeAttribute('hidden');\r\n panels[activeIndex].setAttribute('tabindex', 0);\r\n\r\n store.update({ ...store.getState(), loaded: true }, []);\r\n};\r\n\r\n\r\n","import { createStore } from './store';\r\nimport { findTabsAndPanels, initUI, open } from './dom';\r\nimport { getActiveIndexOnLoad } from './utils';\r\n\r\n/* \r\n * @param settings, Object, merged defaults + options passed in as instantiation config to module default\r\n * @param node, HTMLElement, DOM node to be toggled\r\n *\r\n * @returns Object, Toggle API\r\n */\r\nexport default ({ node, settings }) => {\r\n const store = createStore();\r\n const { tabs, panels } = findTabsAndPanels(node, settings);\r\n \r\n if (!tabs.length || !panels.length || panels.includes(undefined)) return false;\r\n\r\n const activeIndex = getActiveIndexOnLoad(panels, node);\r\n store.update({\r\n settings,\r\n node,\r\n activeIndex: activeIndex !== undefined ? +activeIndex : +settings.activeIndex,\r\n activeTabIndex: activeIndex !== undefined ? +activeIndex : +settings.activeIndex,\r\n tabs,\r\n panels,\r\n loaded: false\r\n }, [ initUI(store), open(store) ]);\r\n\r\n return {\r\n getState: store.getState\r\n };\r\n};","import defaults from './lib/defaults';\nimport factory from './lib/factory';\nimport { getSelection } from './lib/utils';\n\n/*\n * Returns an array of objects augmenting DOM elements that match a selector\n *\n * @param selector, Can be a string, Array of DOM nodes, a NodeList or a single DOM element.\n * @params options, Object, to be merged with defaults to become the settings propery of each returned object\n */\nexport default (selector, options) => {\n let nodes = getSelection(selector);\n\n if (nodes.length === 0) return console.warn(`Tabs not initialised, no elements found for selector '${selector}'`);\n \n //return array of Objects, one for each DOM node found\n //each Object has a prototype consisting of the node (HTMLElement),\n //and a settings property composed from defaults, data-attributes on the node, and options passed to init\n return nodes.map(node => {\n const instance = factory({\n settings: { ...defaults, ...node.dataset, ...options },\n node\n });\n return instance ? Object.create(instance) : void console.warn('Tab not initialised, required markup not found');\n }).filter(instance => typeof instance !== 'undefined');\n};","export const getActiveIndexByHash = panels => {\r\n const hash = location.hash ? location.hash.slice(1) : false;\r\n if (!hash) return undefined;\r\n \r\n return panels.reduce((acc, panel, i) => {\r\n if (panel.getAttribute('id') === hash) acc = i;\r\n return acc;\r\n }, undefined);\r\n};\r\n\r\nexport const getActiveIndexOnLoad = (panels, node) => (location.hash)\r\n ? getActiveIndexByHash(panels)\r\n : (node.getAttribute('data-active-index'))\r\n ? parseInt(node.getAttribute('data-active-index'), 10)\r\n : undefined;\r\n\r\n/*\r\n * Converts a passed selector which can be of varying types into an array of DOM Objects\r\n *\r\n * @param selector, Can be a string, Array of DOM nodes, a NodeList or a single DOM element.\r\n */\r\nexport const getSelection = selector => {\r\n\r\n if (typeof selector === 'string') return [].slice.call(document.querySelectorAll(selector));\r\n if (selector instanceof Array) return selector;\r\n if (Object.prototype.isPrototypeOf.call(NodeList.prototype, selector)) return [].slice.call(selector);\r\n if (selector instanceof HTMLElement) return [selector];\r\n return [];\r\n};"],"names":["defaults","tabSelector","activeClass","updateURL","activation","activeIndex","focusOnLoad","initUI","store","tabs","panels","parentNode","setAttribute","forEach","tab","i","getAttribute","initListeners","firstElementChild","hasAttribute","getPreviousTabIndex","activeTabIndex","length","getNextTabIndex","nextIndex","isManualActivation","getState","settings","onDirectionChangeFunction","focusTab","changePanel","addEventListener","e","previousIndex","keyCode","update","_extends","preventDefault","previousActiveIndex","close","open","window","history","replaceState","URL","classList","remove","setTimeout","focus","loaded","activateTab","add","removeAttribute","index","selector","options","nodes","slice","call","document","querySelectorAll","Array","Object","prototype","isPrototypeOf","NodeList","HTMLElement","getSelection","console","warn","map","node","instance","createStore","state","nextState","effects","effect","findTabsAndPanels","getElementById","substr","includes","undefined","getActiveIndexOnLoad","location","hash","reduce","acc","panel","getActiveIndexByHash","parseInt","factory","dataset","create","filter"],"mappings":"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"}