Skip to content

Commit

Permalink
Merge pull request #4 from georapbox/no-tab-cycling
Browse files Browse the repository at this point in the history
No tab cycling
  • Loading branch information
georapbox authored Jan 3, 2024
2 parents 66b0b93 + 2fa7444 commit 95c837f
Show file tree
Hide file tree
Showing 8 changed files with 431 additions and 45 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## v2.2.0 (2024-01-03)

- Added `no-tab-cycling` attribute to `a-tab-group` element, which disables tab cycling when the user reaches the first or last tab using the keyboard.

## v2.1.0 (2023-12-18)

## Bug Fixes
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ By default, the component comes with the bare minimum styling. However, you can
| `noScrollControls`<br>*`no-scroll-controls`* || boolean | - | `false` | Disables the scroll buttons that appear when tabs overflow. |
| `scrollDistance`<br>*`scroll-distance`* || number | - | `200` | The distance to scroll when the scroll buttons are clicked. It fallsback to the default value if not provided, or its value is `0`. |
| `activation` || `'auto' \| 'manual'` | - | `'auto'` | If set to `'auto'`, navigating the tabs using the keyboard (`Left`, `Right`, `Up`, `Down`, `Home`, `End` arrow keys) will automatically select the tab. If set to `'manual'`, the tab will receive focus but will not be selected until the user presses the `Enter` or `Space` key. |
| `noTabCycling`<br>*`no-tab-cycling`* || boolean | - | `false` | Disables tab cycling when the user reaches the first or last tab using the keyboard. |

#### &lt;a-tab&gt; properties

Expand Down Expand Up @@ -127,7 +128,7 @@ By default, the component comes with the bare minimum styling. However, you can
| `selectTabByIndex`<sup>1</sup> | Instance | Selects the tab at the given index. If the tab at the given index is disabled or already selected, this method does nothing. | `index: number` |
| `selectTabById`<sup>1</sup> | Instance | Selects the tab with the given id. If the tab with the given id is disabled or already selected, this method does nothing. This is mostly useful if you provide your own ids to the tabs. | `id: string` |

<sup>1</sup> These methods are only available after the component has been defined. If you need to call these methods before the component has been defined, you can use the `whenDefined` method of the `CustomElementRegistry` interface. For example:
<sup>1</sup> Instance methods are only available after the component has been defined. To ensure that the components have been defined, you can use the `whenDefined` method of the `CustomElementRegistry` interface. For example:

