@georapbox/a-tab-group
Version:
A custom element to create a group of tabs and tab panels.
266 lines (226 loc) • 16.9 kB
JavaScript
/*!
* @georapbox/a-tab-group
* A custom element to create a group of tabs and tab panels.
*
* @version 2.4.1
* @homepage https://github.com/georapbox/a-tab-group#readme
* @author George Raptis <georapbox@gmail.com>
* @license MIT
*/
var h=(a="",t="")=>{let e=typeof a=="string"&&a!==""?a+"-":"",s=typeof t=="string"&&t!==""?"-"+t:"",i=Math.random().toString(36).substring(2,8);return`${e}${i}${s}`};var b=(a,t)=>{if(Object.prototype.hasOwnProperty.call(t,a)){let e=t[a];delete t[a],t[a]=e}};var E=0,C=`
:host {
box-sizing: border-box;
display: inline-block;
contain: content;
}
:host([hidden]),
[hidden] {
display: none !important;
}
:host *,
:host *::before,
:host *::after {
box-sizing: inherit;
}
.tab {
display: inline-flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.375rem 0.75rem;
white-space: nowrap;
cursor: pointer;
}
:host([disabled]) .tab {
opacity: 0.7;
cursor: not-allowed;
}
:host([selected]) .tab {
color: var(--selected-tab-color);
background-color: var(--selected-tab-bg-color);
}
.tab__close {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
font-size: inherit;
cursor: pointer;
}
`,w=document.createElement("template");w.innerHTML=`
<style>
${C}
</style>
<div part="base" class="tab">
<slot></slot>
</div>
`;var u=class a extends HTMLElement{constructor(){super(),this.shadowRoot||this.attachShadow({mode:"open"}).appendChild(w.content.cloneNode(!0))}static get observedAttributes(){return["selected","disabled","closable"]}attributeChangedCallback(t,e,s){if(t==="selected"&&e!==s&&(this.setAttribute("aria-selected",this.selected.toString()),this.setAttribute("tabindex",this.disabled||!this.selected?"-1":"0")),t==="disabled"&&e!==s&&(this.setAttribute("aria-disabled",this.disabled.toString()),this.setAttribute("tabindex",this.disabled||!this.selected?"-1":"0")),t==="closable"&&e!==s)if(this.closable){let i=document.createElement("span");i.className="tab__close",i.setAttribute("part","close-tab"),i.innerHTML='<svg part="close-tab-icon" xmlns="http://www.w3.org/2000/svg" width="0.875em" height="0.875em" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/></svg>',this.shadowRoot?.querySelector(".tab")?.appendChild(i),i.addEventListener("click",this.#e)}else{let i=this.shadowRoot?.querySelector(".tab__close");i?.removeEventListener("click",this.#e),i?.remove()}}connectedCallback(){this.#s("selected"),this.#s("disabled"),this.#s("closable"),this.id||(this.id=h("tab",(++E).toString())),this.setAttribute("slot","tab"),this.setAttribute("role","tab"),this.setAttribute("aria-selected","false"),this.setAttribute("tabindex",this.disabled||!this.selected?"-1":"0")}disconnectedCallback(){this.shadowRoot?.querySelector(".tab__close")?.removeEventListener("click",this.#e)}get selected(){return this.hasAttribute("selected")}set selected(t){this.toggleAttribute("selected",!!t)}get disabled(){return this.hasAttribute("disabled")}set disabled(t){this.toggleAttribute("disabled",!!t)}get closable(){return this.hasAttribute("closable")}set closable(t){this.toggleAttribute("closable",!!t)}#e=t=>{t.stopPropagation(),this.dispatchEvent(new CustomEvent("a-tab-close",{bubbles:!0,composed:!0,detail:{tabId:this.id}}))};#s(t){return b(t,this)}static defineCustomElement(t="a-tab"){typeof window<"u"&&!window.customElements.get(t)&&window.customElements.define(t,a)}};u.defineCustomElement();var L=0,S=`
:host {
box-sizing: border-box;
display: block;
contain: content;
}
:host([hidden]),
[hidden] {
display: none !important;
}
:host *,
:host *::before,
:host *::after {
box-sizing: inherit;
}
`,T=document.createElement("template");T.innerHTML=`
<style>
${S}
</style>
<div part="base" class="tab-panel">
<slot></slot>
</div>
`;var p=class a extends HTMLElement{constructor(){super(),this.shadowRoot||this.attachShadow({mode:"open"}).appendChild(T.content.cloneNode(!0))}connectedCallback(){this.setAttribute("slot","panel"),this.setAttribute("role","tabpanel"),this.setAttribute("hidden",""),this.id||(this.id=h("panel",(++L).toString()))}static defineCustomElement(t="a-tab-panel"){typeof window<"u"&&!window.customElements.get(t)&&window.customElements.define(t,a)}};p.defineCustomElement();var y=200,n={TOP:"top",BOTTOM:"bottom",START:"start",END:"end"},l={LTR:"ltr",RTL:"rtl"},R=Object.entries(n).map(([,a])=>a),r={AUTO:"auto",MANUAL:"manual"},c={DOWN:"ArrowDown",LEFT:"ArrowLeft",RIGHT:"ArrowRight",UP:"ArrowUp",HOME:"Home",END:"End",ENTER:"Enter",SPACE:" "},x=`
:host {
--selected-tab-color: #005fcc;
--selected-tab-bg-color: transparent;
--tabs-scroll-behavior: smooth;
--scroll-button-width: 2.125em;
--scroll-button-height: 2.125em;
--scroll-button-inline-offset: 0rem;
box-sizing: border-box;
display: block;
contain: content;
}
(prefers-reduced-motion: reduce) {
:host {
--tabs-scroll-behavior: auto;
}
}
:host([hidden]),
[hidden],
::slotted([hidden]) {
display: none !important;
}
:host *,
:host *::before,
:host *::after {
box-sizing: inherit;
}
.tab-group {
display: flex;
width: 100%;
}
.tab-group__nav {
position: relative;
}
.tab-group__nav--has-scroll-controls {
padding: 0 calc(var(--scroll-button-width) + var(--scroll-button-inline-offset));
}
.tab-group__scroll-button {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 50%;
transform: translateY(-50%);
width: var(--scroll-button-width);
height: var(--scroll-button-height);
padding: 0; /* Required for iOS, otherwise the svg is not visible: https://stackoverflow.com/questions/66532071/flex-svg-behaving-strange-in-ios-safari-14-0-3 */
border: 0;
z-index: 1;
background-color: transparent;
font-size: inherit;
cursor: pointer;
color: currentColor;
}
.tab-group__scroll-button--start {
left: var(--scroll-button-inline-offset);
}
.tab-group__scroll-button--end {
right: var(--scroll-button-inline-offset);
}
:host([dir="${l.RTL}"]) .tab-group__scroll-button--start,
:host(:dir(${l.RTL})) .tab-group__scroll-button--start {
right: var(--scroll-button-inline-offset);
left: auto;
transform: translateY(-50%) rotate(180deg);
}
:host([dir="${l.RTL}"]) .tab-group__scroll-button--end,
:host(:dir(${l.RTL})) .tab-group__scroll-button--end {
left: var(--scroll-button-inline-offset);
right: auto;
transform: translateY(-50%) rotate(180deg);
}
.tab-group__tabs {
display: flex;
padding: 0.25rem;
overflow-x: auto;
scroll-behavior: var(--tabs-scroll-behavior);
scrollbar-width: none;
}
.tab-group__tabs::-webkit-scrollbar {
display: none;
}
.tab-group__panels {
padding: 1rem 0;
}
/* placement="top" */
.tab-group,
:host([placement="${n.TOP}"]) .tab-group {
flex-direction: column;
}
/* placement="bottom" */
:host([placement="${n.BOTTOM}"]) .tab-group {
flex-direction: column;
}
:host([placement="${n.BOTTOM}"]) .tab-group__nav {
order: 1;
}
/* placement="start" */
:host([placement="${n.START}"]) .tab-group {
flex-direction: row;
}
:host([placement="${n.START}"]) .tab-group__tabs {
flex-direction: column;
align-items: flex-start;
}
:host([placement="${n.START}"]) .tab-group__panels {
flex: 1;
padding: 0 1rem;
}
/* placement="end" */
:host([placement="${n.END}"]) .tab-group {
flex-direction: row;
}
:host([placement="${n.END}"]) .tab-group__nav {
order: 1;
}
:host([placement="${n.END}"]) .tab-group__tabs {
flex-direction: column;
align-items: flex-start;
}
:host([placement="${n.END}"]) .tab-group__panels {
flex: 1;
padding: 0 1rem;
}
`,A=document.createElement("template");A.innerHTML=`
<style>${x}</style>
<div part="base" class="tab-group">
<div part="nav" class="tab-group__nav">
<button type="button" part="scroll-button scroll-button--start" class="tab-group__scroll-button tab-group__scroll-button--start" aria-label="Scroll to start">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" part="scroll-button-icon">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
</svg>
</button>
<div part="tabs" class="tab-group__tabs" role="tablist" tabindex="-1">
<slot name="tab"></slot>
</div>
<button type="button" part="scroll-button scroll-button--end" class="tab-group__scroll-button tab-group__scroll-button--end" aria-label="Scroll to end">
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1em" fill="currentColor" viewBox="0 0 16 16" part="scroll-button-icon">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
</button>
</div>
<div part="panels" class="tab-group__panels">
<slot name="panel"></slot>
</div>
</div>
`;var f=class a extends HTMLElement{#e=null;#s=null;#l=!1;constructor(){super(),this.shadowRoot||this.attachShadow({mode:"open"}).appendChild(A.content.cloneNode(!0))}static get observedAttributes(){return["placement","no-scroll-controls"]}attributeChangedCallback(t,e,s){t==="placement"&&e!==s&&this.#i(),t==="no-scroll-controls"&&e!==s&&this.#i()}get placement(){return this.getAttribute("placement")||n.TOP}set placement(t){t!=null&&this.setAttribute("placement",t)}get noScrollControls(){return this.hasAttribute("no-scroll-controls")}set noScrollControls(t){this.toggleAttribute("no-scroll-controls",!!t)}get scrollDistance(){let t=Number(this.getAttribute("scroll-distance"));return Math.abs(t)||y}set scrollDistance(t){this.setAttribute("scroll-distance",Math.abs(t).toString()||y.toString())}get activation(){return this.getAttribute("activation")||r.AUTO}set activation(t){this.setAttribute("activation",t||r.AUTO)}get noTabCycling(){return this.hasAttribute("no-tab-cycling")}set noTabCycling(t){this.toggleAttribute("no-tab-cycling",!!t)}connectedCallback(){this.#o("placement"),this.#o("noScrollControls"),this.#o("scrollDistance"),this.#o("activation"),this.#o("noTabCycling");let t=this.shadowRoot?.querySelector("slot[name=tab]"),e=this.shadowRoot?.querySelector("slot[name=panel]"),s=this.shadowRoot?.querySelector(".tab-group__tabs"),i=this.shadowRoot?.querySelector(".tab-group__nav"),o=Array.from(this.shadowRoot?.querySelectorAll(".tab-group__scroll-button")||[]);t?.addEventListener("slotchange",this.#n),e?.addEventListener("slotchange",this.#n),s?.addEventListener("click",this.#g),s?.addEventListener("keydown",this.#p),o.forEach(d=>d.addEventListener("click",this.#f)),this.addEventListener("a-tab-close",this.#m),"ResizeObserver"in window&&(this.#e=new ResizeObserver(d=>{this.#s=window.requestAnimationFrame(()=>{let v=d?.[0]?.target,g=v?.scrollWidth>v?.clientWidth;o.forEach(_=>_.toggleAttribute("hidden",!g)),i?.part.toggle("nav--has-scroll-controls",g),i?.classList.toggle("tab-group__nav--has-scroll-controls",g)})})),this.#h(),this.#i()}disconnectedCallback(){let t=this.shadowRoot?.querySelector("slot[name=tab]"),e=this.shadowRoot?.querySelector("slot[name=panel]"),s=this.shadowRoot?.querySelector(".tab-group__tabs"),i=Array.from(this.shadowRoot?.querySelectorAll(".tab-group__scroll-button")||[]);t?.removeEventListener("slotchange",this.#n),e?.removeEventListener("slotchange",this.#n),s?.removeEventListener("click",this.#g),s?.removeEventListener("keydown",this.#p),i.forEach(o=>o.removeEventListener("click",this.#f)),this.removeEventListener("a-tab-close",this.#m),this.#c()}#v(){if(!this.#e)return;let t=this.shadowRoot?.querySelector(".tab-group__tabs");t&&(this.#e.unobserve(t),this.#e.observe(t))}#c(){this.#e&&(this.#e.disconnect(),this.#s!==null&&(window.cancelAnimationFrame(this.#s),this.#s=null))}#d(){return window.CSS.supports("selector(:dir(ltr))")?this.matches(":dir(ltr)")?l.LTR:l.RTL:window.getComputedStyle(this).direction||l.LTR}#h(){this.hidden=this.#t().length===0}#w(){let t=this.#t();this.#h(),t.forEach(e=>{let s=e.nextElementSibling;if(!s||s.tagName.toLowerCase()!=="a-tab-panel")return console.error(`Tab #${e.id} is not a sibling of a <a-tab-panel>`);e.setAttribute("aria-controls",s.id),s.setAttribute("aria-labelledby",e.id)})}#T(){return Array.from(this.querySelectorAll("a-tab-panel"))}#t(){return Array.from(this.querySelectorAll("a-tab"))}#b(t){let e=t.getAttribute("aria-controls");return this.querySelector(`#${e}`)}#y(){return this.#t().find(e=>!e.disabled)||null}#A(){let t=this.#t();for(let e=t.length-1;e>=0;e--)if(!t[e].disabled)return t[e];return null}#a(){let t=this.#t(),e=this.activation===r.MANUAL?t.findIndex(s=>s.matches(":focus"))-1:t.findIndex(s=>s.selected)-1;for(;t[(e+t.length)%t.length].disabled;)e--;return this.noTabCycling&&e<0?null:t[(e+t.length)%t.length]}#r(){let t=this.#t(),e=this.activation===r.MANUAL?t.findIndex(s=>s.matches(":focus"))+1:t.findIndex(s=>s.selected)+1;for(;t[e%t.length].disabled;)e++;return this.noTabCycling&&e>=t.length?null:t[e%t.length]}#_(){let t=this.#t(),e=this.#T();t.forEach(s=>s.selected=!1),e.forEach(s=>s.hidden=!0)}#i(){let t=this.shadowRoot?.querySelector(".tab-group__nav"),e=this.shadowRoot?.querySelector(".tab-group__tabs"),s=Array.from(this.shadowRoot?.querySelectorAll(".tab-group__scroll-button")||[]);this.noScrollControls||this.placement===n.START||this.placement===n.END?(this.#c(),s.forEach(i=>i.hidden=!0),t?.part.remove("nav--has-scroll-controls"),t?.classList.remove("tab-group__nav--has-scroll-controls"),e?.setAttribute("aria-orientation","vertical")):(this.#v(),s.forEach(i=>i.hidden=!1),e?.setAttribute("aria-orientation","horizontal"))}#E(){let t=this.#t(),e=t.find(s=>s.selected&&!s.disabled)||t.find(s=>!s.disabled);e&&(this.#l&&!e.selected&&this.dispatchEvent(new CustomEvent("a-tab-show",{bubbles:!0,composed:!0,detail:{tabId:e.id}})),this.#u(e))}#u(t){this.#_(),t&&(t.selected=!0);let e=this.#b(t);e&&(e.hidden=!1)}#n=t=>{this.#w(),this.#i(),this.#E(),t.target.name==="tab"&&(this.#l=!0)};#p=t=>{if(t.target.tagName.toLowerCase()!=="a-tab"||t.altKey)return;let e=R.includes(this.placement||"")?this.placement:n.TOP,s=[n.TOP,n.BOTTOM].includes(e||"")?"horizontal":"vertical",i=this.#d(),o=null;switch(t.key){case c.LEFT:s==="horizontal"&&(o=i===l.LTR?this.#a():this.#r(),o&&(this.activation===r.MANUAL?o.focus():this.selectTab(o)));break;case c.RIGHT:s==="horizontal"&&(o=i===l.LTR?this.#r():this.#a(),o&&(this.activation===r.MANUAL?o.focus():this.selectTab(o)));break;case c.UP:s==="vertical"&&(o=this.#a(),o&&(this.activation===r.MANUAL?o.focus():this.selectTab(o)));break;case c.DOWN:s==="vertical"&&(o=this.#r(),o&&(this.activation===r.MANUAL?o.focus():this.selectTab(o)));break;case c.HOME:o=this.#y(),o&&(this.activation===r.MANUAL?o.focus():this.selectTab(o));break;case c.END:o=this.#A(),o&&(this.activation===r.MANUAL?o.focus():this.selectTab(o));break;case c.ENTER:case c.SPACE:o=t.target,o&&this.selectTab(o);break;default:return}t.preventDefault()};#g=t=>{let e=t.target.closest("a-tab");e&&this.selectTab(e)};#f=t=>{let e=t.target.closest(".tab-group__scroll-button"),s=this.shadowRoot?.querySelector(".tab-group__tabs");if(!e||!s)return;let i=e.classList.contains("tab-group__scroll-button--start"),o=this.#d()===l.LTR,d=i?o?-1:1:o?1:-1,m=s.scrollLeft;s.scrollTo({left:m+d*this.scrollDistance})};#m=t=>{let e=t.target,s=this.#b(e);e&&(e.remove(),e.selected&&this.dispatchEvent(new CustomEvent("a-tab-hide",{bubbles:!0,composed:!0,detail:{tabId:e.id}}))),s&&s.tagName.toLowerCase()==="a-tab-panel"&&s.remove()};#o(t){return b(t,this)}selectTabByIndex(t){let s=this.#t()[t];s&&this.selectTab(s)}selectTabById(t){let s=this.#t().find(i=>i.id===t);s&&this.selectTab(s)}selectTab(t){let e=this.#t().find(s=>s.selected);!t||t.disabled||t.selected||t.tagName.toLowerCase()!=="a-tab"||(this.#u(t),window.requestAnimationFrame(()=>{t.scrollIntoView({inline:"nearest",block:"nearest"}),t.focus()}),e&&this.dispatchEvent(new CustomEvent("a-tab-hide",{bubbles:!0,composed:!0,detail:{tabId:e.id}})),this.dispatchEvent(new CustomEvent("a-tab-show",{bubbles:!0,composed:!0,detail:{tabId:t.id}})))}static defineCustomElement(t="a-tab-group"){typeof window<"u"&&!window.customElements.get(t)&&window.customElements.define(t,a)}};f.defineCustomElement();export{u as ATab,f as ATabGroup,p as ATabPanel};
//# sourceMappingURL=a-tab-group.js.map