# Toggle Group

Single or multi-select toggle buttons with event-based parent-child communication.

## Example code

```html
<!DOCTYPE html>
<html>

<head>
  <title>Toggle Group</title>
  <meta name="description"
    content="Single or multi-select toggle buttons with event-based parent-child communication.">
  <script type="importmap">
    {
      "imports": {
        "nanotags": "https://esm.sh/nanotags@latest",
        "nanostores": "https://esm.sh/nanostores@latest"
      }
    }
  </script>
</head>

<body>
  <div id="description">Single or multi-select toggle buttons with <a href='cookbook#child-to-parent'>event-based parent-child communication</a>.</div>

  <div data-type="html">
    <h4>Single select</h4>
    <x-toggle-group mode="single" value="center">
      <x-toggle data-ref="toggles" role="button" value="left">Left</x-toggle>
      <x-toggle data-ref="toggles" role="button" value="center" aria-pressed="true">Center</x-toggle>
      <x-toggle data-ref="toggles" role="button" value="right">Right</x-toggle>
    </x-toggle-group>

    <h4>Multi select</h4>
    <x-toggle-group mode="multi">
      <x-toggle data-ref="toggles" role="button" value="bold"><b>B</b></x-toggle>
      <x-toggle data-ref="toggles" role="button" value="italic"><i>I</i></x-toggle>
      <x-toggle data-ref="toggles" role="button" value="underline"><u>U</u></x-toggle>
    </x-toggle-group>
  </div>

  <script type="module" data-type="javascript">
    import { define, } from "nanotags";
    import { atom } from "nanostores";
    import { attachRovingFocus } from "attachRovingFocus.js";

    define("x-toggle-group")
      .withProps((p) => ({
        mode: p.oneOf(["single", "multi"], "single"),
        value: p.string(""),
      }))
      .withRefs((r) => ({
        toggles: r.many('x-toggle')
      }))
      .setup((ctx) => {
        const $selected = atom(new Set(
          ctx.props.$value.get() ? [ctx.props.$value.get()] : []
        ));

        function toggle(value) {
          const mode = ctx.props.$mode.get();
          const current = $selected.get();

          if (mode === "single") {
            $selected.set(new Set(current.has(value) ? [] : [value]));
          } else {
            const next = new Set(current);
            next.has(value) ? next.delete(value) : next.add(value);
            $selected.set(next);
          }
          console.log("Selected:", [...$selected.get()].join(", ") || "(none)");
        }

        attachRovingFocus(ctx, ctx.host, ctx.refs.toggles);
        ctx.host.setAttribute("role", "group");
        ctx.effect($selected, (selected) => {
          ctx.refs.toggles.forEach((toggle) => {
            toggle.active = selected.has(toggle.getAttribute('value'));
          });
        });
        ctx.on(ctx.refs.toggles, "toggle", (e) => toggle(e.detail));
      });

    define("x-toggle")
      .withProps((p) => ({ value: p.string(""), active: p.boolean(false) }))
      .setup((ctx) => {
        ctx.host.setAttribute("role", "button");

        ctx.on(ctx.host, "click", () => ctx.emit('toggle', ctx.props.$value.get()));
        ctx.on(ctx.host, "keydown", (e) => {
          if (e.key === "Enter" || e.key === " ") {
            e.preventDefault();
            ctx.emit('toggle', ctx.props.$value.get());
          }
        });

        ctx.effect(ctx.props.$active, (active) => {
          ctx.host.setAttribute("aria-pressed", active);
        });
      });
  </script>

  <script type="module" data-type="javascript" data-name="attachRovingFocus.js">
    export function attachRovingFocus(ctx, container, items, options = {}) {
      function setActive(index) {
        items.forEach((item, i) => {
          item.setAttribute("tabindex", i === index ? "0" : "-1");
        });
      }

      setActive(0);

      ctx.on(container, "keydown", (e) => {
        const current = items.indexOf(document.activeElement);
        if (current === -1) return;
        let next = -1;
        if (e.key === "ArrowRight") next = (current + 1) % items.length;
        if (e.key === "ArrowLeft") next = (current - 1 + items.length) % items.length;
        if (e.key === "Home") next = 0;
        if (e.key === "End") next = items.length - 1;
        if (next !== -1) {
          e.preventDefault();
          setActive(next);
          items[next].focus();
          options.onFocus?.(items[next]);
        }
      });

      ctx.on(container, "focusin", (e) => {
        const index = items.indexOf(e.target);
        if (index !== -1) setActive(index);
      });
    }
  </script>

  <style data-type="css">
    x-toggle-group {
      display: inline-flex;
      border: 1px solid var(--border);
      border-radius: 6px;
      overflow: hidden;
    }

    x-toggle {
      padding: 8px 16px;
      cursor: pointer;
      user-select: none;
      background: var(--surface);
      border: none;
      border-right: 1px solid var(--border);
      transition: background 0.15s, color 0.15s;
    }

    x-toggle:last-child {
      border-right: none;
    }

    x-toggle:hover {
      background: var(--surface-hover);
    }

    x-toggle[aria-pressed="true"] {
      background: var(--accent);
      color: white;
    }

    x-toggle[aria-pressed="true"]:hover {
      background: var(--accent-hover);
    }

    x-toggle:focus-visible {
      outline: 2px solid var(--accent);
      outline-offset: -2px;
      z-index: 1;
    }

    h4 {
      margin: 16px 0 8px;
    }

    h4:first-child {
      margin-top: 0;
    }
  </style>
</body>

</html>

```