```js
Promise.all([
Expand All @@ -144,7 +145,7 @@ Promise.all([
| Name | Description | Event Detail |
| ---- | ----------- | ------------ |
| `a-tab-show` | Emitted when a tab is shown (not in the initial render). | `{tabId: string}` |
| `a-tab-hide` | Emitted when a tab is hidden. It is also emitted if the user closes a closable tab. | `{tabId: string}` |
| `a-tab-hide` | Emitted when a tab is hidden. It is also emitted if the user closes a closable tab which is currently selected. | `{tabId: string}` |
| `a-tab-close` | Emitted when a tab is closed by the user (if the tab is closable). | `{tabId: string}` |

## Changelog
Expand Down
6 changes: 6 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ <h2>Demo</h2>
<label for="no-scroll-controls">no-scroll-controls</label>
</div>
</div>
<div class="form-col" style="flex: initial;">
<div>
<input type="checkbox" id="no-tab-cycling" name="no-tab-cycling">
<label for="no-tab-cycling">no-tab-cycling</label>
</div>
</div>
</div>
</fieldset>

Expand Down
2 changes: 1 addition & 1 deletion docs/lib/a-tab-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,4 @@ let t=(t="",e="")=>{let s=Math.random().toString(36).substring(2,8);return`${"st
<slot name="panel"></slot>
</div>
</div>
`;class m extends HTMLElement{#s=null;#o=null;#a=!1;constructor(){super(),this.shadowRoot||this.attachShadow({mode:"open"}).appendChild(g.content.cloneNode(!0))}static get observedAttributes(){return["placement","no-scroll-controls"]}attributeChangedCallback(t,e,s){"placement"===t&&e!==s&&this.#l(),"no-scroll-controls"===t&&e!==s&&this.#l()}get placement(){return this.getAttribute("placement")}set placement(t){null!=t&&this.setAttribute("placement",t)}get noScrollControls(){return this.hasAttribute("no-scroll-controls")}set noScrollControls(t){this.toggleAttribute("no-scroll-controls",!!t)}get scrollDistance(){return Math.abs(Number(this.getAttribute("scroll-distance")))||200}set scrollDistance(t){this.setAttribute("scroll-distance",Math.abs(t).toString()||"200")}get activation(){return this.getAttribute("activation")||b.AUTO}set activation(t){this.setAttribute("activation",t||b.AUTO)}connectedCallback(){this.#e("placement"),this.#e("noScrollControls"),this.#e("scrollDistance"),this.#e("activation");let t=this.shadowRoot?.querySelector("slot[name=tab]"),e=this.shadowRoot?.querySelector("slot[name=panel]"),s=this.shadowRoot?.querySelector(".tab-group__tabs"),o=this.shadowRoot?.querySelector(".tab-group__nav"),a=Array.from(this.shadowRoot?.querySelectorAll(".tab-group__scroll-button")||[]);t?.addEventListener("slotchange",this.#i),e?.addEventListener("slotchange",this.#i),s?.addEventListener("click",this.#r),s?.addEventListener("keydown",this.#n),a.forEach(t=>t.addEventListener("click",this.#c)),this.addEventListener("a-tab-close",this.#d),"ResizeObserver"in window&&(this.#s=new ResizeObserver(t=>{this.#o=window.requestAnimationFrame(()=>{let e=t?.[0],s=e?.target,l=s?.scrollWidth>s?.clientWidth;a.forEach(t=>t.toggleAttribute("hidden",!l)),o?.part.toggle("nav--has-scroll-controls",l),o?.classList.toggle("tab-group__nav--has-scroll-controls",l)})})),this.#h(),this.#l(),this.placement=h.includes(this.placement||"")?this.placement:d.TOP}disconnectedCallback(){let t=this.shadowRoot?.querySelector("slot[name=tab]"),e=this.shadowRoot?.querySelector("slot[name=panel]"),s=this.shadowRoot?.querySelector(".tab-group__tabs"),o=Array.from(this.shadowRoot?.querySelectorAll(".tab-group__scroll-button")||[]);t?.removeEventListener("slotchange",this.#i),e?.removeEventListener("slotchange",this.#i),s?.removeEventListener("click",this.#r),s?.removeEventListener("keydown",this.#n),o.forEach(t=>t.removeEventListener("click",this.#c)),this.removeEventListener("a-tab-close",this.#d),this.#b()}#u(){if(!this.#s)return;let t=this.shadowRoot?.querySelector(".tab-group__tabs");t&&(this.#s.unobserve(t),this.#s.observe(t))}#b(){this.#s&&(this.#s.disconnect(),null!==this.#o&&(window.cancelAnimationFrame(this.#o),this.#o=null))}#p(){return getComputedStyle(this).direction||"ltr"}#h(){this.hidden=0===this.#g().length}#m(){let t=this.#g();this.#h(),t.forEach(t=>{let e=t.nextElementSibling;if(!e||"a-tab-panel"!==e.tagName.toLowerCase())return console.error(`Tab #${t.id} is not a sibling of a <a-tab-panel>`);t.setAttribute("aria-controls",e.id),e.setAttribute("aria-labelledby",t.id)})}#v(){return Array.from(this.querySelectorAll("a-tab-panel"))}#g(){return Array.from(this.querySelectorAll("a-tab"))}#f(t){let e=t.getAttribute("aria-controls");return this.querySelector(`#${e}`)}#w(){return this.#g().find(t=>!t.disabled)}#T(){let t=this.#g();for(let e=t.length-1;e>=0;e--)if(!t[e].disabled)return t[e]}#y(){let t=this.#g(),e=this.activation===b.MANUAL?t.findIndex(t=>t.matches(":focus"))-1:t.findIndex(t=>t.selected)-1;for(;t[(e+t.length)%t.length].disabled;)e--;return t[(e+t.length)%t.length]}#_(){let t=this.#g(),e=this.activation===b.MANUAL?t.findIndex(t=>t.matches(":focus"))+1:t.findIndex(t=>t.selected)+1;for(;t[e%t.length].disabled;)e++;return t[e%t.length]}#A(){let t=this.#g(),e=this.#v();t.forEach(t=>t.selected=!1),e.forEach(t=>t.hidden=!0)}#l(){let t=this.shadowRoot?.querySelector(".tab-group__nav"),e=Array.from(this.shadowRoot?.querySelectorAll(".tab-group__scroll-button")||[]);this.noScrollControls||this.placement===d.START||this.placement===d.END?(this.#b(),e.forEach(t=>t.hidden=!0),t?.part.remove("nav--has-scroll-controls"),t?.classList.remove("tab-group__nav--has-scroll-controls")):(this.#u(),e.forEach(t=>t.hidden=!1))}#E(){let t=this.#g(),e=t.find(t=>t.selected&&!t.disabled)||t.find(t=>!t.disabled);e&&(this.#a&&!e.selected&&this.dispatchEvent(new CustomEvent("a-tab-show",{bubbles:!0,composed:!0,detail:{tabId:e.id}})),this.#C(e))}#C(t){this.#A(),t&&(t.selected=!0);let e=this.#f(t);e&&(e.hidden=!1)}#i=t=>{this.#m(),this.#l(),this.#E(),"tab"===t.target.name&&(this.#a=!0)};#n=t=>{if("a-tab"!==t.target.tagName.toLowerCase()||t.altKey)return;let e=h.includes(this.placement||"")?this.placement:d.TOP,s=[d.TOP,d.BOTTOM].includes(e||"")?"horizontal":"vertical",o=this.#p(),a=null;switch(t.key){case u.LEFT:"horizontal"===s&&(a="ltr"===o?this.#y():this.#_())&&(this.activation===b.MANUAL?a.focus():this.selectTab(a));break;case u.RIGHT:"horizontal"===s&&(a="ltr"===o?this.#_():this.#y())&&(this.activation===b.MANUAL?a.focus():this.selectTab(a));break;case u.UP:"vertical"===s&&(a=this.#y())&&(this.activation===b.MANUAL?a.focus():this.selectTab(a));break;case u.DOWN:"vertical"===s&&(a=this.#_())&&(this.activation===b.MANUAL?a.focus():this.selectTab(a));break;case u.HOME:(a=this.#w())&&(this.activation===b.MANUAL?a.focus():this.selectTab(a));break;case u.END:(a=this.#T())&&(this.activation===b.MANUAL?a.focus():this.selectTab(a));break;case u.ENTER:case u.SPACE:(a=t.target)&&this.selectTab(a);break;default:return}t.preventDefault()};#r=t=>{let e=t.target.closest("a-tab");e&&this.selectTab(e)};#c=t=>{let e=t.target.closest(".tab-group__scroll-button"),s=this.shadowRoot?.querySelector(".tab-group__tabs");if(!e||!s)return;let o=e.classList.contains("tab-group__scroll-button--start")?-1:1,a=s.scrollLeft;s.scrollTo({left:a+o*this.scrollDistance})};#d=t=>{let e=t.target,s=this.#f(e);e&&(e.remove(),e.selected&&this.dispatchEvent(new CustomEvent("a-tab-hide",{bubbles:!0,composed:!0,detail:{tabId:e.id}}))),s&&"a-tab-panel"===s.tagName.toLowerCase()&&s.remove()};#e(t){return e(t,this)}selectTabByIndex(t){let e=this.#g()[t];e&&this.selectTab(e)}selectTabById(t){let e=this.#g().find(e=>e.id===t);e&&this.selectTab(e)}selectTab(t){let e=this.#g().find(t=>t.selected);!t||t.disabled||t.selected||"a-tab"!==t.tagName.toLowerCase()||(this.#C(t),setTimeout(()=>{t.scrollIntoView({inline:"nearest",block:"nearest"}),t.focus()},0),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"){"undefined"==typeof window||window.customElements.get(t)||window.customElements.define(t,m)}}m.defineCustomElement();export{m as TabGroup};
`;class m extends HTMLElement{#s=null;#o=null;#a=!1;constructor(){super(),this.shadowRoot||this.attachShadow({mode:"open"}).appendChild(g.content.cloneNode(!0))}static get observedAttributes(){return["placement","no-scroll-controls"]}attributeChangedCallback(t,e,s){"placement"===t&&e!==s&&this.#l(),"no-scroll-controls"===t&&e!==s&&this.#l()}get placement(){return this.getAttribute("placement")||d.TOP}set placement(t){null!=t&&this.setAttribute("placement",t)}get noScrollControls(){return this.hasAttribute("no-scroll-controls")}set noScrollControls(t){this.toggleAttribute("no-scroll-controls",!!t)}get scrollDistance(){return Math.abs(Number(this.getAttribute("scroll-distance")))||200}set scrollDistance(t){this.setAttribute("scroll-distance",Math.abs(t).toString()||"200")}get activation(){return this.getAttribute("activation")||b.AUTO}set activation(t){this.setAttribute("activation",t||b.AUTO)}get noTabCycling(){return this.hasAttribute("no-tab-cycling")}set noTabCycling(t){this.toggleAttribute("no-tab-cycling",!!t)}connectedCallback(){this.#e("placement"),this.#e("noScrollControls"),this.#e("scrollDistance"),this.#e("activation"),this.#e("noTabCycling");let t=this.shadowRoot?.querySelector("slot[name=tab]"),e=this.shadowRoot?.querySelector("slot[name=panel]"),s=this.shadowRoot?.querySelector(".tab-group__tabs"),o=this.shadowRoot?.querySelector(".tab-group__nav"),a=Array.from(this.shadowRoot?.querySelectorAll(".tab-group__scroll-button")||[]);t?.addEventListener("slotchange",this.#i),e?.addEventListener("slotchange",this.#i),s?.addEventListener("click",this.#r),s?.addEventListener("keydown",this.#n),a.forEach(t=>t.addEventListener("click",this.#c)),this.addEventListener("a-tab-close",this.#d),"ResizeObserver"in window&&(this.#s=new ResizeObserver(t=>{this.#o=window.requestAnimationFrame(()=>{let e=t?.[0],s=e?.target,l=s?.scrollWidth>s?.clientWidth;a.forEach(t=>t.toggleAttribute("hidden",!l)),o?.part.toggle("nav--has-scroll-controls",l),o?.classList.toggle("tab-group__nav--has-scroll-controls",l)})})),this.#h(),this.#l(),this.placement=h.includes(this.placement||"")?this.placement:d.TOP}disconnectedCallback(){let t=this.shadowRoot?.querySelector("slot[name=tab]"),e=this.shadowRoot?.querySelector("slot[name=panel]"),s=this.shadowRoot?.querySelector(".tab-group__tabs"),o=Array.from(this.shadowRoot?.querySelectorAll(".tab-group__scroll-button")||[]);t?.removeEventListener("slotchange",this.#i),e?.removeEventListener("slotchange",this.#i),s?.removeEventListener("click",this.#r),s?.removeEventListener("keydown",this.#n),o.forEach(t=>t.removeEventListener("click",this.#c)),this.removeEventListener("a-tab-close",this.#d),this.#b()}#u(){if(!this.#s)return;let t=this.shadowRoot?.querySelector(".tab-group__tabs");t&&(this.#s.unobserve(t),this.#s.observe(t))}#b(){this.#s&&(this.#s.disconnect(),null!==this.#o&&(window.cancelAnimationFrame(this.#o),this.#o=null))}#p(){return getComputedStyle(this).direction||"ltr"}#h(){this.hidden=0===this.#g().length}#m(){let t=this.#g();this.#h(),t.forEach(t=>{let e=t.nextElementSibling;if(!e||"a-tab-panel"!==e.tagName.toLowerCase())return console.error(`Tab #${t.id} is not a sibling of a <a-tab-panel>`);t.setAttribute("aria-controls",e.id),e.setAttribute("aria-labelledby",t.id)})}#v(){return Array.from(this.querySelectorAll("a-tab-panel"))}#g(){return Array.from(this.querySelectorAll("a-tab"))}#f(t){let e=t.getAttribute("aria-controls");return this.querySelector(`#${e}`)}#w(){return this.#g().find(t=>!t.disabled)||null}#T(){let t=this.#g();for(let e=t.length-1;e>=0;e--)if(!t[e].disabled)return t[e];return null}#y(){let t=this.#g(),e=this.activation===b.MANUAL?t.findIndex(t=>t.matches(":focus"))-1:t.findIndex(t=>t.selected)-1;for(;t[(e+t.length)%t.length].disabled;)e--;return this.noTabCycling&&e<0?null:t[(e+t.length)%t.length]}#A(){let t=this.#g(),e=this.activation===b.MANUAL?t.findIndex(t=>t.matches(":focus"))+1:t.findIndex(t=>t.selected)+1;for(;t[e%t.length].disabled;)e++;return this.noTabCycling&&e>=t.length?null:t[e%t.length]}#_(){let t=this.#g(),e=this.#v();t.forEach(t=>t.selected=!1),e.forEach(t=>t.hidden=!0)}#l(){let t=this.shadowRoot?.querySelector(".tab-group__nav"),e=Array.from(this.shadowRoot?.querySelectorAll(".tab-group__scroll-button")||[]);this.noScrollControls||this.placement===d.START||this.placement===d.END?(this.#b(),e.forEach(t=>t.hidden=!0),t?.part.remove("nav--has-scroll-controls"),t?.classList.remove("tab-group__nav--has-scroll-controls")):(this.#u(),e.forEach(t=>t.hidden=!1))}#E(){let t=this.#g(),e=t.find(t=>t.selected&&!t.disabled)||t.find(t=>!t.disabled);e&&(this.#a&&!e.selected&&this.dispatchEvent(new CustomEvent("a-tab-show",{bubbles:!0,composed:!0,detail:{tabId:e.id}})),this.#C(e))}#C(t){this.#_(),t&&(t.selected=!0);let e=this.#f(t);e&&(e.hidden=!1)}#i=t=>{this.#m(),this.#l(),this.#E(),"tab"===t.target.name&&(this.#a=!0)};#n=t=>{if("a-tab"!==t.target.tagName.toLowerCase()||t.altKey)return;let e=h.includes(this.placement||"")?this.placement:d.TOP,s=[d.TOP,d.BOTTOM].includes(e||"")?"horizontal":"vertical",o=this.#p(),a=null;switch(t.key){case u.LEFT:"horizontal"===s&&(a="ltr"===o?this.#y():this.#A())&&(this.activation===b.MANUAL?a.focus():this.selectTab(a));break;case u.RIGHT:"horizontal"===s&&(a="ltr"===o?this.#A():this.#y())&&(this.activation===b.MANUAL?a.focus():this.selectTab(a));break;case u.UP:"vertical"===s&&(a=this.#y())&&(this.activation===b.MANUAL?a.focus():this.selectTab(a));break;case u.DOWN:"vertical"===s&&(a=this.#A())&&(this.activation===b.MANUAL?a.focus():this.selectTab(a));break;case u.HOME:(a=this.#w())&&(this.activation===b.MANUAL?a.focus():this.selectTab(a));break;case u.END:(a=this.#T())&&(this.activation===b.MANUAL?a.focus():this.selectTab(a));break;case u.ENTER:case u.SPACE:(a=t.target)&&this.selectTab(a);break;default:return}t.preventDefault()};#r=t=>{let e=t.target.closest("a-tab");e&&this.selectTab(e)};#c=t=>{let e=t.target.closest(".tab-group__scroll-button"),s=this.shadowRoot?.querySelector(".tab-group__tabs");if(!e||!s)return;let o=e.classList.contains("tab-group__scroll-button--start")?-1:1,a=s.scrollLeft;s.scrollTo({left:a+o*this.scrollDistance})};#d=t=>{let e=t.target,s=this.#f(e);e&&(e.remove(),e.selected&&this.dispatchEvent(new CustomEvent("a-tab-hide",{bubbles:!0,composed:!0,detail:{tabId:e.id}}))),s&&"a-tab-panel"===s.tagName.toLowerCase()&&s.remove()};#e(t){return e(t,this)}selectTabByIndex(t){let e=this.#g()[t];e&&this.selectTab(e)}selectTabById(t){let e=this.#g().find(e=>e.id===t);e&&this.selectTab(e)}selectTab(t){let e=this.#g().find(t=>t.selected);!t||t.disabled||t.selected||"a-tab"!==t.tagName.toLowerCase()||(this.#C(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"){"undefined"==typeof window||window.customElements.get(t)||window.customElements.define(t,m)}}m.defineCustomElement();export{m as TabGroup};
Loading

0 comments on commit 95c837f

Please sign in to comment.