@c2n/button-group 0.0.8 → 0.0.10

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @c2n/button-group
2
2
 
3
- Button group built with Lit: attaches `c2-button` / `c2-icon-button` items into one control, optionally as a segmented control with single or multiple selection.
3
+ Button group built with Lit: attach `c2-button` / `c2-icon-button` items into one joined control or a polished segmented selector.
4
4
 
5
5
  ```bash
6
6
  npm install @c2n/button-group
@@ -21,11 +21,18 @@ npm install @c2n/button-group
21
21
  <c2-button value="week">Week</c2-button>
22
22
  <c2-button value="month">Month</c2-button>
23
23
  </c2-button-group>
24
+
25
+ <c2-button-group appearance="segmented" size="m" value="week">
26
+ <c2-button value="day">Day</c2-button>
27
+ <c2-button value="week">Week</c2-button>
28
+ <c2-button value="month">Month</c2-button>
29
+ </c2-button-group>
24
30
  ```
25
31
 
26
32
  - **Layout**: items share borders and only the outside corners are rounded (`--c2-button-group--border-radius`); a divider (`--c2-button-group__divider--width` / `--color`) separates them. `--c2-button-group--border` puts one border on every item for an outlined group. A positive `--c2-button-group--gap` separates the items into individually rounded buttons. `orientation="vertical"` stacks them.
27
- - **Selection**: `selection="single"` keeps exactly one item pressed, `selection="multiple"` toggles items independently. Items identify themselves with `value` (their index otherwise); the group's `value` is the pressed value or a comma-separated list. `change` fires on user clicks with `detail.value`; setting `value` from code is silent.
28
- - **Accessibility**: `role="group"` (name it with `aria-label`); pressed items carry `selected`, which `c2-button` and `c2-icon-button` announce as `aria-pressed`, and the group marks items `toggle` so unpressed ones announce `false`.
33
+ - **Segmented**: `appearance="segmented"` provides an inset track and animated single-selection indicator. It selects the first enabled item by default, supports `size="s|m|l"`, and can fill its container with `stretched`.
34
+ - **Selection**: `selection="single"` keeps exactly one item pressed, `selection="multiple"` toggles items independently. Items identify themselves with `value` (their index otherwise); the group's `value` is the pressed value or a comma-separated list. `change` fires on pointer or keyboard selection with `detail.value`; setting `value` from code is silent.
35
+ - **Accessibility**: `role="group"` (name it with `aria-label`); pressed items carry `selected`, which `c2-button` and `c2-icon-button` announce as `aria-pressed`. Single selection supports Arrow, Home and End keys and skips disabled items.
29
36
  - **Disabled**: `disabled` on the group disables every item and restores them when cleared, leaving items you disabled yourself alone.
30
37
 
31
38
  Colours and sizes come from the buttons' own variables set on the group (`--c2-button__container--*`, including the `__selected--*` pressed state added for groups; `--c2-icon-button--*` for icon items). `--c2-button-group__item--flex: 1 1 0` makes every item the same width. The full list is in `custom-elements.json` and on the docs site.
@@ -1,9 +1,11 @@
1
- import { LitElement, html, isServer, unsafeCSS } from "lit";
2
- import { property } from "lit/decorators.js";
1
+ import { LitElement, html, isServer, nothing, unsafeCSS } from "lit";
2
+ import { property, state } from "lit/decorators.js";
3
+ import { styleMap } from "lit/directives/style-map.js";
3
4
  import { customElement } from "@c2n/core/element-helper.js";
4
5
  import "@c2n/button";
6
+ import "@c2n/icon-button";
5
7
  //#region src/button-group.scss?inline
