@magic-spells/tab-group 0.1.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"tab-group.esm.js","sources":["../src/tab-group.js"],"sourcesContent":["import './index.scss';\n\n/**\n * @module TabGroup\n * A fully accessible tab group web component\n */\n\n/**\n * @class TabGroup\n * the parent container that coordinates tabs and panels\n */\nexport default class TabGroup extends HTMLElement {\n\t// static counter to ensure global unique ids for tabs and panels\n\tstatic tabCount = 0;\n\tstatic panelCount = 0;\n\n\tconstructor() {\n\t\tsuper();\n\t\t// ensure that the number of <tab-button> and <tab-panel> elements match\n\t\t// note: in some scenarios the child elements might not be available in the constructor,\n\t\t// so adjust as necessary or consider running this check in connectedCallback()\n\t\tthis.ensureConsistentTabsAndPanels();\n\t}\n\n\t/**\n\t * @function ensureConsistentTabsAndPanels\n\t * makes sure there is an equal number of <tab-button> and <tab-panel> elements.\n\t * if there are more panels than tabs, inject extra tab buttons.\n\t * if there are more tabs than panels, inject extra panels.\n\t */\n\tensureConsistentTabsAndPanels() {\n\t\t// get current tabs and panels within the tab group\n\t\tlet tabs = this.querySelectorAll(\"tab-button\");\n\t\tlet panels = this.querySelectorAll(\"tab-panel\");\n\n\t\t// if there are more panels than tabs\n\t\tif (panels.length > tabs.length) {\n\t\t\tconst difference = panels.length - tabs.length;\n\t\t\t// try to find a <tab-list> to insert new tabs\n\t\t\tlet tabList = this.querySelector(\"tab-list\");\n\t\t\tif (!tabList) {\n\t\t\t\t// if not present, create one and insert it at the beginning\n\t\t\t\ttabList = document.createElement(\"tab-list\");\n\t\t\t\tthis.insertBefore(tabList, this.firstChild);\n\t\t\t}\n\t\t\t// inject extra <tab-button> elements into the tab list\n\t\t\tfor (let i = 0; i < difference; i++) {\n\t\t\t\tconst newTab = document.createElement(\"tab-button\");\n\t\t\t\tnewTab.textContent = \"default tab\";\n\t\t\t\ttabList.appendChild(newTab);\n\t\t\t}\n\t\t}\n\t\t// if there are more tabs than panels\n\t\telse if (tabs.length > panels.length) {\n\t\t\tconst difference = tabs.length - panels.length;\n\t\t\t// inject extra <tab-panel> elements at the end of the tab group\n\t\t\tfor (let i = 0; i < difference; i++) {\n\t\t\t\tconst newPanel = document.createElement(\"tab-panel\");\n\t\t\t\tnewPanel.innerHTML = \"<p>default panel content</p>\";\n\t\t\t\tthis.appendChild(newPanel);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * called when the element is connected to the dom\n\t */\n\tconnectedCallback() {\n\t\tconst _ = this;\n\n\t\t// find the <tab-list> element (should be exactly one)\n\t\t_.tabList = _.querySelector(\"tab-list\");\n\t\tif (!_.tabList) return;\n\n\t\t// find all <tab-button> elements inside the <tab-list>\n\t\t_.tabButtons = Array.from(_.tabList.querySelectorAll(\"tab-button\"));\n\n\t\t// find all <tab-panel> elements inside the <tab-group>\n\t\t_.tabPanels = Array.from(_.querySelectorAll(\"tab-panel\"));\n\n\t\t// initialize each tab-button with roles, ids and aria attributes\n\t\t_.tabButtons.forEach((tab, index) => {\n\t\t\tconst tabIndex = TabGroup.tabCount++;\n\n\t\t\t// generate a unique id for each tab, e.g. \"tab-0\", \"tab-1\", ...\n\t\t\tconst tabId = `tab-${tabIndex}`;\n\t\t\ttab.id = tabId;\n\n\t\t\t// generate a corresponding panel id, e.g. \"panel-0\"\n\t\t\tconst panelId = `panel-${tabIndex}`;\n\t\t\ttab.setAttribute(\"role\", \"tab\");\n\t\t\ttab.setAttribute(\"aria-controls\", panelId);\n\n\t\t\t// first tab is active by default\n\t\t\tif (index === 0) {\n\t\t\t\ttab.setAttribute(\"aria-selected\", \"true\");\n\t\t\t\ttab.setAttribute(\"tabindex\", \"0\");\n\t\t\t} else {\n\t\t\t\ttab.setAttribute(\"aria-selected\", \"false\");\n\t\t\t\ttab.setAttribute(\"tabindex\", \"-1\");\n\t\t\t}\n\t\t});\n\n\t\t// initialize each tab-panel with roles, ids and aria attributes\n\t\t_.tabPanels.forEach((panel, index) => {\n\t\t\tconst panelIndex = TabGroup.panelCount++;\n\t\t\tconst panelId = `panel-${panelIndex}`;\n\t\t\tpanel.id = panelId;\n\n\t\t\tpanel.setAttribute(\"role\", \"tabpanel\");\n\t\t\tpanel.setAttribute(\"aria-labelledby\", `tab-${panelIndex}`);\n\n\t\t\t// hide panels except for the first one\n\t\t\tpanel.hidden = index !== 0;\n\t\t});\n\n\t\t// set up keyboard navigation and click delegation on the <tab-list>\n\t\t_.tabList.setAttribute(\"role\", \"tablist\");\n\t\t_.tabList.addEventListener(\"keydown\", (e) => _.onKeyDown(e));\n\t\t_.tabList.addEventListener(\"click\", (e) => _.onClick(e));\n\t}\n\n\t/**\n\t * @function setActiveTab\n\t * activates a tab and updates aria attributes\n\t * @param {number} index - index of the tab to activate\n\t */\n\tsetActiveTab(index) {\n\t\tconst _ = this;\n\t\tconst previousIndex = _.tabButtons.findIndex(tab => tab.getAttribute(\"aria-selected\") === \"true\");\n\n\t\t// update each tab-button\n\t\t_.tabButtons.forEach((tab, i) => {\n\t\t\tconst isActive = i === index;\n\t\t\ttab.setAttribute(\"aria-selected\", isActive ? \"true\" : \"false\");\n\t\t\ttab.setAttribute(\"tabindex\", isActive ? \"0\" : \"-1\");\n\t\t\tif (isActive) {\n\t\t\t\ttab.focus();\n\t\t\t}\n\t\t});\n\n\t\t// update each tab-panel\n\t\t_.tabPanels.forEach((panel, i) => {\n\t\t\tpanel.hidden = i !== index;\n\t\t});\n\n\t\t// dispatch event only if the tab actually changed\n\t\tif (previousIndex !== index) {\n\t\t\tconst detail = {\n\t\t\t\tpreviousIndex,\n\t\t\t\tcurrentIndex: index,\n\t\t\t\tpreviousTab: _.tabButtons[previousIndex],\n\t\t\t\tcurrentTab: _.tabButtons[index],\n\t\t\t\tpreviousPanel: _.tabPanels[previousIndex],\n\t\t\t\tcurrentPanel: _.tabPanels[index]\n\t\t\t};\n\t\t\t_.dispatchEvent(new CustomEvent('tabchange', { detail, bubbles: true }));\n\t\t}\n\t}\n\n\t/**\n\t * @function onClick\n\t * handles click events on the <tab-list> via event delegation\n\t * @param {MouseEvent} e - the click event\n\t */\n\tonClick(e) {\n\t\tconst _ = this;\n\t\t// check if the click occurred on or within a <tab-button>\n\t\tconst tabButton = e.target.closest(\"tab-button\");\n\t\tif (!tabButton) return;\n\n\t\t// determine the index of the clicked tab-button\n\t\tconst index = _.tabButtons.indexOf(tabButton);\n\t\tif (index === -1) return;\n\n\t\t// activate the tab with the corresponding index\n\t\t_.setActiveTab(index);\n\t}\n\n\t/**\n\t * @function onKeyDown\n\t * handles keyboard navigation for the tabs\n\t * @param {KeyboardEvent} e - the keydown event\n\t */\n\tonKeyDown(e) {\n\t\tconst _ = this;\n\t\t// only process keys if focus is on a <tab-button>\n\t\tconst targetIndex = _.tabButtons.indexOf(e.target);\n\t\tif (targetIndex === -1) return;\n\n\t\tlet newIndex = targetIndex;\n\t\tswitch (e.key) {\n\t\t\tcase \"ArrowLeft\":\n\t\t\tcase \"ArrowUp\":\n\t\t\t\t// move to the previous tab (wrap around if necessary)\n\t\t\t\tnewIndex = targetIndex > 0 ? targetIndex - 1 : _.tabButtons.length - 1;\n\t\t\t\te.preventDefault();\n\t\t\t\tbreak;\n\t\t\tcase \"ArrowRight\":\n\t\t\tcase \"ArrowDown\":\n\t\t\t\t// move to the next tab (wrap around if necessary)\n\t\t\t\tnewIndex = (targetIndex + 1) % _.tabButtons.length;\n\t\t\t\te.preventDefault();\n\t\t\t\tbreak;\n\t\t\tcase \"Home\":\n\t\t\t\t// jump to the first tab\n\t\t\t\tnewIndex = 0;\n\t\t\t\te.preventDefault();\n\t\t\t\tbreak;\n\t\t\tcase \"End\":\n\t\t\t\t// jump to the last tab\n\t\t\t\tnewIndex = _.tabButtons.length - 1;\n\t\t\t\te.preventDefault();\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\treturn; // ignore other keys\n\t\t}\n\t\t_.setActiveTab(newIndex);\n\t}\n}\n\n/**\n * @class TabList\n * a container for the <tab-button> elements\n */\nclass TabList extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\t}\n\n\tconnectedCallback() {\n\t\tconst _ = this;\n\t\t// additional logic or styling can be added here if desired\n\t}\n}\n\n/**\n * @class TabButton\n * a single tab button element\n */\nclass TabButton extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\t}\n\n\tconnectedCallback() {\n\t\tconst _ = this;\n\t\t// note: role and other attributes are handled by the parent\n\t}\n}\n\n/**\n * @class TabPanel\n * a single tab panel element\n */\nclass TabPanel extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tconst _ = this;\n\t}\n\n\tconnectedCallback() {\n\t\tconst _ = this;\n\t\t// note: role and other attributes are handled by the parent\n\t}\n}\n\n// define the custom elements\ncustomElements.define(\"tab-group\", TabGroup);\ncustomElements.define(\"tab-list\", TabList);\ncustomElements.define(\"tab-button\", TabButton);\ncustomElements.define(\"tab-panel\", TabPanel);\n\n// export the main component\nexport { TabGroup };\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tab-group.esm.js","sources":["../src/tab-group.js"],"sourcesContent":["import './tab-group.css';\n\n/**\n * @module TabGroup\n * A fully accessible tab group web component\n */\n\nlet instanceCount = 0;\n\n/**\n * @class TabGroup\n * the parent container that coordinates tabs and panels\n */\nexport default class TabGroup extends HTMLElement {\n\t/**\n\t * @function ensureConsistentTabsAndPanels\n\t * makes sure there is an equal number of <tab-button> and <tab-panel> elements.\n\t * if there are more panels than tabs, inject extra tab buttons.\n\t * if there are more tabs than panels, inject extra panels.\n\t */\n\tensureConsistentTabsAndPanels() {\n\t\t// get current tabs and panels scoped to direct children only\n\t\tlet tabs = this.querySelectorAll(':scope > tab-list > tab-button');\n\t\tlet panels = this.querySelectorAll(':scope > tab-panel');\n\n\t\t// if there are more panels than tabs\n\t\tif (panels.length > tabs.length) {\n\t\t\tconst difference = panels.length - tabs.length;\n\t\t\t// try to find a <tab-list> to insert new tabs\n\t\t\tlet tabList = this.querySelector(':scope > tab-list');\n\t\t\tif (!tabList) {\n\t\t\t\t// if not present, create one and insert it at the beginning\n\t\t\t\ttabList = document.createElement('tab-list');\n\t\t\t\tthis.insertBefore(tabList, this.firstChild);\n\t\t\t}\n\t\t\t// inject extra <tab-button> elements into the tab list\n\t\t\tfor (let i = 0; i < difference; i++) {\n\t\t\t\tconst newTab = document.createElement('tab-button');\n\t\t\t\tnewTab.textContent = 'default tab';\n\t\t\t\ttabList.appendChild(newTab);\n\t\t\t}\n\t\t}\n\t\t// if there are more tabs than panels\n\t\telse if (tabs.length > panels.length) {\n\t\t\tconst difference = tabs.length - panels.length;\n\t\t\t// inject extra <tab-panel> elements at the end of the tab group\n\t\t\tfor (let i = 0; i < difference; i++) {\n\t\t\t\tconst newPanel = document.createElement('tab-panel');\n\t\t\t\tnewPanel.innerHTML = '<p>default panel content</p>';\n\t\t\t\tthis.appendChild(newPanel);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * called when the element is connected to the dom\n\t */\n\tconnectedCallback() {\n\t\t// assign a stable instance id on first connect\n\t\tif (!this._instanceId) {\n\t\t\tthis._instanceId = `tg-${instanceCount++}`;\n\t\t}\n\n\t\t// ensure that the number of <tab-button> and <tab-panel> elements match\n\t\tthis.ensureConsistentTabsAndPanels();\n\n\t\t// find the <tab-list> element (should be exactly one)\n\t\tthis.tabList = this.querySelector(':scope > tab-list');\n\t\tif (!this.tabList) return;\n\n\t\t// find all <tab-button> elements inside the <tab-list>\n\t\tthis.tabButtons = Array.from(\n\t\t\tthis.tabList.querySelectorAll('tab-button')\n\t\t);\n\n\t\t// find all <tab-panel> elements inside the <tab-group>\n\t\tthis.tabPanels = Array.from(this.querySelectorAll(':scope > tab-panel'));\n\n\t\tconst prefix = this._instanceId;\n\n\t\t// initialize each tab-button with roles, ids and aria attributes\n\t\tthis.tabButtons.forEach((tab, index) => {\n\t\t\tconst tabId = `${prefix}-tab-${index}`;\n\t\t\tconst panelId = `${prefix}-panel-${index}`;\n\t\t\ttab.id = tabId;\n\t\t\ttab.setAttribute('role', 'tab');\n\t\t\ttab.setAttribute('aria-controls', panelId);\n\n\t\t\t// first tab is active by default\n\t\t\tif (index === 0) {\n\t\t\t\ttab.setAttribute('aria-selected', 'true');\n\t\t\t\ttab.setAttribute('tabindex', '0');\n\t\t\t} else {\n\t\t\t\ttab.setAttribute('aria-selected', 'false');\n\t\t\t\ttab.setAttribute('tabindex', '-1');\n\t\t\t}\n\t\t});\n\n\t\t// initialize each tab-panel with roles, ids and aria attributes\n\t\tthis.tabPanels.forEach((panel, index) => {\n\t\t\tconst panelId = `${prefix}-panel-${index}`;\n\t\t\tpanel.id = panelId;\n\t\t\tpanel.setAttribute('role', 'tabpanel');\n\t\t\tpanel.setAttribute('aria-labelledby', `${prefix}-tab-${index}`);\n\n\t\t\t// hide panels except for the first one\n\t\t\tpanel.hidden = index !== 0;\n\t\t});\n\n\t\t// set up keyboard navigation and click delegation on the <tab-list>\n\t\tthis.tabList.setAttribute('role', 'tablist');\n\n\t\t// store bound handlers so we can remove them in disconnectedCallback\n\t\tif (!this._onKeyDown) {\n\t\t\tthis._onKeyDown = (e) => this.onKeyDown(e);\n\t\t\tthis._onClick = (e) => this.onClick(e);\n\t\t}\n\t\tthis.tabList.addEventListener('keydown', this._onKeyDown);\n\t\tthis.tabList.addEventListener('click', this._onClick);\n\t}\n\n\t/**\n\t * called when the element is disconnected from the dom\n\t */\n\tdisconnectedCallback() {\n\t\tif (this._animationController) {\n\t\t\tthis._animationController.abort();\n\t\t\tthis._animationController = null;\n\t\t}\n\t\tif (this.tabList && this._onKeyDown) {\n\t\t\tthis.tabList.removeEventListener('keydown', this._onKeyDown);\n\t\t\tthis.tabList.removeEventListener('click', this._onClick);\n\t\t}\n\t}\n\n\t/**\n\t * reads animation attributes from the element\n\t */\n\t_getAnimateConfig() {\n\t\tconst outClass = this.getAttribute('animate-out-class');\n\t\tconst inClass = this.getAttribute('animate-in-class');\n\t\tconst timeout = parseInt(this.getAttribute('animate-timeout'), 10) || 500;\n\t\treturn { outClass, inClass, timeout, hasAnimation: !!(outClass || inClass) };\n\t}\n\n\t/**\n\t * adds a class and waits for animationend (or timeout), with abort support\n\t */\n\t_waitForAnimation(element, className, timeout, signal) {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (signal.aborted) {\n\t\t\t\tresolve();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\telement.classList.add(className);\n\n\t\t\tlet timer;\n\t\t\tconst cleanup = () => {\n\t\t\t\telement.classList.remove(className);\n\t\t\t\tclearTimeout(timer);\n\t\t\t\telement.removeEventListener('animationend', onEnd);\n\t\t\t\tsignal.removeEventListener('abort', onAbort);\n\t\t\t\tresolve();\n\t\t\t};\n\n\t\t\tconst onEnd = (e) => {\n\t\t\t\tif (e.target === element) cleanup();\n\t\t\t};\n\n\t\t\tconst onAbort = () => cleanup();\n\n\t\t\telement.addEventListener('animationend', onEnd);\n\t\t\tsignal.addEventListener('abort', onAbort);\n\t\t\ttimer = setTimeout(cleanup, timeout);\n\t\t});\n\t}\n\n\t/**\n\t * orchestrates out-animation → swap → in-animation\n\t */\n\tasync _animateTransition(oldPanel, newPanel, config, controller) {\n\t\tconst { signal } = controller;\n\n\t\t// Phase 1: animate out\n\t\tif (config.outClass && oldPanel) {\n\t\t\tawait this._waitForAnimation(oldPanel, config.outClass, config.timeout, signal);\n\t\t}\n\t\tif (signal.aborted) return;\n\n\t\t// Phase 2: swap hidden\n\t\tif (oldPanel) oldPanel.hidden = true;\n\t\tnewPanel.hidden = false;\n\n\t\t// Phase 3: animate in\n\t\tif (config.inClass) {\n\t\t\tif (signal.aborted) return;\n\t\t\t// force reflow so the browser sees the element before animating\n\t\t\tnewPanel.offsetHeight;\n\t\t\tawait this._waitForAnimation(newPanel, config.inClass, config.timeout, signal);\n\t\t}\n\t}\n\n\t/**\n\t * @function setActiveTab\n\t * activates a tab and updates aria attributes\n\t * @param {number} index - index of the tab to activate\n\t */\n\tsetActiveTab(index) {\n\t\tif (index < 0 || index >= this.tabButtons.length) return;\n\t\tconst previousIndex = this.tabButtons.findIndex(\n\t\t\t(tab) => tab.getAttribute('aria-selected') === 'true'\n\t\t);\n\n\t\t// cancel any in-flight animation\n\t\tif (this._animationController) {\n\t\t\tthis._animationController.abort();\n\t\t\tthis._animationController = null;\n\t\t\t// force-hide all panels (clean slate)\n\t\t\tthis.tabPanels.forEach((panel) => {\n\t\t\t\tpanel.hidden = true;\n\t\t\t});\n\t\t}\n\n\t\t// update each tab-button (ARIA updates fire immediately)\n\t\tthis.tabButtons.forEach((tab, i) => {\n\t\t\tconst isActive = i === index;\n\t\t\ttab.setAttribute('aria-selected', isActive ? 'true' : 'false');\n\t\t\ttab.setAttribute('tabindex', isActive ? '0' : '-1');\n\t\t\tif (isActive) {\n\t\t\t\ttab.focus();\n\t\t\t}\n\t\t});\n\n\t\t// dispatch event only if the tab actually changed\n\t\tif (previousIndex !== index) {\n\t\t\tconst detail = {\n\t\t\t\tpreviousIndex,\n\t\t\t\tcurrentIndex: index,\n\t\t\t\tpreviousTab: this.tabButtons[previousIndex],\n\t\t\t\tcurrentTab: this.tabButtons[index],\n\t\t\t\tpreviousPanel: this.tabPanels[previousIndex],\n\t\t\t\tcurrentPanel: this.tabPanels[index],\n\t\t\t};\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent('tabchange', { detail, bubbles: true })\n\t\t\t);\n\t\t}\n\n\t\tconst config = this._getAnimateConfig();\n\t\tconst oldPanel = previousIndex >= 0 ? this.tabPanels[previousIndex] : null;\n\t\tconst newPanel = this.tabPanels[index];\n\n\t\tif (!config.hasAnimation || previousIndex === index) {\n\t\t\t// instant switch (original behavior)\n\t\t\tthis.tabPanels.forEach((panel, i) => {\n\t\t\t\tpanel.hidden = i !== index;\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\t// animated transition\n\t\tconst controller = new AbortController();\n\t\tthis._animationController = controller;\n\n\t\t// old panel was already force-hidden by abort above, so if we aborted\n\t\t// a previous animation, skip animate-out (old panel is already gone)\n\t\tconst skipOut = oldPanel && oldPanel.hidden;\n\n\t\tif (skipOut) {\n\t\t\t// just animate in the new panel\n\t\t\tnewPanel.hidden = false;\n\t\t\tif (config.inClass) {\n\t\t\t\tnewPanel.offsetHeight;\n\t\t\t\tthis._waitForAnimation(newPanel, config.inClass, config.timeout, controller.signal).then(() => {\n\t\t\t\t\tif (this._animationController === controller) {\n\t\t\t\t\t\tthis._animationController = null;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tthis._animationController = null;\n\t\t\t}\n\t\t} else {\n\t\t\t// full out → swap → in sequence\n\t\t\tthis._animateTransition(oldPanel, newPanel, config, controller).then(() => {\n\t\t\t\tif (this._animationController === controller) {\n\t\t\t\t\tthis._animationController = null;\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * @function onClick\n\t * handles click events on the <tab-list> via event delegation\n\t * @param {MouseEvent} e - the click event\n\t */\n\tonClick(e) {\n\t\t// check if the click occurred on or within a <tab-button>\n\t\tconst tabButton = e.target.closest('tab-button');\n\t\tif (!tabButton) return;\n\n\t\t// determine the index of the clicked tab-button\n\t\tconst index = this.tabButtons.indexOf(tabButton);\n\t\tif (index === -1) return;\n\n\t\t// activate the tab with the corresponding index\n\t\tthis.setActiveTab(index);\n\t}\n\n\t/**\n\t * @function onKeyDown\n\t * handles keyboard navigation for the tabs\n\t * @param {KeyboardEvent} e - the keydown event\n\t */\n\tonKeyDown(e) {\n\t\t// only process keys if focus is on a <tab-button>\n\t\tconst targetIndex = this.tabButtons.indexOf(e.target);\n\t\tif (targetIndex === -1) return;\n\n\t\tlet newIndex = targetIndex;\n\t\tswitch (e.key) {\n\t\t\tcase 'ArrowLeft':\n\t\t\tcase 'ArrowUp':\n\t\t\t\t// move to the previous tab (wrap around if necessary)\n\t\t\t\tnewIndex =\n\t\t\t\t\ttargetIndex > 0 ? targetIndex - 1 : this.tabButtons.length - 1;\n\t\t\t\te.preventDefault();\n\t\t\t\tbreak;\n\t\t\tcase 'ArrowRight':\n\t\t\tcase 'ArrowDown':\n\t\t\t\t// move to the next tab (wrap around if necessary)\n\t\t\t\tnewIndex = (targetIndex + 1) % this.tabButtons.length;\n\t\t\t\te.preventDefault();\n\t\t\t\tbreak;\n\t\t\tcase 'Home':\n\t\t\t\t// jump to the first tab\n\t\t\t\tnewIndex = 0;\n\t\t\t\te.preventDefault();\n\t\t\t\tbreak;\n\t\t\tcase 'End':\n\t\t\t\t// jump to the last tab\n\t\t\t\tnewIndex = this.tabButtons.length - 1;\n\t\t\t\te.preventDefault();\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\treturn; // ignore other keys\n\t\t}\n\t\tthis.setActiveTab(newIndex);\n\t}\n}\n\n/**\n * @class TabList\n * a container for the <tab-button> elements\n */\nclass TabList extends HTMLElement {}\n\n/**\n * @class TabButton\n * a single tab button element\n */\nclass TabButton extends HTMLElement {}\n\n/**\n * @class TabPanel\n * a single tab panel element\n */\nclass TabPanel extends HTMLElement {}\n\n// define the custom elements (guarded against double-registration and SSR)\nif (typeof window !== 'undefined' && window.customElements) {\n\tif (!customElements.get('tab-group'))\n\t\tcustomElements.define('tab-group', TabGroup);\n\tif (!customElements.get('tab-list'))\n\t\tcustomElements.define('tab-list', TabList);\n\tif (!customElements.get('tab-button'))\n\t\tcustomElements.define('tab-button', TabButton);\n\tif (!customElements.get('tab-panel'))\n\t\tcustomElements.define('tab-panel', TabPanel);\n}\n"],"names":[],"mappings":"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"}
package/dist/tab-group.js CHANGED
@@ -9,23 +9,13 @@
9
9
  * A fully accessible tab group web component
