Tabs

A roving-tabindex tab list. Only the active tab is in the tab order (tabindex="0", the rest -1); arrow keys move the active tab and shift focus with it. Panels are plain elements toggled with data-show.

Markup

<div role="tablist" aria-label="Product details">
  <template data-each="tabs" data-key="id">
    <button
      role="tab"
      data-bind="data-id:item.id, class:tabClass(item.id), aria-selected:sel(item.id), tabindex:tabIndex(item.id)"
      @click="select"
      @keydown.left.prevent="prev"
      @keydown.right.prevent="next"
      data-text="item.label"
    ></button>
  </template>
</div>

<div data-show="active === 'overview'">...</div>
<div data-show="active === 'specs'">...</div>
<div data-show="active === 'reviews'">...</div>

Component

Micra.define("tabs", {
  state: {
    active: "overview",
    tabs: [
      { id: "overview", label: "Overview" },
      { id: "specs", label: "Specs" },
      { id: "reviews", label: "Reviews" },
    ],
  },
  sel(id) {
    return this.state.active === id ? "true" : "false";
  },
  tabIndex(id) {
    return this.state.active === id ? "0" : "-1";
  },
  tabClass(id) {
    return this.state.active === id ? "tab tab-active" : "tab";
  },
  select(e) {
    this.state.active = e.currentTarget.dataset.id;
  },
  step(delta) {
    const t = this.state.tabs;
    const i = t.findIndex((x) => x.id === this.state.active);
    this.state.active = t[(i + delta + t.length) % t.length].id;
    setTimeout(
      () => this.$el.querySelector('[data-id="' + this.state.active + '"]')?.focus(),
      0,
    );
  },
  prev() {
    this.step(-1);
  },
  next() {
    this.step(1);
  },
});