6
- var button_group_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: inline-flex;\n align-items: stretch;\n gap: var(--c2-button-group--gap,0px);\n vertical-align: middle;\n max-width: 100%;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n:host([orientation=vertical]) {\n flex-direction: column;\n align-items: stretch;\n}\n\n::slotted(*) {\n position: relative;\n flex: var(--c2-button-group__item--flex,0 0 auto);\n min-width: 0;\n --c2-button__container--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n --c2-icon-button--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n}\n\n::slotted(*) {\n --c2-button__container--border: var(--c2-button-group--border,1px solid transparent);\n --c2-icon-button--border: var(--c2-button-group--border,1px solid transparent);\n}\n\n::slotted(:only-child) {\n --c2-button__container--border-radius: var(--c2-button-group--border-radius,6px);\n --c2-icon-button--border-radius: var(--c2-button-group--border-radius,6px);\n}\n\n::slotted(:first-child:not(:only-child)) {\n --c2-button__container--border-radius: var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px);\n --c2-icon-button--border-radius: var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px);\n}\n\n::slotted(:last-child:not(:only-child)) {\n --c2-button__container--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n --c2-icon-button--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n}\n\n:host(:dir(rtl)) ::slotted(:first-child:not(:only-child)) {\n --c2-button__container--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n --c2-icon-button--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n}\n\n:host(:dir(rtl)) ::slotted(:last-child:not(:only-child)) {\n --c2-button__container--border-radius: var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px);\n --c2-icon-button--border-radius: var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px);\n}\n\n::slotted(:not(:first-child)) {\n margin-inline-start: calc(-1 * max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000));\n box-shadow: calc(-1 * max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000)) 0 0 0 var(--c2-button-group__divider--color,rgba(255, 255, 255, 0.35));\n}\n\n:host(:dir(rtl)) ::slotted(:not(:first-child)) {\n box-shadow: max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000) 0 0 0 var(--c2-button-group__divider--color,rgba(255, 255, 255, 0.35));\n}\n\n:host([orientation=vertical]) ::slotted(:first-child:not(:only-child)) {\n --c2-button__container--border-radius: var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n --c2-icon-button--border-radius: var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n}\n\n:host([orientation=vertical]) ::slotted(:last-child:not(:only-child)) {\n --c2-button__container--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px);\n --c2-icon-button--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px);\n}\n\n:host([orientation=vertical]) ::slotted(:not(:first-child)) {\n margin-inline-start: 0;\n margin-block-start: calc(-1 * max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000));\n box-shadow: 0 calc(-1 * max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000)) 0 0 var(--c2-button-group__divider--color,rgba(255, 255, 255, 0.35));\n}\n\n::slotted(:hover),\n::slotted(:focus-within),\n::slotted([selected]) {\n z-index: 1;\n}";
8
+ var button_group_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: inline-flex;\n align-items: stretch;\n gap: var(--c2-button-group--gap,0px);\n vertical-align: middle;\n max-width: 100%;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.indicator {\n display: none;\n}\n\n:host([orientation=vertical]) {\n flex-direction: column;\n align-items: stretch;\n}\n\n::slotted(*) {\n position: relative;\n flex: var(--c2-button-group__item--flex,0 0 auto);\n min-width: 0;\n --c2-button__container--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n --c2-icon-button--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n}\n\n::slotted(*) {\n --c2-button__container--border: var(--c2-button-group--border,1px solid transparent);\n --c2-icon-button--border: var(--c2-button-group--border,1px solid transparent);\n}\n\n::slotted(:only-child) {\n --c2-button__container--border-radius: var(--c2-button-group--border-radius,6px);\n --c2-icon-button--border-radius: var(--c2-button-group--border-radius,6px);\n}\n\n::slotted(:first-child:not(:only-child)) {\n --c2-button__container--border-radius: var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px);\n --c2-icon-button--border-radius: var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px);\n}\n\n::slotted(:last-child:not(:only-child)) {\n --c2-button__container--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n --c2-icon-button--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n}\n\n:host(:dir(rtl)) ::slotted(:first-child:not(:only-child)) {\n --c2-button__container--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n --c2-icon-button--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n}\n\n:host(:dir(rtl)) ::slotted(:last-child:not(:only-child)) {\n --c2-button__container--border-radius: var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px);\n --c2-icon-button--border-radius: var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px);\n}\n\n::slotted(:not(:first-child)) {\n margin-inline-start: calc(-1 * max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000));\n box-shadow: calc(-1 * max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000)) 0 0 0 var(--c2-button-group__divider--color,rgba(255, 255, 255, 0.35));\n}\n\n:host(:dir(rtl)) ::slotted(:not(:first-child)) {\n box-shadow: max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000) 0 0 0 var(--c2-button-group__divider--color,rgba(255, 255, 255, 0.35));\n}\n\n:host([orientation=vertical]) ::slotted(:first-child:not(:only-child)) {\n --c2-button__container--border-radius: var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n --c2-icon-button--border-radius: var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n}\n\n:host([orientation=vertical]) ::slotted(:last-child:not(:only-child)) {\n --c2-button__container--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px);\n --c2-icon-button--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px);\n}\n\n:host([orientation=vertical]) ::slotted(:not(:first-child)) {\n margin-inline-start: 0;\n margin-block-start: calc(-1 * max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000));\n box-shadow: 0 calc(-1 * max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000)) 0 0 var(--c2-button-group__divider--color,rgba(255, 255, 255, 0.35));\n}\n\n::slotted(:hover),\n::slotted(:focus-within),\n::slotted([selected]) {\n z-index: 1;\n}\n\n:host([appearance=segmented]) {\n position: relative;\n box-sizing: border-box;\n padding: var(--c2-button-group__segmented--padding,3px);\n gap: var(--c2-button-group__segmented--gap,2px);\n border: var(--c2-button-group__segmented--border,1px solid transparent);\n border-radius: var(--c2-button-group--border-radius,6px);\n background-color: var(--c2-button-group__segmented--background-color,#f4f4f5);\n isolation: isolate;\n}\n\n:host([appearance=segmented][stretched]) {\n display: flex;\n width: 100%;\n}\n\n:host([appearance=segmented]) .indicator {\n position: absolute;\n inset: 0 auto auto 0;\n z-index: 0;\n display: block;\n box-sizing: border-box;\n pointer-events: none;\n border: var(--c2-button-group__indicator--border,1px solid rgba(24, 24, 27, 0.08));\n border-radius: var(--c2-button-group__indicator--border-radius,6px);\n background-color: var(--c2-button-group__indicator--background-color,#ffffff);\n box-shadow: var(--c2-button-group__indicator--box-shadow,0 1px 3px rgba(24, 24, 27, 0.16));\n will-change: transform;\n transition: transform var(--c2-button-group__indicator--transition-duration,200ms) cubic-bezier(0.2, 0, 0, 1), width var(--c2-button-group__indicator--transition-duration,200ms) cubic-bezier(0.2, 0, 0, 1), height var(--c2-button-group__indicator--transition-duration,200ms) cubic-bezier(0.2, 0, 0, 1);\n}\n\n:host([appearance=segmented]) ::slotted(*) {\n z-index: 1;\n margin: 0;\n box-shadow: none;\n flex: var(--c2-button-group__item--flex,0 0 auto);\n --c2-button__container--border: 1px solid transparent;\n --c2-button__container--border-radius: var(--c2-button-group__indicator--border-radius,6px);\n --c2-button__container--background-color: transparent;\n --c2-button__container--color: var(--c2-button-group__segmented-item--color,#52525b);\n --c2-button__container__hover--background-color: var(--c2-button-group__segmented-item__hover--background-color,rgba(0, 0, 0, 0.05));\n --c2-button__container__active--background-color: var(--c2-button-group__segmented-item__active--background-color,rgba(0, 0, 0, 0.08));\n --c2-button__container__selected--background-color: transparent;\n --c2-button__container__selected--color: var(--c2-button-group__segmented-item__selected--color,#18181b);\n --c2-button__container__selected--border: 1px solid transparent;\n --c2-icon-button--border: 1px solid transparent;\n --c2-icon-button--border-radius: var(--c2-button-group__indicator--border-radius,6px);\n --c2-icon-button--background-color: transparent;\n --c2-icon-button__hover--background-color: var(--c2-button-group__segmented-item__hover--background-color,rgba(0, 0, 0, 0.05));\n --c2-icon-button__selected--background-color: transparent;\n}\n\n:host([appearance=segmented][stretched]) ::slotted(*) {\n flex: 1 1 0;\n}\n\n:host([appearance=segmented][size=s]) ::slotted(*) {\n --c2-button__container--height: 28px;\n --c2-button__container--padding-left: 10px;\n --c2-button__container--padding-right: 10px;\n --c2-button__container--font-size: 12px;\n --c2-icon-button__state-layer--size: 28px;\n --c2-icon-button__icon--width: 16px;\n --c2-icon-button__icon--height: 16px;\n}\n\n:host([appearance=segmented][size=m]) ::slotted(*) {\n --c2-button__container--height: 32px;\n --c2-button__container--padding-left: 14px;\n --c2-button__container--padding-right: 14px;\n --c2-button__container--font-size: 14px;\n --c2-icon-button__state-layer--size: 32px;\n --c2-icon-button__icon--width: 18px;\n --c2-icon-button__icon--height: 18px;\n}\n\n:host([appearance=segmented][size=l]) ::slotted(*) {\n --c2-button__container--height: 40px;\n --c2-button__container--padding-left: 18px;\n --c2-button__container--padding-right: 18px;\n --c2-button__container--font-size: 15px;\n --c2-icon-button__state-layer--size: 40px;\n --c2-icon-button__icon--width: 20px;\n --c2-icon-button__icon--height: 20px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host([appearance=segmented]) .indicator {\n transition: none;\n }\n}";
7
9
  //#endregion
8
10
  //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
9
11
  function __decorate(decorators, target, key, desc) {
@@ -25,14 +27,19 @@ var ButtonGroup = class ButtonGroup extends LitElement {
25
27
  this.value = "";
26
28
  this.disabled = false;
27
29
  this.orientation = "horizontal";
30
+ this.appearance = "joined";
31
+ this.size = "m";
32
+ this.stretched = false;
33
+ this.indicatorStyle = {};
28
34
  this.handleClick = (event) => {
29
- if (this.selection === "none" || this.disabled) return;
35
+ const selection = this.effectiveSelection;
36
+ if (selection === "none" || this.disabled) return;
30
37
  const items = this.items;
31
38
  const item = event.composedPath().find((node) => node instanceof HTMLElement && node.parentElement === this);
32
39
  if (!item || item.hasAttribute("disabled")) return;
33
40
  const value = this.itemValue(item, items.indexOf(item));
34
41
  let next;
35
- if (this.selection === "single") {
42
+ if (selection === "single") {
36
43
  if (this.value === value) return;
37
44
  next = value;
38
45
  } else {
@@ -46,7 +53,39 @@ var ButtonGroup = class ButtonGroup extends LitElement {
46
53
  composed: true
47
54
  }));
48
55
  };
49
- if (!isServer) this.addEventListener("click", this.handleClick);
56
+ this.handleKeyDown = (event) => {
57
+ const directionKeys = this.orientation === "vertical" ? ["ArrowUp", "ArrowDown"] : ["ArrowLeft", "ArrowRight"];
58
+ if (this.effectiveSelection !== "single" || this.disabled || ![
59
+ ...directionKeys,
60
+ "Home",
61
+ "End"
62
+ ].includes(event.key)) return;
63
+ const enabledItems = this.items.filter((item) => !item.hasAttribute("disabled"));
64
+ if (!enabledItems.length) return;
65
+ const current = event.composedPath().find((node) => node instanceof HTMLElement && enabledItems.includes(node));
66
+ if (!current) return;
67
+ event.preventDefault();
68
+ const currentIndex = enabledItems.indexOf(current);
69
+ const isRtl = getComputedStyle(this).direction === "rtl";
70
+ let nextIndex;
71
+ if (event.key === "Home") nextIndex = 0;
72
+ else if (event.key === "End") nextIndex = enabledItems.length - 1;
73
+ else nextIndex = (currentIndex + (event.key === "ArrowUp" || event.key === "ArrowLeft" && !isRtl || event.key === "ArrowRight" && isRtl ? -1 : 1) + enabledItems.length) % enabledItems.length;
74
+ const nextItem = enabledItems[nextIndex];
75
+ const value = this.itemValue(nextItem, this.items.indexOf(nextItem));
76
+ nextItem.focus();
77
+ if (this.value === value) return;
78
+ this.value = value;
79
+ this.dispatchEvent(new CustomEvent("change", {
80
+ detail: { value },
81
+ bubbles: true,
82
+ composed: true
83
+ }));
84
+ };
85
+ if (!isServer) {
86
+ this.addEventListener("click", this.handleClick);
87
+ this.addEventListener("keydown", this.handleKeyDown);
88
+ }
50
89
  }
51
90
  connectedCallback() {
52
91
  super.connectedCallback();
@@ -54,12 +93,16 @@ var ButtonGroup = class ButtonGroup extends LitElement {
54
93
  if (!isServer) {
55
94
  this.observer ??= new MutationObserver(() => this.syncItems());
56
95
  this.observer.observe(this, { childList: true });
96
+ this.resizeObserver ??= new ResizeObserver(() => this.queueIndicatorUpdate());
97
+ this.resizeObserver.observe(this);
57
98
  this.syncItems();
58
99
  }
59
100
  }
60
101
  disconnectedCallback() {
61
102
  super.disconnectedCallback();
62
103
  this.observer?.disconnect();
104
+ this.resizeObserver?.disconnect();
105
+ if (this.indicatorFrame !== void 0) cancelAnimationFrame(this.indicatorFrame);
63
106
  }
64
107
  /** The item elements, in order. */
65
108
  get items() {
@@ -73,12 +116,21 @@ var ButtonGroup = class ButtonGroup extends LitElement {
73
116
  return item.getAttribute("value") ?? String(index);
74
117
  }
75
118
  updated(changed) {
76
- if (changed.has("value") || changed.has("selection") || changed.has("disabled")) this.syncItems();
119
+ if (changed.has("value") || changed.has("selection") || changed.has("disabled") || changed.has("appearance")) this.syncItems();
120
+ if (changed.has("value") || changed.has("selection") || changed.has("appearance") || changed.has("orientation") || changed.has("size") || changed.has("stretched")) this.queueIndicatorUpdate();
121
+ }
122
+ get effectiveSelection() {
123
+ return this.appearance === "segmented" ? "single" : this.selection;
77
124
  }
78
125
  /** Mirrors `value`, `selection` and `disabled` onto the items. */
79
126
  syncItems() {
80
127
  const items = this.items;
81
- const selectable = this.selection !== "none";
128
+ const selection = this.effectiveSelection;
129
+ const selectable = selection !== "none";
130
+ if (selection === "single" && this.appearance === "segmented" && !this.value) {
131
+ const firstEnabledIndex = items.findIndex((item) => !item.hasAttribute("disabled"));
132
+ if (firstEnabledIndex >= 0) this.value = this.itemValue(items[firstEnabledIndex], firstEnabledIndex);
133
+ }
82
134
  const selected = new Set(this.selectedValues);
83
135
  items.forEach((item, index) => {
84
136
  if (selectable) item.setAttribute("toggle", "");
@@ -95,9 +147,35 @@ var ButtonGroup = class ButtonGroup extends LitElement {
95
147
  item.removeAttribute(GROUP_DISABLED);
96
148
  }
97
149
  });
150
+ this.resizeObserver?.disconnect();
151
+ this.resizeObserver?.observe(this);
152
+ items.forEach((item) => this.resizeObserver?.observe(item));
153
+ this.queueIndicatorUpdate();
154
+ }
155
+ queueIndicatorUpdate() {
156
+ if (isServer) return;
157
+ if (this.indicatorFrame !== void 0) cancelAnimationFrame(this.indicatorFrame);
158
+ this.indicatorFrame = requestAnimationFrame(() => {
159
+ this.indicatorFrame = void 0;
160
+ const selected = this.items.find((item, index) => this.itemValue(item, index) === this.value);
161
+ if (this.appearance !== "segmented" || !selected) {
162
+ this.indicatorStyle = {};
163
+ return;
164
+ }
165
+ const hostRect = this.getBoundingClientRect();
166
+ const itemRect = selected.getBoundingClientRect();
167
+ this.indicatorStyle = {
168
+ width: `${itemRect.width}px`,
169
+ height: `${itemRect.height}px`,
170
+ transform: `translate3d(${itemRect.left - hostRect.left - this.clientLeft}px, ${itemRect.top - hostRect.top - this.clientTop}px, 0)`
171
+ };
172
+ });
98
173
  }
99
174
  render() {
100
- return html`<slot></slot>`;
175
+ return html`
176
+ <span class="indicator" style=${Object.keys(this.indicatorStyle).length ? styleMap(this.indicatorStyle) : nothing} aria-hidden="true"></span>
177
+ <slot @slotchange=${this.syncItems}></slot>
178
+ `;
101
179
  }
102
180
  };
103
181
  __decorate([property({ reflect: true })], ButtonGroup.prototype, "selection", void 0);
@@ -107,10 +185,17 @@ __decorate([property({
107
185
  reflect: true
108
186
  })], ButtonGroup.prototype, "disabled", void 0);
109
187
  __decorate([property({ reflect: true })], ButtonGroup.prototype, "orientation", void 0);
188
+ __decorate([property({ reflect: true })], ButtonGroup.prototype, "appearance", void 0);
189
+ __decorate([property({ reflect: true })], ButtonGroup.prototype, "size", void 0);
190
+ __decorate([property({
191
+ type: Boolean,
192
+ reflect: true
193
+ })], ButtonGroup.prototype, "stretched", void 0);
110
194
  __decorate([property({
111
195
  type: String,
112
196
  attribute: "aria-label"
113
197
  })], ButtonGroup.prototype, "ariaLabel", void 0);
198
+ __decorate([state()], ButtonGroup.prototype, "indicatorStyle", void 0);
114
199
  ButtonGroup = __decorate([customElement("c2-button-group")], ButtonGroup);
115
200
  //#endregion
116
201
  export { ButtonGroup };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c2n/button-group",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "main": "dist/button-group.js",
6
6
  "exports": {
@@ -53,7 +53,8 @@
53
53
  "type-check": {
54
54
  "dependencies": [
55
55
  "../../core:build",
56
- "../button:build"
56
+ "../button:build",
57
+ "../icon-button:build"
57
58
  ],
58
59
  "command": "tsc -p tsconfig.lib.json --composite false"
59
60
  },
@@ -65,13 +66,14 @@
65
66
  }
66
67
  },
67
68
  "dependencies": {
68
- "@c2n/button": "0.0.8",
69
- "@c2n/core": "0.0.8",
69
+ "@c2n/button": "0.0.10",
70
+ "@c2n/core": "0.0.10",
71
+ "@c2n/icon-button": "0.0.10",
70
72
  "lit": "3.3.3"
71
73
  },
72
74
  "devDependencies": {
73
75
  "@c2n/config": "*"
74
76
  },
75
77
  "customElements": "custom-elements.json",
76
- "gitHead": "be59cbccee1d33ca248e39f69b05c0b595ec6c6d"
78
+ "gitHead": "8d57710b3345fcb01cdb4cb15d2d43ba8bf21642"
77
79
  }
@@ -1,8 +1,11 @@
1
1
  import { LitElement, type PropertyValues } from 'lit';
2
2
  import type { TypedAddEventListener, TypedRemoveEventListener } from '@c2n/core/event-helper.js';
3
3
  import '@c2n/button';
4
+ import '@c2n/icon-button';
4
5
  export type ButtonGroupSelection = 'none' | 'single' | 'multiple';
5
6
  export type ButtonGroupOrientation = 'horizontal' | 'vertical';
7
+ export type ButtonGroupAppearance = 'joined' | 'segmented';
8
+ export type ButtonGroupSize = 's' | 'm' | 'l';
6
9
  /** Events fired by {@link ButtonGroup}, keyed for `addEventListener`. */
7
10
  export interface ButtonGroupEventMap {
8
11
  change: CustomEvent<{
@@ -16,7 +19,8 @@ export interface ButtonGroup {
16
19
  /**
17
20
  * Attaches related buttons into one control: shared borders, outside corners only, a divider between items. Children
18
21
  * are `c2-button` or `c2-icon-button` elements (any element works for layout). With `selection="single"` the group is
19
- * a segmented control that keeps one item pressed; with `selection="multiple"` each item toggles independently. The
22
+ * a segmented control that keeps one item pressed; with `selection="multiple"` each item toggles independently. Set
23
+ * `orientation="vertical"` to stack either appearance and use Up/Down keyboard navigation. The
20
24
  * pressed items carry `selected` (and `aria-pressed`), the group reports them in `value` and fires `change`.
21
25
  *
22
26
  * @tag c2-button-group
@@ -31,6 +35,19 @@ export interface ButtonGroup {
31
35
  * @cssproperty {pixel} [--c2-button-group__divider--width=1px] - Width of the line between attached items; also how much their borders overlap.
32
36
  * @cssproperty {color} [--c2-button-group__divider--color=rgba(255, 255, 255, 0.35)] - Colour of the line between attached items. Match the border colour for outlined groups, `transparent` for none.
33
37
  * @cssproperty {flex} [--c2-button-group__item--flex=0 0 auto] - `1 1 0` makes every item grow to the same width.
38
+ * @cssproperty {color} [--c2-button-group__segmented--background-color=#f4f4f5] - Track background in the segmented appearance.
39
+ * @cssproperty {border} [--c2-button-group__segmented--border=1px solid transparent] - Track border in the segmented appearance.
40
+ * @cssproperty {padding} [--c2-button-group__segmented--padding=3px] - Space around the segmented items.
41
+ * @cssproperty {pixel} [--c2-button-group__segmented--gap=2px] - Space between segmented items.
42
+ * @cssproperty {color} [--c2-button-group__segmented-item--color=#52525b] - Segmented item text colour.
43
+ * @cssproperty {color} [--c2-button-group__segmented-item__hover--background-color=rgba(0, 0, 0, 0.05)] - Segmented item hover background.
44
+ * @cssproperty {color} [--c2-button-group__segmented-item__active--background-color=rgba(0, 0, 0, 0.08)] - Segmented item pressed background.
45
+ * @cssproperty {color} [--c2-button-group__segmented-item__selected--color=#18181b] - Selected segmented item text colour.
46
+ * @cssproperty {color} [--c2-button-group__indicator--background-color=#ffffff] - Sliding selection indicator background.
47
+ * @cssproperty {border} [--c2-button-group__indicator--border=1px solid rgba(24, 24, 27, 0.08)] - Sliding selection indicator border.
48
+ * @cssproperty {border-radius} [--c2-button-group__indicator--border-radius=6px] - Sliding selection indicator corner radius.
49
+ * @cssproperty {box-shadow} [--c2-button-group__indicator--box-shadow=0 1px 3px rgba(24, 24, 27, 0.16)] - Sliding selection indicator shadow.
50
+ * @cssproperty {duration} [--c2-button-group__indicator--transition-duration=200ms] - Sliding selection indicator animation duration.
34
51
  * @slotcomponent c2-button
35
52
  * @slotcomponent c2-icon-button
36
53
  */
@@ -44,9 +61,18 @@ export declare class ButtonGroup extends LitElement {
44
61
  disabled: boolean;
45
62
  /** Stacks the items vertically. */
46
63
  orientation: ButtonGroupOrientation;
64
+ /** `segmented` adds an inset track and sliding selected surface and always uses single selection. */
65
+ appearance: ButtonGroupAppearance;
66
+ /** Segmented control size. */
67
+ size: ButtonGroupSize;
68
+ /** Makes the group and every item share the available width. */
69
+ stretched: boolean;
47
70
  /** Accessible name of the group. */
48
71
  ariaLabel: string;
49
72
  private observer?;
73
+ private resizeObserver?;
74
+ private indicatorFrame?;
75
+ private indicatorStyle;
50
76
  constructor();
51
77
  connectedCallback(): void;
52
78
  disconnectedCallback(): void;
@@ -56,9 +82,12 @@ export declare class ButtonGroup extends LitElement {
56
82
  get selectedValues(): string[];
57
83
  private itemValue;
58
84
  updated(changed: PropertyValues<this>): void;
85
+ private get effectiveSelection();
59
86
  /** Mirrors `value`, `selection` and `disabled` onto the items. */
60
87
  private syncItems;
61
88
  private handleClick;
89
+ private handleKeyDown;
90
+ private queueIndicatorUpdate;
62
91
  render(): import("lit-html").TemplateResult<1>;
63
92
  }
64
93
  declare global {
@@ -1 +1 @@
1
- {"version":3,"file":"button-group.d.ts","sourceRoot":"","sources":["../../src/button-group.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA6B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAA;AAGhF,OAAO,KAAK,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AAEhG,OAAO,aAAa,CAAA;AAGpB,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAA;AACjE,MAAM,MAAM,sBAAsB,GAAG,YAAY,GAAG,UAAU,CAAA;AAI9D,yEAAyE;AACzE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,WAAW,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACvC;AAED,MAAM,WAAW,WAAW;IAC1B,gBAAgB,EAAE,qBAAqB,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAA;IACzE,mBAAmB,EAAE,wBAAwB,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAA;CAChF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBACa,WAAY,SAAQ,UAAU;IACzC,OAAgB,MAAM,0BAAoB;IAE1C,sHAAsH;IACzF,SAAS,EAAE,oBAAoB,CAAS;IAErE,0IAA0I;IAC7G,KAAK,SAAK;IAEvC,2BAA2B;IACiB,QAAQ,UAAQ;IAE5D,mCAAmC;IACN,WAAW,EAAE,sBAAsB,CAAe;IAE/E,oCAAoC;IAE3B,SAAS,EAAG,MAAM,CAAA;IAE3B,OAAO,CAAC,QAAQ,CAAC,CAAkB;;IAO1B,iBAAiB;IAUjB,oBAAoB;IAK7B,mCAAmC;IACnC,IAAI,KAAK,IAAI,WAAW,EAAE,CAEzB;IAED,mCAAmC;IACnC,IAAI,cAAc,IAAI,MAAM,EAAE,CAE7B;IAED,OAAO,CAAC,SAAS;IAIR,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAI9C,kEAAkE;IAClE,OAAO,CAAC,SAAS;IAsBjB,OAAO,CAAC,WAAW,CAgBlB;IAEQ,MAAM;CAGhB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,iBAAiB,EAAE,WAAW,CAAA;KAC/B;CACF"}
1
+ {"version":3,"file":"button-group.d.ts","sourceRoot":"","sources":["../../src/button-group.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsC,KAAK,cAAc,EAAE,MAAM,KAAK,CAAA;AAIzF,OAAO,KAAK,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AAEhG,OAAO,aAAa,CAAA;AAEpB,OAAO,kBAAkB,CAAA;AAGzB,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAA;AACjE,MAAM,MAAM,sBAAsB,GAAG,YAAY,GAAG,UAAU,CAAA;AAC9D,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,WAAW,CAAA;AAC1D,MAAM,MAAM,eAAe,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;AAI7C,yEAAyE;AACzE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,WAAW,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACvC;AAED,MAAM,WAAW,WAAW;IAC1B,gBAAgB,EAAE,qBAAqB,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAA;IACzE,mBAAmB,EAAE,wBAAwB,CAAC,WAAW,EAAE,mBAAmB,CAAC,CAAA;CAChF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBACa,WAAY,SAAQ,UAAU;IACzC,OAAgB,MAAM,0BAAoB;IAE1C,sHAAsH;IACzF,SAAS,EAAE,oBAAoB,CAAS;IAErE,0IAA0I;IAC7G,KAAK,SAAK;IAEvC,2BAA2B;IACiB,QAAQ,UAAQ;IAE5D,mCAAmC;IACN,WAAW,EAAE,sBAAsB,CAAe;IAE/E,qGAAqG;IACxE,UAAU,EAAE,qBAAqB,CAAW;IAEzE,8BAA8B;IACD,IAAI,EAAE,eAAe,CAAM;IAExD,gEAAgE;IACpB,SAAS,UAAQ;IAE7D,oCAAoC;IAE3B,SAAS,EAAG,MAAM,CAAA;IAE3B,OAAO,CAAC,QAAQ,CAAC,CAAkB;IACnC,OAAO,CAAC,cAAc,CAAC,CAAgB;IACvC,OAAO,CAAC,cAAc,CAAC,CAAQ;IAEtB,OAAO,CAAC,cAAc,CAA6B;;IAUnD,iBAAiB;IAYjB,oBAAoB;IAO7B,mCAAmC;IACnC,IAAI,KAAK,IAAI,WAAW,EAAE,CAEzB;IAED,mCAAmC;IACnC,IAAI,cAAc,IAAI,MAAM,EAAE,CAE7B;IAED,OAAO,CAAC,SAAS;IAIR,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAa9C,OAAO,KAAK,kBAAkB,GAE7B;IAED,kEAAkE;IAClE,OAAO,CAAC,SAAS;IA+BjB,OAAO,CAAC,WAAW,CAiBlB;IAED,OAAO,CAAC,aAAa,CAwBpB;IAED,OAAO,CAAC,oBAAoB;IAqBnB,MAAM;CAMhB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,iBAAiB,EAAE,WAAW,CAAA;KAC/B;CACF"}