10
10
  */
11
11
 
12
+ let instanceCount = 0;
13
+
12
14
  /**
13
15
  * @class TabGroup
14
16
  * the parent container that coordinates tabs and panels
15
17
  */
16
18
  class TabGroup extends HTMLElement {
17
- // static counter to ensure global unique ids for tabs and panels
18
- static tabCount = 0;
19
- static panelCount = 0;
20
-
21
- constructor() {
22
- super();
23
- // ensure that the number of <tab-button> and <tab-panel> elements match
24
- // note: in some scenarios the child elements might not be available in the constructor,
25
- // so adjust as necessary or consider running this check in connectedCallback()
26
- this.ensureConsistentTabsAndPanels();
27
- }
28
-
29
19
  /**
30
20
  * @function ensureConsistentTabsAndPanels
31
21
  * makes sure there is an equal number of <tab-button> and <tab-panel> elements.
@@ -33,24 +23,24 @@
33
23
  * if there are more tabs than panels, inject extra panels.
34
24
  */
35
25
  ensureConsistentTabsAndPanels() {
36
- // get current tabs and panels within the tab group
37
- let tabs = this.querySelectorAll("tab-button");
38
- let panels = this.querySelectorAll("tab-panel");
26
+ // get current tabs and panels scoped to direct children only
27
+ let tabs = this.querySelectorAll(':scope > tab-list > tab-button');
28
+ let panels = this.querySelectorAll(':scope > tab-panel');
39
29
 
40
30
  // if there are more panels than tabs
41
31
  if (panels.length > tabs.length) {
42
32
  const difference = panels.length - tabs.length;
43
33
  // try to find a <tab-list> to insert new tabs
44
- let tabList = this.querySelector("tab-list");
34
+ let tabList = this.querySelector(':scope > tab-list');
45
35
  if (!tabList) {
46
36
  // if not present, create one and insert it at the beginning
47
- tabList = document.createElement("tab-list");
37
+ tabList = document.createElement('tab-list');
48
38
  this.insertBefore(tabList, this.firstChild);
49
39
  }
50
40
  // inject extra <tab-button> elements into the tab list
51
41
  for (let i = 0; i < difference; i++) {
52
- const newTab = document.createElement("tab-button");
53
- newTab.textContent = "default tab";
42
+ const newTab = document.createElement('tab-button');
43
+ newTab.textContent = 'default tab';
54
44
  tabList.appendChild(newTab);
55
45
  }
56
46
  }
@@ -59,8 +49,8 @@
59
49
  const difference = tabs.length - panels.length;
60
50
  // inject extra <tab-panel> elements at the end of the tab group
61
51
  for (let i = 0; i < difference; i++) {
62
- const newPanel = document.createElement("tab-panel");
63
- newPanel.innerHTML = "<p>default panel content</p>";
52
+ const newPanel = document.createElement('tab-panel');
53
+ newPanel.innerHTML = '<p>default panel content</p>';
64
54
  this.appendChild(newPanel);
65
55
  }
66
56
  }
@@ -70,58 +60,149 @@
70
60
  * called when the element is connected to the dom
71
61
  */
72
62
  connectedCallback() {
73
- const _ = this;
63
+ // assign a stable instance id on first connect
64
+ if (!this._instanceId) {
65
+ this._instanceId = `tg-${instanceCount++}`;
66
+ }
67
+
68
+ // ensure that the number of <tab-button> and <tab-panel> elements match
69
+ this.ensureConsistentTabsAndPanels();
74
70
 
75
71
  // find the <tab-list> element (should be exactly one)
76
- _.tabList = _.querySelector("tab-list");
77
- if (!_.tabList) return;
72
+ this.tabList = this.querySelector(':scope > tab-list');
73
+ if (!this.tabList) return;
78
74
 
79
75
  // find all <tab-button> elements inside the <tab-list>
80
- _.tabButtons = Array.from(_.tabList.querySelectorAll("tab-button"));
76
+ this.tabButtons = Array.from(
77
+ this.tabList.querySelectorAll('tab-button')
78
+ );
81
79
 
82
80
  // find all <tab-panel> elements inside the <tab-group>
83
- _.tabPanels = Array.from(_.querySelectorAll("tab-panel"));
81
+ this.tabPanels = Array.from(this.querySelectorAll(':scope > tab-panel'));
84
82
 
85
- // initialize each tab-button with roles, ids and aria attributes
86
- _.tabButtons.forEach((tab, index) => {
87
- const tabIndex = TabGroup.tabCount++;
83
+ const prefix = this._instanceId;
88
84
 
89
- // generate a unique id for each tab, e.g. "tab-0", "tab-1", ...
90
- const tabId = `tab-${tabIndex}`;
85
+ // initialize each tab-button with roles, ids and aria attributes
86
+ this.tabButtons.forEach((tab, index) => {
87
+ const tabId = `${prefix}-tab-${index}`;
88
+ const panelId = `${prefix}-panel-${index}`;
91
89
  tab.id = tabId;
92
-
93
- // generate a corresponding panel id, e.g. "panel-0"
94
- const panelId = `panel-${tabIndex}`;
95
- tab.setAttribute("role", "tab");
96
- tab.setAttribute("aria-controls", panelId);
90
+ tab.setAttribute('role', 'tab');
91
+ tab.setAttribute('aria-controls', panelId);
97
92
 
98
93
  // first tab is active by default
99
94
  if (index === 0) {
100
- tab.setAttribute("aria-selected", "true");
101
- tab.setAttribute("tabindex", "0");
95
+ tab.setAttribute('aria-selected', 'true');
96
+ tab.setAttribute('tabindex', '0');
102
97
  } else {
103
- tab.setAttribute("aria-selected", "false");
104
- tab.setAttribute("tabindex", "-1");
98
+ tab.setAttribute('aria-selected', 'false');
99
+ tab.setAttribute('tabindex', '-1');
105
100
  }
106
101
  });
107
102
 
108
103
  // initialize each tab-panel with roles, ids and aria attributes
109
- _.tabPanels.forEach((panel, index) => {
110
- const panelIndex = TabGroup.panelCount++;
111
- const panelId = `panel-${panelIndex}`;
104
+ this.tabPanels.forEach((panel, index) => {
105
+ const panelId = `${prefix}-panel-${index}`;
112
106
  panel.id = panelId;
113
-
114
- panel.setAttribute("role", "tabpanel");
115
- panel.setAttribute("aria-labelledby", `tab-${panelIndex}`);
107
+ panel.setAttribute('role', 'tabpanel');
108
+ panel.setAttribute('aria-labelledby', `${prefix}-tab-${index}`);
116
109
 
117
110
  // hide panels except for the first one
118
111
  panel.hidden = index !== 0;
119
112
  });
120
113
 
121
114
  // set up keyboard navigation and click delegation on the <tab-list>
122
- _.tabList.setAttribute("role", "tablist");
123
- _.tabList.addEventListener("keydown", (e) => _.onKeyDown(e));
124
- _.tabList.addEventListener("click", (e) => _.onClick(e));
115
+ this.tabList.setAttribute('role', 'tablist');
116
+
117
+ // store bound handlers so we can remove them in disconnectedCallback
118
+ if (!this._onKeyDown) {
119
+ this._onKeyDown = (e) => this.onKeyDown(e);
120
+ this._onClick = (e) => this.onClick(e);
121
+ }
122
+ this.tabList.addEventListener('keydown', this._onKeyDown);
123
+ this.tabList.addEventListener('click', this._onClick);
124
+ }
125
+
126
+ /**
127
+ * called when the element is disconnected from the dom
128
+ */
129
+ disconnectedCallback() {
130
+ if (this._animationController) {
131
+ this._animationController.abort();
132
+ this._animationController = null;
133
+ }
134
+ if (this.tabList && this._onKeyDown) {
135
+ this.tabList.removeEventListener('keydown', this._onKeyDown);
136
+ this.tabList.removeEventListener('click', this._onClick);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * reads animation attributes from the element
142
+ */
143
+ _getAnimateConfig() {
144
+ const outClass = this.getAttribute('animate-out-class');
145
+ const inClass = this.getAttribute('animate-in-class');
146
+ const timeout = parseInt(this.getAttribute('animate-timeout'), 10) || 500;
147
+ return { outClass, inClass, timeout, hasAnimation: !!(outClass || inClass) };
148
+ }
149
+
150
+ /**
151
+ * adds a class and waits for animationend (or timeout), with abort support
152
+ */
153
+ _waitForAnimation(element, className, timeout, signal) {
154
+ return new Promise((resolve) => {
155
+ if (signal.aborted) {
156
+ resolve();
157
+ return;
158
+ }
159
+
160
+ element.classList.add(className);
161
+
162
+ let timer;
163
+ const cleanup = () => {
164
+ element.classList.remove(className);
165
+ clearTimeout(timer);
166
+ element.removeEventListener('animationend', onEnd);
167
+ signal.removeEventListener('abort', onAbort);
168
+ resolve();
169
+ };
170
+
171
+ const onEnd = (e) => {
172
+ if (e.target === element) cleanup();
173
+ };
174
+
175
+ const onAbort = () => cleanup();
176
+
177
+ element.addEventListener('animationend', onEnd);
178
+ signal.addEventListener('abort', onAbort);
179
+ timer = setTimeout(cleanup, timeout);
180
+ });
181
+ }
182
+
183
+ /**
184
+ * orchestrates out-animation → swap → in-animation
185
+ */
186
+ async _animateTransition(oldPanel, newPanel, config, controller) {
187
+ const { signal } = controller;
188
+
189
+ // Phase 1: animate out
190
+ if (config.outClass && oldPanel) {
191
+ await this._waitForAnimation(oldPanel, config.outClass, config.timeout, signal);
192
+ }
193
+ if (signal.aborted) return;
194
+
195
+ // Phase 2: swap hidden
196
+ if (oldPanel) oldPanel.hidden = true;
197
+ newPanel.hidden = false;
198
+
199
+ // Phase 3: animate in
200
+ if (config.inClass) {
201
+ if (signal.aborted) return;
202
+ // force reflow so the browser sees the element before animating
203
+ newPanel.offsetHeight;
204
+ await this._waitForAnimation(newPanel, config.inClass, config.timeout, signal);
205
+ }
125
206
  }
126
207
 
127
208
  /**
@@ -130,35 +211,86 @@
130
211
  * @param {number} index - index of the tab to activate
131
212
  */
132
213
  setActiveTab(index) {
133
- const _ = this;
134
- const previousIndex = _.tabButtons.findIndex(tab => tab.getAttribute("aria-selected") === "true");
214
+ if (index < 0 || index >= this.tabButtons.length) return;
215
+ const previousIndex = this.tabButtons.findIndex(
216
+ (tab) => tab.getAttribute('aria-selected') === 'true'
217
+ );
218
+
219
+ // cancel any in-flight animation
220
+ if (this._animationController) {
221
+ this._animationController.abort();
222
+ this._animationController = null;
223
+ // force-hide all panels (clean slate)
224
+ this.tabPanels.forEach((panel) => {
225
+ panel.hidden = true;
226
+ });
227
+ }
135
228
 
136
- // update each tab-button
137
- _.tabButtons.forEach((tab, i) => {
229
+ // update each tab-button (ARIA updates fire immediately)
230
+ this.tabButtons.forEach((tab, i) => {
138
231
  const isActive = i === index;
139
- tab.setAttribute("aria-selected", isActive ? "true" : "false");
140
- tab.setAttribute("tabindex", isActive ? "0" : "-1");
232
+ tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
233
+ tab.setAttribute('tabindex', isActive ? '0' : '-1');
141
234
  if (isActive) {
142
235
  tab.focus();
143
236
  }
144
237
  });
145
238
 
146
- // update each tab-panel
147
- _.tabPanels.forEach((panel, i) => {
148
- panel.hidden = i !== index;
149
- });
150
-
151
239
  // dispatch event only if the tab actually changed
152
240
  if (previousIndex !== index) {
153
241
  const detail = {
154
242
  previousIndex,
155
243
  currentIndex: index,
156
- previousTab: _.tabButtons[previousIndex],
157
- currentTab: _.tabButtons[index],
158
- previousPanel: _.tabPanels[previousIndex],
159
- currentPanel: _.tabPanels[index]
244
+ previousTab: this.tabButtons[previousIndex],
245
+ currentTab: this.tabButtons[index],
246
+ previousPanel: this.tabPanels[previousIndex],
247
+ currentPanel: this.tabPanels[index],
160
248
  };
161
- _.dispatchEvent(new CustomEvent('tabchange', { detail, bubbles: true }));
249
+ this.dispatchEvent(
250
+ new CustomEvent('tabchange', { detail, bubbles: true })
251
+ );
252
+ }
253
+
254
+ const config = this._getAnimateConfig();
255
+ const oldPanel = previousIndex >= 0 ? this.tabPanels[previousIndex] : null;
256
+ const newPanel = this.tabPanels[index];
257
+
258
+ if (!config.hasAnimation || previousIndex === index) {
259
+ // instant switch (original behavior)
260
+ this.tabPanels.forEach((panel, i) => {
261
+ panel.hidden = i !== index;
262
+ });
263
+ return;
264
+ }
265
+
266
+ // animated transition
267
+ const controller = new AbortController();
268
+ this._animationController = controller;
269
+
270
+ // old panel was already force-hidden by abort above, so if we aborted
271
+ // a previous animation, skip animate-out (old panel is already gone)
272
+ const skipOut = oldPanel && oldPanel.hidden;
273
+
274
+ if (skipOut) {
275
+ // just animate in the new panel
276
+ newPanel.hidden = false;
277
+ if (config.inClass) {
278
+ newPanel.offsetHeight;
279
+ this._waitForAnimation(newPanel, config.inClass, config.timeout, controller.signal).then(() => {
280
+ if (this._animationController === controller) {
281
+ this._animationController = null;
282
+ }
283
+ });
284
+ } else {
285
+ this._animationController = null;
286
+ }
287
+ } else {
288
+ // full out → swap → in sequence
289
+ this._animateTransition(oldPanel, newPanel, config, controller).then(() => {
290
+ if (this._animationController === controller) {
291
+ this._animationController = null;
292
+ }
293
+ });
162
294
  }
163
295
  }
164
296
 
@@ -168,17 +300,16 @@
168
300
  * @param {MouseEvent} e - the click event
169
301
  */
170
302
  onClick(e) {
171
- const _ = this;
172
303
  // check if the click occurred on or within a <tab-button>
173
- const tabButton = e.target.closest("tab-button");
304
+ const tabButton = e.target.closest('tab-button');
174
305
  if (!tabButton) return;
175
306
 
176
307
  // determine the index of the clicked tab-button
177
- const index = _.tabButtons.indexOf(tabButton);
308
+ const index = this.tabButtons.indexOf(tabButton);
178
309
  if (index === -1) return;
179
310
 
180
311
  // activate the tab with the corresponding index
181
- _.setActiveTab(index);
312
+ this.setActiveTab(index);
182
313
  }
183
314
 
184
315
  /**
@@ -187,39 +318,39 @@
187
318
  * @param {KeyboardEvent} e - the keydown event
188
319
  */
189
320
  onKeyDown(e) {
190
- const _ = this;
191
321
  // only process keys if focus is on a <tab-button>
192
- const targetIndex = _.tabButtons.indexOf(e.target);
322
+ const targetIndex = this.tabButtons.indexOf(e.target);
193
323
  if (targetIndex === -1) return;
194
324
 
195
325
  let newIndex = targetIndex;
196
326
  switch (e.key) {
197
- case "ArrowLeft":
198
- case "ArrowUp":
327
+ case 'ArrowLeft':
328
+ case 'ArrowUp':
199
329
  // move to the previous tab (wrap around if necessary)
200
- newIndex = targetIndex > 0 ? targetIndex - 1 : _.tabButtons.length - 1;
330
+ newIndex =
331
+ targetIndex > 0 ? targetIndex - 1 : this.tabButtons.length - 1;
201
332
  e.preventDefault();
202
333
  break;
203
- case "ArrowRight":
204
- case "ArrowDown":
334
+ case 'ArrowRight':
335
+ case 'ArrowDown':
205
336
  // move to the next tab (wrap around if necessary)
206
- newIndex = (targetIndex + 1) % _.tabButtons.length;
337
+ newIndex = (targetIndex + 1) % this.tabButtons.length;
207
338
  e.preventDefault();
208
339
  break;
209
- case "Home":
340
+ case 'Home':
210
341
  // jump to the first tab
211
342
  newIndex = 0;
212
343
  e.preventDefault();
213
344
  break;
214
- case "End":
345
+ case 'End':
215
346
  // jump to the last tab
216
- newIndex = _.tabButtons.length - 1;
347
+ newIndex = this.tabButtons.length - 1;
217
348
  e.preventDefault();
218
349
  break;
219
350
  default:
220
351
  return; // ignore other keys
221
352
  }
222
- _.setActiveTab(newIndex);
353
+ this.setActiveTab(newIndex);
223
354
  }
224
355
  }
225
356
 
@@ -227,51 +358,32 @@
227
358
  * @class TabList
228
359
  * a container for the <tab-button> elements
229
360
  */
230
- class TabList extends HTMLElement {
231
- constructor() {
232
- super();
233
- }
234
-
235
- connectedCallback() {
236
- // additional logic or styling can be added here if desired
237
- }
238
- }
361
+ class TabList extends HTMLElement {}
239
362
 
240
363
  /**
241
364
  * @class TabButton
242
365
  * a single tab button element
243
366
  */
244
- class TabButton extends HTMLElement {
245
- constructor() {
246
- super();
247
- }
248
-
249
- connectedCallback() {
250
- // note: role and other attributes are handled by the parent
251
- }
252
- }
367
+ class TabButton extends HTMLElement {}
253
368
 
254
369
  /**
255
370
  * @class TabPanel
256
371
  * a single tab panel element
257
372
  */
258
- class TabPanel extends HTMLElement {
259
- constructor() {
260
- super();
261
- }
262
-
263
- connectedCallback() {
264
- // note: role and other attributes are handled by the parent
265
- }
373
+ class TabPanel extends HTMLElement {}
374
+
375
+ // define the custom elements (guarded against double-registration and SSR)
376
+ if (typeof window !== 'undefined' && window.customElements) {
377
+ if (!customElements.get('tab-group'))
378
+ customElements.define('tab-group', TabGroup);
379
+ if (!customElements.get('tab-list'))
380
+ customElements.define('tab-list', TabList);
381
+ if (!customElements.get('tab-button'))
382
+ customElements.define('tab-button', TabButton);
383
+ if (!customElements.get('tab-panel'))
384
+ customElements.define('tab-panel', TabPanel);
266
385
  }
267
386
 
268
- // define the custom elements
269
- customElements.define("tab-group", TabGroup);
270
- customElements.define("tab-list", TabList);
271
- customElements.define("tab-button", TabButton);
272
- customElements.define("tab-panel", TabPanel);
273
-
274
- exports.TabGroup = TabGroup;
275
387
  exports.default = TabGroup;
276
388
 
277
389
  Object.defineProperty(exports, '__esModule', { value: true });