@agent-ui-kit/components 0.0.3
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/dist/base-styles.css +8 -0
- package/dist/component-styles.css +147 -0
- package/dist/controls/_base/index.d.ts +3 -0
- package/dist/controls/_base/index.js +9 -0
- package/dist/controls/_base/indicator-element.d.ts +132 -0
- package/dist/controls/_base/indicator-element.js +158 -0
- package/dist/controls/_base/listbox-element.d.ts +70 -0
- package/dist/controls/_base/listbox-element.js +74 -0
- package/dist/controls/_base/range-element.d.ts +120 -0
- package/dist/controls/_base/range-element.js +158 -0
- package/dist/controls/_surface/container-box.css +301 -0
- package/dist/controls/_surface/container.css +156 -0
- package/dist/controls/_token-surface/token-surface.d.ts +35 -0
- package/dist/controls/_token-surface/token-surface.js +97 -0
- package/dist/controls/attachment/attachment-meta.d.ts +8 -0
- package/dist/controls/attachment/attachment-meta.js +84 -0
- package/dist/controls/attachment/attachment.css +91 -0
- package/dist/controls/attachment/attachment.d.ts +40 -0
- package/dist/controls/attachment/attachment.js +70 -0
- package/dist/controls/avatar/avatar-initials.d.ts +4 -0
- package/dist/controls/avatar/avatar-initials.js +30 -0
- package/dist/controls/avatar/avatar.css +87 -0
- package/dist/controls/avatar/avatar.d.ts +40 -0
- package/dist/controls/avatar/avatar.js +77 -0
- package/dist/controls/badge/badge.css +175 -0
- package/dist/controls/badge/badge.d.ts +36 -0
- package/dist/controls/badge/badge.js +71 -0
- package/dist/controls/bar-chart/bar-chart.css +110 -0
- package/dist/controls/bar-chart/bar-chart.d.ts +26 -0
- package/dist/controls/bar-chart/bar-chart.js +80 -0
- package/dist/controls/bar-chart/bar-math.d.ts +31 -0
- package/dist/controls/bar-chart/bar-math.js +106 -0
- package/dist/controls/button/button.css +263 -0
- package/dist/controls/button/button.d.ts +61 -0
- package/dist/controls/button/button.js +115 -0
- package/dist/controls/calendar/calendar.css +495 -0
- package/dist/controls/calendar/calendar.d.ts +161 -0
- package/dist/controls/calendar/calendar.js +1004 -0
- package/dist/controls/card/card-content.d.ts +24 -0
- package/dist/controls/card/card-content.js +133 -0
- package/dist/controls/card/card-footer.d.ts +3 -0
- package/dist/controls/card/card-footer.js +16 -0
- package/dist/controls/card/card-header.d.ts +3 -0
- package/dist/controls/card/card-header.js +18 -0
- package/dist/controls/card/card.css +445 -0
- package/dist/controls/card/card.d.ts +51 -0
- package/dist/controls/card/card.js +61 -0
- package/dist/controls/checkbox/checkbox.css +204 -0
- package/dist/controls/checkbox/checkbox.d.ts +114 -0
- package/dist/controls/checkbox/checkbox.js +130 -0
- package/dist/controls/code/code.css +72 -0
- package/dist/controls/code/code.d.ts +23 -0
- package/dist/controls/code/code.js +40 -0
- package/dist/controls/color-picker/color-picker.css +209 -0
- package/dist/controls/color-picker/color-picker.d.ts +84 -0
- package/dist/controls/color-picker/color-picker.js +545 -0
- package/dist/controls/color-picker/color.d.ts +40 -0
- package/dist/controls/color-picker/color.js +159 -0
- package/dist/controls/column/column.css +151 -0
- package/dist/controls/column/column.d.ts +107 -0
- package/dist/controls/column/column.js +57 -0
- package/dist/controls/combo-box/combo-box.css +397 -0
- package/dist/controls/combo-box/combo-box.d.ts +133 -0
- package/dist/controls/combo-box/combo-box.js +680 -0
- package/dist/controls/command-modal/command-modal.css +226 -0
- package/dist/controls/command-modal/command-modal.d.ts +42 -0
- package/dist/controls/command-modal/command-modal.js +315 -0
- package/dist/controls/disclosure/disclosure.css +162 -0
- package/dist/controls/disclosure/disclosure.d.ts +37 -0
- package/dist/controls/disclosure/disclosure.js +207 -0
- package/dist/controls/field/field.css +140 -0
- package/dist/controls/field/field.d.ts +27 -0
- package/dist/controls/field/field.js +228 -0
- package/dist/controls/form-provider/form-provider.css +40 -0
- package/dist/controls/form-provider/form-provider.d.ts +42 -0
- package/dist/controls/form-provider/form-provider.js +105 -0
- package/dist/controls/grid/grid.css +72 -0
- package/dist/controls/grid/grid.d.ts +60 -0
- package/dist/controls/grid/grid.js +56 -0
- package/dist/controls/icon/icon.css +37 -0
- package/dist/controls/icon/icon.d.ts +25 -0
- package/dist/controls/icon/icon.js +55 -0
- package/dist/controls/index.d.ts +57 -0
- package/dist/controls/index.js +136 -0
- package/dist/controls/ladder/ladder.css +89 -0
- package/dist/controls/ladder/ladder.d.ts +27 -0
- package/dist/controls/ladder/ladder.js +78 -0
- package/dist/controls/list/list.css +118 -0
- package/dist/controls/list/list.d.ts +84 -0
- package/dist/controls/list/list.js +48 -0
- package/dist/controls/menu/menu.css +195 -0
- package/dist/controls/menu/menu.d.ts +44 -0
- package/dist/controls/menu/menu.js +248 -0
- package/dist/controls/modal/modal.css +99 -0
- package/dist/controls/modal/modal.d.ts +61 -0
- package/dist/controls/modal/modal.js +155 -0
- package/dist/controls/popover/popover.css +83 -0
- package/dist/controls/popover/popover.d.ts +44 -0
- package/dist/controls/popover/popover.js +164 -0
- package/dist/controls/progress/progress.css +106 -0
- package/dist/controls/progress/progress.d.ts +28 -0
- package/dist/controls/progress/progress.js +80 -0
- package/dist/controls/radio/radio-group.css +84 -0
- package/dist/controls/radio/radio-group.d.ts +152 -0
- package/dist/controls/radio/radio-group.js +350 -0
- package/dist/controls/radio/radio.css +219 -0
- package/dist/controls/radio/radio.d.ts +19 -0
- package/dist/controls/radio/radio.js +75 -0
- package/dist/controls/ramp/ramp.css +75 -0
- package/dist/controls/ramp/ramp.d.ts +29 -0
- package/dist/controls/ramp/ramp.js +72 -0
- package/dist/controls/row/row.css +152 -0
- package/dist/controls/row/row.d.ts +95 -0
- package/dist/controls/row/row.js +45 -0
- package/dist/controls/segment/segment.css +66 -0
- package/dist/controls/segment/segment.d.ts +3 -0
- package/dist/controls/segment/segment.js +26 -0
- package/dist/controls/segmented-control/segmented-control.css +189 -0
- package/dist/controls/segmented-control/segmented-control.d.ts +20 -0
- package/dist/controls/segmented-control/segmented-control.js +64 -0
- package/dist/controls/select/select.css +467 -0
- package/dist/controls/select/select.d.ts +147 -0
- package/dist/controls/select/select.js +538 -0
- package/dist/controls/slider/slider.css +231 -0
- package/dist/controls/slider/slider.d.ts +111 -0
- package/dist/controls/slider/slider.js +60 -0
- package/dist/controls/slider-multi/slider-multi.css +197 -0
- package/dist/controls/slider-multi/slider-multi.d.ts +149 -0
- package/dist/controls/slider-multi/slider-multi.js +380 -0
- package/dist/controls/sparkline/sparkline-math.d.ts +23 -0
- package/dist/controls/sparkline/sparkline-math.js +79 -0
- package/dist/controls/sparkline/sparkline.css +61 -0
- package/dist/controls/sparkline/sparkline.d.ts +27 -0
- package/dist/controls/sparkline/sparkline.js +74 -0
- package/dist/controls/split/constrain.d.ts +44 -0
- package/dist/controls/split/constrain.js +126 -0
- package/dist/controls/split/split-pane.css +63 -0
- package/dist/controls/split/split-pane.d.ts +49 -0
- package/dist/controls/split/split-pane.js +42 -0
- package/dist/controls/split/split.css +127 -0
- package/dist/controls/split/split.d.ts +42 -0
- package/dist/controls/split/split.js +364 -0
- package/dist/controls/stat/stat-model.d.ts +22 -0
- package/dist/controls/stat/stat-model.js +69 -0
- package/dist/controls/stat/stat.css +101 -0
- package/dist/controls/stat/stat.d.ts +30 -0
- package/dist/controls/stat/stat.js +73 -0
- package/dist/controls/status-stream/status-stream.css +151 -0
- package/dist/controls/status-stream/status-stream.d.ts +108 -0
- package/dist/controls/status-stream/status-stream.js +417 -0
- package/dist/controls/swatch/swatch.css +66 -0
- package/dist/controls/swatch/swatch.d.ts +28 -0
- package/dist/controls/swatch/swatch.js +74 -0
- package/dist/controls/swiper/swiper-item.css +17 -0
- package/dist/controls/swiper/swiper-item.d.ts +31 -0
- package/dist/controls/swiper/swiper-item.js +47 -0
- package/dist/controls/swiper/swiper-label.css +10 -0
- package/dist/controls/swiper/swiper-label.d.ts +4 -0
- package/dist/controls/swiper/swiper-label.js +20 -0
- package/dist/controls/swiper/swiper-paddles.css +26 -0
- package/dist/controls/swiper/swiper-paddles.d.ts +10 -0
- package/dist/controls/swiper/swiper-paddles.js +62 -0
- package/dist/controls/swiper/swiper-pagination.css +60 -0
- package/dist/controls/swiper/swiper-pagination.d.ts +31 -0
- package/dist/controls/swiper/swiper-pagination.js +78 -0
- package/dist/controls/swiper/swiper.css +153 -0
- package/dist/controls/swiper/swiper.d.ts +158 -0
- package/dist/controls/swiper/swiper.js +659 -0
- package/dist/controls/switch/switch.css +194 -0
- package/dist/controls/switch/switch.d.ts +5 -0
- package/dist/controls/switch/switch.js +18 -0
- package/dist/controls/table/table-model.d.ts +43 -0
- package/dist/controls/table/table-model.js +124 -0
- package/dist/controls/table/table.css +90 -0
- package/dist/controls/table/table.d.ts +29 -0
- package/dist/controls/table/table.js +178 -0
- package/dist/controls/tabs/tab-panel.d.ts +10 -0
- package/dist/controls/tabs/tab-panel.js +89 -0
- package/dist/controls/tabs/tab.d.ts +26 -0
- package/dist/controls/tabs/tab.js +67 -0
- package/dist/controls/tabs/tabs.css +181 -0
- package/dist/controls/tabs/tabs.d.ts +61 -0
- package/dist/controls/tabs/tabs.js +176 -0
- package/dist/controls/text/href.d.ts +26 -0
- package/dist/controls/text/href.js +48 -0
- package/dist/controls/text/text.css +328 -0
- package/dist/controls/text/text.d.ts +85 -0
- package/dist/controls/text/text.js +273 -0
- package/dist/controls/text-field/text-field.css +498 -0
- package/dist/controls/text-field/text-field.d.ts +226 -0
- package/dist/controls/text-field/text-field.js +905 -0
- package/dist/controls/textarea/textarea.css +251 -0
- package/dist/controls/textarea/textarea.d.ts +136 -0
- package/dist/controls/textarea/textarea.js +312 -0
- package/dist/controls/theme-provider/theme-provider.css +48 -0
- package/dist/controls/theme-provider/theme-provider.d.ts +59 -0
- package/dist/controls/theme-provider/theme-provider.js +40 -0
- package/dist/controls/timeline/timeline.css +33 -0
- package/dist/controls/timeline/timeline.d.ts +39 -0
- package/dist/controls/timeline/timeline.js +52 -0
- package/dist/controls/timeline-item/timeline-item.css +495 -0
- package/dist/controls/timeline-item/timeline-item.d.ts +79 -0
- package/dist/controls/timeline-item/timeline-item.js +360 -0
- package/dist/controls/toast/toast-region.css +56 -0
- package/dist/controls/toast/toast-region.d.ts +20 -0
- package/dist/controls/toast/toast-region.js +113 -0
- package/dist/controls/toast/toast.css +96 -0
- package/dist/controls/toast/toast.d.ts +28 -0
- package/dist/controls/toast/toast.js +191 -0
- package/dist/controls/toolbar/toolbar.css +125 -0
- package/dist/controls/toolbar/toolbar.d.ts +108 -0
- package/dist/controls/toolbar/toolbar.js +68 -0
- package/dist/controls/tooltip/tooltip.css +80 -0
- package/dist/controls/tooltip/tooltip.d.ts +57 -0
- package/dist/controls/tooltip/tooltip.js +205 -0
- package/dist/descriptor/component-descriptor.d.ts +112 -0
- package/dist/descriptor/component-descriptor.js +492 -0
- package/dist/descriptor/index.d.ts +1 -0
- package/dist/descriptor/index.js +5 -0
- package/dist/dom/container.d.ts +45 -0
- package/dist/dom/container.js +60 -0
- package/dist/dom/element.d.ts +98 -0
- package/dist/dom/element.js +221 -0
- package/dist/dom/form.d.ts +240 -0
- package/dist/dom/form.js +399 -0
- package/dist/dom/index.d.ts +10 -0
- package/dist/dom/index.js +31 -0
- package/dist/dom/props.d.ts +56 -0
- package/dist/dom/props.js +231 -0
- package/dist/dom/repeat.d.ts +10 -0
- package/dist/dom/repeat.js +202 -0
- package/dist/dom/template.d.ts +161 -0
- package/dist/dom/template.js +634 -0
- package/dist/dom/watch.d.ts +11 -0
- package/dist/dom/watch.js +82 -0
- package/dist/foundation-styles.css +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/reactive/graph.d.ts +65 -0
- package/dist/reactive/graph.js +310 -0
- package/dist/reactive/index.d.ts +3 -0
- package/dist/reactive/index.js +4 -0
- package/dist/reactive/scheduler.d.ts +13 -0
- package/dist/reactive/scheduler.js +49 -0
- package/dist/traits/area-drag.d.ts +12 -0
- package/dist/traits/area-drag.js +70 -0
- package/dist/traits/form-registry.d.ts +36 -0
- package/dist/traits/form-registry.js +108 -0
- package/dist/traits/index.d.ts +24 -0
- package/dist/traits/index.js +19 -0
- package/dist/traits/overlay.d.ts +50 -0
- package/dist/traits/overlay.js +277 -0
- package/dist/traits/pane-resize.d.ts +26 -0
- package/dist/traits/pane-resize.js +103 -0
- package/dist/traits/press-activation.d.ts +11 -0
- package/dist/traits/press-activation.js +55 -0
- package/dist/traits/roving-focus.d.ts +52 -0
- package/dist/traits/roving-focus.js +214 -0
- package/dist/traits/scroll-fade.d.ts +28 -0
- package/dist/traits/scroll-fade.js +120 -0
- package/dist/traits/selection-commit.d.ts +30 -0
- package/dist/traits/selection-commit.js +146 -0
- package/dist/traits/tabbable.d.ts +12 -0
- package/dist/traits/tabbable.js +59 -0
- package/dist/traits/track-user-invalid.d.ts +36 -0
- package/dist/traits/track-user-invalid.js +52 -0
- package/dist/traits/value-codec.d.ts +120 -0
- package/dist/traits/value-codec.js +497 -0
- package/dist/traits/value-drag.d.ts +18 -0
- package/dist/traits/value-drag.js +84 -0
- package/package.json +263 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// radio-group.ts — UIRadioGroupElement, the radio-group container (Wave 1 Indicator, S3).
|
|
2
|
+
//
|
|
3
|
+
// The CONTAINER owns everything the radio itself cannot own: single-selection exclusivity, roving-focus
|
|
4
|
+
// keyboard navigation (Arrow/Home/End), the group form value (the selected radio's `value`), and the
|
|
5
|
+
// required → valueMissing validity verdict. Extends UIFormElement DIRECTLY — NOT UIIndicatorElement; the
|
|
6
|
+
// group itself is not an indicator, it is a form-value-owning container whose children are the indicators.
|
|
7
|
+
//
|
|
8
|
+
// ADR-0051 growth: the group wires its own `trackUserInvalid` controller + `formUserInvalid()` override
|
|
9
|
+
// (the text-field precedent) — the group, not any individual radio, is the constraint owner, so the
|
|
10
|
+
// user-invalid TIMING and the :state(user-invalid)/aria-invalid reflection live here. The group has no
|
|
11
|
+
// visual surface of its own (radio-group.css); the CSS danger leg reaches into each `ui-radio` child's
|
|
12
|
+
// own `::before` border. `UISegmentedControlElement` inherits this unchanged — it never overrides
|
|
13
|
+
// `connected()`/`formReset()`, so the wiring applies to its own internals directly.
|
|
14
|
+
//
|
|
15
|
+
// ARIA: `role='radiogroup'` via internals (FACE — never a host attribute). Each `ui-radio` child carries
|
|
16
|
+
// `role='radio'` via its own internals. The group provides no `tabindex` of its own; the rovingFocus trait
|
|
17
|
+
// manages tabindexes on the radio children (exactly one is tabindex=0; others are -1), matching the ARIA
|
|
18
|
+
// APG radio-group keyboard pattern.
|
|
19
|
+
//
|
|
20
|
+
// Selection model:
|
|
21
|
+
// · Arrow keys (Up/Down) + Home/End: move focus AND selection simultaneously (selection-follows-focus,
|
|
22
|
+
// the ARIA APG radio-group contract; ADR-0022 roving tier).
|
|
23
|
+
// · Click / Space: check the targeted radio → base toggle fires `change` → group's delegated change
|
|
24
|
+
// listener calls #commit() for exclusivity + form-value update.
|
|
25
|
+
// · #commit(index): the ONE user-driven commit path — checks radios[index], clears all others, updates
|
|
26
|
+
// #selectedValue, emits `change` on the group when the selection is new. Built on #applySelection,
|
|
27
|
+
// the shared (silent, non-emitting) selection-transition primitive it shares with the public `value`
|
|
28
|
+
// accessor below (a programmatic value write selects/clears without emitting `change` — the
|
|
29
|
+
// `UICheckboxElement.checked` / `UISelectElement.value` fleet convention for programmatic sets).
|
|
30
|
+
//
|
|
31
|
+
// Form value: #selectedValue (a signal) drives the UIFormElement effects for setFormValue + setValidity.
|
|
32
|
+
// The signal is updated inside #applySelection, which every path (Arrow key via onMove, click via change
|
|
33
|
+
// delegation, AND the public `value` setter) converges on. Reading this.#selectedValue.value inside
|
|
34
|
+
// formValue() + formValidity() keeps the effects reactive — they re-publish to internals automatically
|
|
35
|
+
// when the signal changes. The public `value` GETTER is the same signal read, exposed for external
|
|
36
|
+
// callers (e.g. the A2UI catalog's two-way `value:{prop:'value',event:'change'}` bind).
|
|
37
|
+
//
|
|
38
|
+
// ADR-0095 (supersedes ADR-0086's `variant="segmented"`): the segmented presentation is now the standalone
|
|
39
|
+
// `UISegmentedControlElement` (controls/segmented-control/), NOT a variant of this class. `variant` does not
|
|
40
|
+
// exist here — the group is back to its dot-only presentation surface. ADR-0103: the group OWNS its interior
|
|
41
|
+
// layout (radio-group.css's `@scope` flex column/row + `--ui-radio-group-gap`) — direct-children discovery
|
|
42
|
+
// (`#radios()` below) means the layout cannot be left to page-author composition, unlike a coordination
|
|
43
|
+
// wrapper. Two PROTECTED seams exist purely so that subclass can reuse this class's exclusivity/roving/value/
|
|
44
|
+
// validity machinery without forking it:
|
|
45
|
+
// · `defaultOrientation()` — the class-derived roving-axis default (this base returns 'vertical'; the
|
|
46
|
+
// resolve-once-and-reflect-at-connect mechanism below calls it only when no explicit `orientation`
|
|
47
|
+
// attribute is authored). ADR-0095 clause 1.
|
|
48
|
+
// · `selectionChanged(radios, index)` — a no-op hook fired on every selection-defining event (connect
|
|
49
|
+
// seed, every `#applySelection`, and `formReset()`) — the SAME three call sites ADR-0086 wired its
|
|
50
|
+
// (now-retired) `#writeIndexCount` state seam to. `UISegmentedControlElement` overrides it to write its
|
|
51
|
+
// own `--ui-segmented-control-index`/`-count` moving-indicator state. ADR-0095 clause 2.
|
|
52
|
+
//
|
|
53
|
+
// Layer: controls/ — imports reactive + dom + traits + controls/radio (inward-only ✓).
|
|
54
|
+
import { signal } from "../../reactive/index.js";
|
|
55
|
+
import { UIFormElement } from "../../dom/form.js";
|
|
56
|
+
import { prop } from "../../dom/props.js";
|
|
57
|
+
import { rovingFocus } from "../../traits/roving-focus.js";
|
|
58
|
+
import { trackUserInvalid } from "../../traits/track-user-invalid.js";
|
|
59
|
+
import { UIRadioElement } from "./radio.js";
|
|
60
|
+
const groupProps = {
|
|
61
|
+
// Universal form attributes (name / disabled / required) — spread so the group participates
|
|
62
|
+
// in forms as the single form value owner for the radio family (ADR-0013 formProps spread pattern).
|
|
63
|
+
...UIFormElement.formProps,
|
|
64
|
+
// ADR-0086 clause 1 (orientation SURVIVES the ADR-0095 supersession — clause 1 there: "orientation stays
|
|
65
|
+
// on the group"). The roving axis. Default 'vertical' (today's shipped roving). The EFFECTIVE orientation
|
|
66
|
+
// (author-set vs the class-derived default, `defaultOrientation()` below) is resolved once at connect —
|
|
67
|
+
// see `connected()` below — and reflected back here so CSS and the roving trait read ONE source.
|
|
68
|
+
orientation: { ...prop.enum(['horizontal', 'vertical'], 'vertical'), reflect: true },
|
|
69
|
+
};
|
|
70
|
+
export class UIRadioGroupElement extends UIFormElement {
|
|
71
|
+
static props = groupProps;
|
|
72
|
+
// The committed selection — the group's form value. A signal so the UIFormElement base's scope-owned
|
|
73
|
+
// `formValue()` + `formValidity()` effects re-run (and re-publish to internals) when it changes.
|
|
74
|
+
// null = no radio selected; a string = the selected radio's `value` prop.
|
|
75
|
+
#selectedValue = signal(null);
|
|
76
|
+
// The user-invalid TIMING controller (ADR-0051), created per connection (re-arms on reconnect;
|
|
77
|
+
// released on disconnect) — the text-field/select precedent.
|
|
78
|
+
#userInvalid = null;
|
|
79
|
+
connected() {
|
|
80
|
+
// ARIA: radiogroup role via internals, never a host role/aria-* attribute (FACE).
|
|
81
|
+
this.internals.role = 'radiogroup';
|
|
82
|
+
// ADR-0051 — the user-invalid TIMING controller. `blur` never bubbles, but the capture phase
|
|
83
|
+
// reaches this ancestor before whichever `ui-radio` child held focus (the track-user-invalid.ts
|
|
84
|
+
// precedent); `change` is the group's OWN re-emitted event (#commit(), below) — not the child
|
|
85
|
+
// radio's original (that one is stopImmediatePropagation'd) — so this listener always sees the
|
|
86
|
+
// group's real commits regardless of listener registration order. Reflects :state(user-invalid)
|
|
87
|
+
// + internals.ariaInvalid — the group carries no visual surface of its own (radio-group.css); the
|
|
88
|
+
// CSS leg reaches into each ui-radio child's own ::before border.
|
|
89
|
+
const invalidController = trackUserInvalid(this, { invalid: () => !this.formValidity().valid });
|
|
90
|
+
this.#userInvalid = invalidController;
|
|
91
|
+
this.effect(() => {
|
|
92
|
+
if (invalidController.userInvalid()) {
|
|
93
|
+
this.internals.states?.add('user-invalid');
|
|
94
|
+
this.internals.ariaInvalid = 'true';
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.internals.states?.delete('user-invalid');
|
|
98
|
+
this.internals.ariaInvalid = null;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
// Well-known data attribute marker: lets UIRadioElement.grouped() detect any UIRadioGroupElement
|
|
102
|
+
// subclass (e.g. probe subclasses in tests) via a CSS attribute selector without a circular import.
|
|
103
|
+
this.dataset['radioGroup'] = '';
|
|
104
|
+
// Seed from any initially-checked radio (HTML-parsed content: radios connect before the group's
|
|
105
|
+
// connected() runs, so they are already present and may carry a `checked` attribute from markup).
|
|
106
|
+
const radios = this.#radios();
|
|
107
|
+
const initial = radios.find((r) => r.checked);
|
|
108
|
+
if (initial)
|
|
109
|
+
this.#selectedValue.value = initial.value;
|
|
110
|
+
// ADR-0095 clause 1 (was ADR-0086 clause 1, variant-derived) — resolve the EFFECTIVE orientation
|
|
111
|
+
// ONCE, here, BEFORE the rovingFocus call below. `rovingFocus` captures `orientation` as a static
|
|
112
|
+
// value at call time (roving-focus.ts:148 reads the closed-over param directly, not a callback like
|
|
113
|
+
// `items`/`syncIndex`), so resolving this AFTER the call would silently keep the trait on the
|
|
114
|
+
// default. Precedence: an author-set `orientation` attribute wins; otherwise `defaultOrientation()`
|
|
115
|
+
// supplies the CLASS-derived default (this base: `'vertical'`, today's shipped roving, unchanged;
|
|
116
|
+
// `UISegmentedControlElement` overrides it to `'horizontal'`). Reflecting the resolved value back to
|
|
117
|
+
// the host attribute gives CSS (`[orientation]`) and the roving trait one single source of truth.
|
|
118
|
+
const resolvedOrientation = this.hasAttribute('orientation')
|
|
119
|
+
? this.orientation
|
|
120
|
+
: this.defaultOrientation();
|
|
121
|
+
this.orientation = resolvedOrientation;
|
|
122
|
+
// ADR-0095 clause 2 (was ADR-0086 clauses 2/3, `#writeIndexCount`) — the protected post-selection
|
|
123
|
+
// hook, seeded here at connect. A no-op in this base; `UISegmentedControlElement` overrides it to
|
|
124
|
+
// write its own moving-indicator state.
|
|
125
|
+
this.selectionChanged(radios, this.#checkedIndex());
|
|
126
|
+
// ── roving-focus (Arrow/Home/End: selection-follows-focus) ──────────────────────────────────
|
|
127
|
+
// The ARIA APG radio-group keyboard contract: Arrow keys move focus AND selection simultaneously.
|
|
128
|
+
// `initialIndex` seeds the roving cursor at the currently-checked radio (or first if none), so
|
|
129
|
+
// reconnect restores the correct tabindex=0 position. `syncIndex` re-reads the checked position
|
|
130
|
+
// before each keydown so a preceding click (processed via the change-delegation path below) is
|
|
131
|
+
// visible to the next Arrow move.
|
|
132
|
+
rovingFocus(this, {
|
|
133
|
+
items: () => this.#radios(),
|
|
134
|
+
orientation: resolvedOrientation,
|
|
135
|
+
loop: true,
|
|
136
|
+
typeAhead: false,
|
|
137
|
+
initialIndex: () => {
|
|
138
|
+
const i = this.#checkedIndex();
|
|
139
|
+
return i >= 0 ? i : 0; // default to first when nothing is checked
|
|
140
|
+
},
|
|
141
|
+
syncIndex: () => this.#checkedIndex(),
|
|
142
|
+
onMove: (index) => { if (!this.effectiveDisabled())
|
|
143
|
+
this.#commit(index); },
|
|
144
|
+
});
|
|
145
|
+
// ── delegated change listener (click / Space commit path) ───────────────────────────────────
|
|
146
|
+
// The base indicator emits `change` (bubbling) when it toggles unchecked → checked via a click or
|
|
147
|
+
// Space keyup. The group's capture guard in UIRadioElement.grouped() prevents the already-checked →
|
|
148
|
+
// unchecked direction from firing (stopImmediatePropagation before the base toggle), so only the
|
|
149
|
+
// relevant direction (unchecked → checked) bubbles here. This handler:
|
|
150
|
+
// · Identifies the originating UIRadioElement inside this group.
|
|
151
|
+
// · Stops the radio's change event from propagating further (the group re-emits its own).
|
|
152
|
+
// · Calls #commit to enforce exclusivity and update the group's form value.
|
|
153
|
+
this.listen(this, 'change', (event) => {
|
|
154
|
+
if (this.effectiveDisabled()) {
|
|
155
|
+
// A disabled group swallows child change events entirely: stopImmediatePropagation prevents
|
|
156
|
+
// any other listener on this element (or ancestors) from seeing the radio's change (C7).
|
|
157
|
+
event.stopImmediatePropagation();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const target = event.target;
|
|
161
|
+
if (!(target instanceof UIRadioElement))
|
|
162
|
+
return;
|
|
163
|
+
if (!this.contains(target))
|
|
164
|
+
return;
|
|
165
|
+
// Stop immediate propagation: prevent the radio's internal change event from reaching
|
|
166
|
+
// other listeners registered on the group (e.g. external consumers). The group re-emits
|
|
167
|
+
// its own semantic change event below via #commit, so external code sees exactly one change.
|
|
168
|
+
event.stopImmediatePropagation();
|
|
169
|
+
const index = this.#radios().indexOf(target);
|
|
170
|
+
if (index !== -1)
|
|
171
|
+
this.#commit(index);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// ── public value accessor ───────────────────────────────────────────────────────────────────────
|
|
175
|
+
/**
|
|
176
|
+
* The group's selected value — the checked `ui-radio` child's `value`, or `null` when none is
|
|
177
|
+
* selected. A public getter/setter pair delegating to the private `#selectedValue` signal (the
|
|
178
|
+
* `UICheckboxElement.indeterminate` precedent, checkbox.ts:39 — a plain accessor over a private
|
|
179
|
+
* signal, not a `prop.*`-declared attribute: the value is DERIVED from child radio state, so it
|
|
180
|
+
* has nothing of its own to reflect).
|
|
181
|
+
*
|
|
182
|
+
* The getter reads the signal directly — a reactive caller (e.g. inside `this.effect`) tracks it
|
|
183
|
+
* exactly like any other signal read.
|
|
184
|
+
*
|
|
185
|
+
* The setter is the PROGRAMMATIC path (e.g. a two-way data-bind write): it selects the child
|
|
186
|
+
* `ui-radio` whose `value` matches (unchecking all others) via `#applySelection` — the SAME state
|
|
187
|
+
* transition `#commit` drives for a user click/keyboard commit — but it does NOT emit `change`.
|
|
188
|
+
* This matches the fleet convention for a programmatic prop write on a value-bearing control
|
|
189
|
+
* (`UICheckboxElement.checked` / `UISelectElement.value` are directly settable and never
|
|
190
|
+
* self-emit on assignment; only the interaction-driven commit path — `#commit` here — emits).
|
|
191
|
+
*
|
|
192
|
+
* `null` clears the selection (unchecks every radio). A value matching NO child radio also CLEARS
|
|
193
|
+
* the selection, rather than a silent no-op — the native `HTMLSelectElement.value` precedent:
|
|
194
|
+
* assigning a `<select>`'s `.value` to a string with no matching `<option>` resolves
|
|
195
|
+
* `selectedIndex` to `-1` (the value reads back as `''`) instead of leaving the prior selection
|
|
196
|
+
* in place.
|
|
197
|
+
*/
|
|
198
|
+
get value() {
|
|
199
|
+
return this.#selectedValue.value;
|
|
200
|
+
}
|
|
201
|
+
set value(v) {
|
|
202
|
+
const radios = this.#radios();
|
|
203
|
+
const index = v === null ? -1 : radios.findIndex((r) => r.value === v);
|
|
204
|
+
this.#applySelection(radios, index);
|
|
205
|
+
}
|
|
206
|
+
// ── private helpers ─────────────────────────────────────────────────────────────────────────────
|
|
207
|
+
/**
|
|
208
|
+
* Live ordered set of `UIRadioElement` children (direct children only; re-read on each call).
|
|
209
|
+
* Uses `instanceof UIRadioElement` so subclasses (e.g. probe subclasses in tests) are also found,
|
|
210
|
+
* rather than relying on the `ui-radio` tag name which would miss subclasses.
|
|
211
|
+
*/
|
|
212
|
+
#radios() {
|
|
213
|
+
return [...this.children].filter((el) => el instanceof UIRadioElement);
|
|
214
|
+
}
|
|
215
|
+
/** Index of the currently checked radio (first match), or -1 when nothing is checked. */
|
|
216
|
+
#checkedIndex() {
|
|
217
|
+
return this.#radios().findIndex((r) => r.checked);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* ADR-0095 clause 1 — the CLASS-derived default roving-axis orientation, consulted by `connected()`'s
|
|
221
|
+
* resolve-once-and-reflect ONLY when no explicit `orientation` attribute is authored. This base returns
|
|
222
|
+
* `'vertical'` (today's shipped roving, unchanged). `UISegmentedControlElement` overrides this to
|
|
223
|
+
* `'horizontal'` — the ADR-0086 "variant=segmented ⇒ horizontal" default, now class-derived instead of
|
|
224
|
+
* variant-derived.
|
|
225
|
+
*/
|
|
226
|
+
defaultOrientation() {
|
|
227
|
+
return 'vertical';
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* ADR-0095 clause 2 — a protected, no-op-in-the-base post-selection hook, fired from every
|
|
231
|
+
* selection-DEFINING event: the connect-time seed, every `#applySelection` (both `#commit`'s user-driven
|
|
232
|
+
* path AND the public `value` setter's programmatic path), and `formReset()`. This is the SAME seam
|
|
233
|
+
* ADR-0086's (now-retired) private `#writeIndexCount` fired from — moved here, as a protected hook,
|
|
234
|
+
* so `UISegmentedControlElement` can write its own `--ui-segmented-control-index`/`-count` moving-
|
|
235
|
+
* indicator state WITHOUT this base class knowing anything about segments or indicators. A no-op here
|
|
236
|
+
* costs nothing for the plain dot-group presentation.
|
|
237
|
+
*/
|
|
238
|
+
selectionChanged(_radios, _index) { }
|
|
239
|
+
/**
|
|
240
|
+
* The shared selection-transition path: checks `radios[index]`, unchecks all others (direct prop
|
|
241
|
+
* writes — no click events, no change events on siblings, avoids re-entrancy into the group's own
|
|
242
|
+
* change listener via the UIRadioElement guard), updates `#selectedValue`, and fires the
|
|
243
|
+
* `selectionChanged` hook. `index === -1` clears the selection (no radio checked, `#selectedValue` →
|
|
244
|
+
* `null`) — the public `value` setter's "no match" path. Does NOT emit `change`; that is exclusively
|
|
245
|
+
* `#commit`'s concern (the user-driven path) — this is the silent primitive both `#commit` and the
|
|
246
|
+
* public `value` setter build on. Returns whether the selection actually changed.
|
|
247
|
+
*/
|
|
248
|
+
#applySelection(radios, index) {
|
|
249
|
+
const radio = index >= 0 ? radios[index] : undefined;
|
|
250
|
+
const newValue = radio ? radio.value : null;
|
|
251
|
+
const changed = newValue !== this.#selectedValue.value;
|
|
252
|
+
// Exclusivity: check the target (if any), uncheck all others (direct writes — no click, no re-entrancy).
|
|
253
|
+
radios.forEach((r, i) => {
|
|
254
|
+
r.checked = i === index;
|
|
255
|
+
});
|
|
256
|
+
// ADR-0095 clause 2 — fire the post-selection hook on every transition (both the Arrow-key/click/Space
|
|
257
|
+
// commit paths AND the public value setter converge here); a no-op in this base.
|
|
258
|
+
this.selectionChanged(radios, index);
|
|
259
|
+
if (changed)
|
|
260
|
+
this.#selectedValue.value = newValue;
|
|
261
|
+
return changed;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The ONE user-driven commit path — invoked from both the rovingFocus `onMove` callback (Arrow keys)
|
|
265
|
+
* and the delegated change listener (click/Space). Applies the transition via `#applySelection` and
|
|
266
|
+
* emits `change` on the group when the selection is new. An out-of-range `index` (no radio at that
|
|
267
|
+
* position) is a no-op — unlike the public `value` setter's "no match" path, this never clears an
|
|
268
|
+
* existing selection (there is no user gesture that should silently blank the group).
|
|
269
|
+
*/
|
|
270
|
+
#commit(index) {
|
|
271
|
+
const radios = this.#radios();
|
|
272
|
+
if (!radios[index])
|
|
273
|
+
return;
|
|
274
|
+
if (this.#applySelection(radios, index))
|
|
275
|
+
this.emit('change');
|
|
276
|
+
}
|
|
277
|
+
// ── form hooks ──────────────────────────────────────────────────────────────────────────────────
|
|
278
|
+
/**
|
|
279
|
+
* The group's form value: the selected radio's `value`, or `null` when nothing is checked.
|
|
280
|
+
* Reads `#selectedValue.value` (a signal) so the UIFormElement base's scope-owned setFormValue
|
|
281
|
+
* effect re-runs automatically when selection changes.
|
|
282
|
+
*/
|
|
283
|
+
formValue() {
|
|
284
|
+
return this.#selectedValue.value;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Validity: `required` + no selection → `valueMissing`. Reads `#selectedValue.value` so the
|
|
288
|
+
* UIFormElement base's setValidity effect re-runs when selection changes. An empty, required group
|
|
289
|
+
* stays invalid until a radio is committed; a non-required empty group is always valid.
|
|
290
|
+
*/
|
|
291
|
+
formValidity() {
|
|
292
|
+
if (this.required && this.#selectedValue.value === null) {
|
|
293
|
+
return {
|
|
294
|
+
valid: false,
|
|
295
|
+
flags: { valueMissing: true },
|
|
296
|
+
message: 'Please select one of these options.',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return { valid: true };
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Form reset — group-level coordination (bug-A fix). Each child radio is its OWN `UIFormElement`
|
|
303
|
+
* participant with its OWN `formResetCallback` (now fixed, indicator-element.ts) that silently restores
|
|
304
|
+
* ITS `checked` to ITS `defaultChecked` — but the GROUP owns a SEPARATE `#selectedValue` signal
|
|
305
|
+
* (`formValue()`/`formValidity()` above), which no radio's own reset can reach. Recompute it here from
|
|
306
|
+
* every child's `defaultChecked` (a stable, native-parity PUBLIC getter — see UIIndicatorElement) rather
|
|
307
|
+
* than each child's live `checked`: the platform resets the group and its radios as INDEPENDENT
|
|
308
|
+
* `UIFormElement`s, in an order this class must not assume (tree order suggests ancestor-first, i.e.
|
|
309
|
+
* this runs BEFORE the radios' own resets — reading live `checked` here would see PRE-reset values).
|
|
310
|
+
* `defaultChecked` sidesteps the ordering question entirely: it is a fixed snapshot from each radio's
|
|
311
|
+
* first connect, unaffected by reset order on either side. Silent — no `change` emitted (a reset is not
|
|
312
|
+
* a user commit; matches `#commit`'s own emit-on-user-action-only discipline).
|
|
313
|
+
*
|
|
314
|
+
* Bug fix (component-reviewer B4, blocking; ADR-0086, preserved verbatim by ADR-0095 clause 2): a reset
|
|
315
|
+
* must ALSO fire the post-selection hook (`selectionChanged`). The hook was previously (ADR-0086) a
|
|
316
|
+
* private `#writeIndexCount` written only at connect and in `#commit` — but a radio's own reset
|
|
317
|
+
* (`indicator-element.ts`) restores its `checked` OUTSIDE `#commit`, so without this the seam desyncs
|
|
318
|
+
* in a subclass that consumes it (e.g. `UISegmentedControlElement`'s moving indicator): the CSS
|
|
319
|
+
* `:not(:has(ui-radio[checked]))`-style gate correctly re-shows the indicator (the DOM `[checked]` is
|
|
320
|
+
* genuinely restored), but a STALE index would still park the moving fill on the pre-reset selection —
|
|
321
|
+
* a visible indicator/ink mismatch. Recomputed from `defaultChecked` for the same reset-order-
|
|
322
|
+
* independence reason as `#selectedValue` above (not live `checked`, which may not have reset yet on
|
|
323
|
+
* this radio's siblings). `findIndex` returns `-1` when no radio was ever default-checked; a consuming
|
|
324
|
+
* subclass's hook is expected to clamp that to `0` the same way ADR-0086's seam did — harmless, since a
|
|
325
|
+
* segmented indicator hides itself whenever nothing is `[checked]`.
|
|
326
|
+
*/
|
|
327
|
+
formReset() {
|
|
328
|
+
const radios = this.#radios();
|
|
329
|
+
const defaultIndex = radios.findIndex((r) => r.defaultChecked);
|
|
330
|
+
const defaultRadio = defaultIndex >= 0 ? radios[defaultIndex] : undefined;
|
|
331
|
+
this.#selectedValue.value = defaultRadio ? defaultRadio.value : null;
|
|
332
|
+
this.selectionChanged(radios, defaultIndex);
|
|
333
|
+
// ADR-0051 — a reset must not leave a required-empty group showing :state(user-invalid) until
|
|
334
|
+
// the user re-interacts (the text-field formReset() precedent).
|
|
335
|
+
this.#userInvalid?.reset();
|
|
336
|
+
}
|
|
337
|
+
disconnected() {
|
|
338
|
+
this.#userInvalid?.release(); // idempotent — the listeners already die with the connection scope
|
|
339
|
+
this.#userInvalid = null;
|
|
340
|
+
}
|
|
341
|
+
/** Feeds `FormConnectDetail.userInvalid` (ADR-0050) — the `trackUserInvalid` tracker IS the one
|
|
342
|
+
* timing source; this override just exposes its gate. Inherited by `UISegmentedControlElement`
|
|
343
|
+
* unchanged (it never overrides connected()/formReset(), so this base's wiring applies to its
|
|
344
|
+
* own internals directly). */
|
|
345
|
+
formUserInvalid() {
|
|
346
|
+
return this.#userInvalid?.userInvalid() ?? false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!customElements.get('ui-radio-group'))
|
|
350
|
+
customElements.define('ui-radio-group', UIRadioGroupElement);
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/* radio.css — ui-radio, the radio-button indicator (Wave 1 S3, ADR-0042 / ADR-0041).
|
|
2
|
+
*
|
|
3
|
+
* Two SECTIONED blocks:
|
|
4
|
+
* [1] TOKEN BLOCK — `:where(ui-radio)` declares `--ui-radio-*` from the compact ramp + colour roles.
|
|
5
|
+
* [size] repoints the box from the --md-sys-compact-{sm/md/lg} widget ramp (ADR-0041). No --md-sys-scale
|
|
6
|
+
* multiplier: the compact ramp is an EXPLICIT per-[scale] lookup re-tabled in dimensions.css, so the
|
|
7
|
+
* radio box tracks scale automatically (ADR-0038 pattern). [checked] reflects so the dot selector
|
|
8
|
+
* works as a CSS attribute hook (the custom state :state(checked) is the browser-only alternative;
|
|
9
|
+
* both selectors appear for coverage).
|
|
10
|
+
* [2] STYLES BLOCK — `@scope (ui-radio)` consumes only `--ui-radio-*`.
|
|
11
|
+
* Anatomy: the host is an inline-flex row; `::before` is the circular indicator box (border-radius
|
|
12
|
+
* 50%); `::after` is the checked DOT, a separate absolutely-positioned circle centred on `::before`
|
|
13
|
+
* (switch/slider precedent: ring/track = `::before`, glyph/thumb = `::after`); the default slot
|
|
14
|
+
* content flows after both as the optional label. Focus ring: `:focus-visible` on `:scope` (the
|
|
15
|
+
* whole host), not only on the circle — standard fleet focus treatment (ADR-0009).
|
|
16
|
+
* Forced-colors: the ring + dot survive via `ButtonText`/`Highlight`; the focus ring via
|
|
17
|
+
* `--md-sys-color-focus-ring`.
|
|
18
|
+
*
|
|
19
|
+
* 2026-07-07 fix (Kim-filed visual bug): the checked ring AND dot both read `--ui-radio-ink` (the SAME
|
|
20
|
+
* primary blue) — the box-shadow-inset dot rendered as a solid blue disc indistinguishable from its own
|
|
21
|
+
* ring, unlike ui-switch's checked thumb (`--md-sys-color-primary-on-primary`, bright on a primary track).
|
|
22
|
+
* Fix: the checked ring FILLS solid primary (`background: var(--ui-radio-ink)`, matching the switch's
|
|
23
|
+
* filled-track pattern) and the NEW `::after` dot reads the bright `--md-sys-color-primary-on-primary` role —
|
|
24
|
+
* the switch's own high-contrast pair, verified 4.79:1 light / 3.32:1 dark against the primary fill
|
|
25
|
+
* (`tokens.test.ts`'s AA-selected block independently reproduces the light leg; both clear the SC 1.4.11
|
|
26
|
+
* 3:1 non-text bar). Radio's geometry (a single filled disc, no rail/page-surface straddle) has none of
|
|
27
|
+
* ui-slider's competing-backdrop constraint, so the switch's flat on-primary pair ports directly.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/* ════════════════ [1] TOKEN BLOCK — :where(ui-radio) declares --ui-radio-* ════════════════ */
|
|
31
|
+
|
|
32
|
+
:where(ui-radio) {
|
|
33
|
+
/* Box size from the widget ramp (ADR-0041). Default = md. */
|
|
34
|
+
--ui-radio-box: var(--md-sys-compact-md);
|
|
35
|
+
|
|
36
|
+
/* Dot geometry: the SAME 22%-inset law as before (unchanged diameter maths), now sizing a real
|
|
37
|
+
`::after` circle instead of an inset box-shadow annulus — the fix touches COLOUR, not proportion. */
|
|
38
|
+
--ui-radio-dot-inset: calc(var(--ui-radio-box) * 0.22);
|
|
39
|
+
--ui-radio-dot-size: calc(var(--ui-radio-box) - 2 * var(--ui-radio-dot-inset));
|
|
40
|
+
|
|
41
|
+
/* Border: idle ring colour. */
|
|
42
|
+
--ui-radio-border: var(--md-sys-color-neutral-on-surface-variant);
|
|
43
|
+
|
|
44
|
+
/* Ink: the checked ring + FILL repoint (matches ui-switch's filled-track pattern). */
|
|
45
|
+
--ui-radio-ink: var(--md-sys-color-primary);
|
|
46
|
+
|
|
47
|
+
/* Dot: the checked glyph — BRIGHT on-primary, the ui-switch checked-thumb role (2026-07-07 fix). Was
|
|
48
|
+
`--ui-radio-ink` reused (same blue as the ring/fill it sits on) — a same-hue dot never reads as a
|
|
49
|
+
distinct indicator. Clears SC 1.4.11 (3:1) against the primary fill: 4.79:1 light / 3.32:1 dark. */
|
|
50
|
+
--ui-radio-dot: var(--md-sys-color-primary-on-primary);
|
|
51
|
+
|
|
52
|
+
/* Gap: between the circle and the label slot (font/2, the anatomy gap law — density-bearing). */
|
|
53
|
+
--ui-radio-gap: var(--md-sys-gap-md);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* [size] → compact ramp repoint (ADR-0041). Gap follows the font-size tier (density gap = font/2). */
|
|
57
|
+
:where(ui-radio[size='sm']) {
|
|
58
|
+
--ui-radio-box: var(--md-sys-compact-sm);
|
|
59
|
+
--ui-radio-gap: var(--md-sys-gap-sm);
|
|
60
|
+
}
|
|
61
|
+
:where(ui-radio[size='lg']) {
|
|
62
|
+
--ui-radio-box: var(--md-sys-compact-lg);
|
|
63
|
+
--ui-radio-gap: var(--md-sys-gap-lg);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* disabled → muted neutral roles. Holds at idle (pointer-inert below). Disabled is SC 1.4.11-EXEMPT
|
|
67
|
+
(ADR-0059 precedent: an inactive component doesn't need the checked ring/dot to stay distinguishable),
|
|
68
|
+
so ring, fill, AND dot collapse onto the SAME muted neutral — a flat disabled disc. */
|
|
69
|
+
:where(ui-radio[disabled]) {
|
|
70
|
+
--ui-radio-border: var(--md-sys-color-neutral-on-surface-variant);
|
|
71
|
+
--ui-radio-ink: var(--md-sys-color-neutral-on-surface-variant);
|
|
72
|
+
--ui-radio-dot: var(--md-sys-color-neutral-on-surface-variant);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ════════════════ [2] STYLES BLOCK — @scope (ui-radio) consumes ONLY --ui-radio-* ════════════════ */
|
|
76
|
+
|
|
77
|
+
@scope (ui-radio) {
|
|
78
|
+
/* Host: inline-flex row — circle (::before) + optional label slot side by side.
|
|
79
|
+
position: relative is the containing block for the absolutely-positioned ::after dot (switch/slider
|
|
80
|
+
precedent — the thumb/dot is centred on the ring via calc + translate, not flow layout). */
|
|
81
|
+
:scope {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: var(--ui-radio-gap);
|
|
85
|
+
position: relative;
|
|
86
|
+
|
|
87
|
+
/* Control-standard single-line line-height (ADR-0036): 1 keeps the text box = the font box. */
|
|
88
|
+
line-height: var(--md-sys-control-line-height);
|
|
89
|
+
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
-webkit-user-select: none; /* WebKit/Safari: prefix needed */
|
|
92
|
+
user-select: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ── Circular indicator box ──────────────────────────────────────────────────────────────────── */
|
|
96
|
+
|
|
97
|
+
/* ::before = the circle. Sized to --ui-radio-box; circular via border-radius 50%; ring via border.
|
|
98
|
+
background: transparent at idle (the interior stays empty — just the 2px ring). */
|
|
99
|
+
:scope::before {
|
|
100
|
+
content: '';
|
|
101
|
+
display: block;
|
|
102
|
+
flex-shrink: 0; /* never crushed by label text */
|
|
103
|
+
|
|
104
|
+
inline-size: var(--ui-radio-box);
|
|
105
|
+
block-size: var(--ui-radio-box);
|
|
106
|
+
border-radius: 50%;
|
|
107
|
+
|
|
108
|
+
/* 2px border (same weight as the focus ring and --md-sys-widget-inset's 2px law; ADR-0041). */
|
|
109
|
+
border: 2px solid var(--ui-radio-border);
|
|
110
|
+
box-sizing: border-box;
|
|
111
|
+
background: transparent;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ── Checked: ring FILLS solid primary; the dot (::after) carries the bright glyph ─────────────── */
|
|
115
|
+
|
|
116
|
+
/* [checked] is the reflected attribute (boolean prop, reflect=true). :state(checked) is the
|
|
117
|
+
custom-state equivalent (absent in jsdom; browser-only). Both selectors ensure coverage.
|
|
118
|
+
The circle fills solid primary (ring border + interior both --ui-radio-ink) — the same "filled
|
|
119
|
+
track" treatment ui-switch gives its checked track; the ::after dot below is what reads as the
|
|
120
|
+
distinct indicator, not a same-hue box-shadow annulus (the 2026-07-07 fix). */
|
|
121
|
+
:scope:is([checked])::before,
|
|
122
|
+
:scope:state(checked)::before {
|
|
123
|
+
border-color: var(--ui-radio-ink);
|
|
124
|
+
background: var(--ui-radio-ink);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ── Dot (::after) — the bright checked glyph, centred on the ::before circle ────────────────────
|
|
128
|
+
* ::before is the FIRST flex item at the host's inline-start (align-items: center vertically centres
|
|
129
|
+
* it); its horizontal centre is therefore `--ui-radio-box / 2` from the host's inline-start. The dot
|
|
130
|
+
* is absolutely positioned at that point (inset-inline-start + inset-block-start: 50%) and pulled back
|
|
131
|
+
* by half its own size (translate(-50%, -50%)) so its CENTRE — not its corner — lands there. Sized to
|
|
132
|
+
* --ui-radio-dot-size (the same 22%-inset diameter maths the old box-shadow used). Transparent at idle
|
|
133
|
+
* (no dot); the bright --ui-radio-dot fill is repointed in on checked. */
|
|
134
|
+
:scope::after {
|
|
135
|
+
content: '';
|
|
136
|
+
position: absolute;
|
|
137
|
+
box-sizing: border-box;
|
|
138
|
+
|
|
139
|
+
inline-size: var(--ui-radio-dot-size);
|
|
140
|
+
block-size: var(--ui-radio-dot-size);
|
|
141
|
+
border-radius: 50%;
|
|
142
|
+
|
|
143
|
+
inset-block-start: 50%;
|
|
144
|
+
inset-inline-start: calc(var(--ui-radio-box) / 2);
|
|
145
|
+
transform: translate(-50%, -50%);
|
|
146
|
+
|
|
147
|
+
background: transparent;
|
|
148
|
+
transition: background-color var(--md-sys-motion-duration-fast) var(--md-sys-motion-easing-standard);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
:scope:is([checked])::after,
|
|
152
|
+
:scope:state(checked)::after {
|
|
153
|
+
background: var(--ui-radio-dot);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ── Interaction states ──────────────────────────────────────────────────────────────────────── */
|
|
157
|
+
|
|
158
|
+
/* hover: subtle border lift — repoint border to the ink colour (matches native <input type=radio>
|
|
159
|
+
hover treatment: the ring reinforces intent before click). No background change (no filled bg). */
|
|
160
|
+
:scope:hover::before {
|
|
161
|
+
border-color: var(--ui-radio-ink);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Focus ring (ADR-0009): fleet-wide shared `:focus-visible` outline on the HOST (covers the circle
|
|
165
|
+
AND the label text — standard for labelled controls). Not on ::before alone, so screen-reader
|
|
166
|
+
users see a full enclosing ring. layout-neutral (outline outside the box model). */
|
|
167
|
+
:scope:focus-visible {
|
|
168
|
+
outline: var(--md-sys-state-focus-ring-width) solid var(--md-sys-color-focus-ring);
|
|
169
|
+
outline-offset: var(--md-sys-state-focus-ring-offset);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Motion (interaction-states standard) — transition state-PAINT properties on ::before (the ring/fill)
|
|
173
|
+
and ::after (the dot, transition declared alongside its rule above). Geometry (box size) is never
|
|
174
|
+
transitioned. Unconditional — no :state(ready) gate (the base sets no such state). */
|
|
175
|
+
:scope::before {
|
|
176
|
+
transition:
|
|
177
|
+
border-color var(--md-sys-motion-duration-fast) var(--md-sys-motion-easing-standard),
|
|
178
|
+
background-color var(--md-sys-motion-duration-fast) var(--md-sys-motion-easing-standard);
|
|
179
|
+
}
|
|
180
|
+
@media (prefers-reduced-motion: reduce) {
|
|
181
|
+
:scope::before,
|
|
182
|
+
:scope::after {
|
|
183
|
+
transition: none;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Disabled: pointer-inert + cursor reset (the reflected [disabled] attribute makes the host inert).
|
|
188
|
+
TKT-0047 — the redundant `opacity: 0.5` this rule carried is removed: the TOKEN block's own
|
|
189
|
+
`:where(ui-radio[disabled])` already repoints border/ink/dot to the muted neutral role, converging
|
|
190
|
+
on the fleet's dominant disabled mechanism (token-repoint, not opacity) — the doubled dimming was
|
|
191
|
+
never load-bearing, just an acknowledged v1 leftover. */
|
|
192
|
+
:scope:is([disabled]) {
|
|
193
|
+
pointer-events: none;
|
|
194
|
+
cursor: default;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* ── Forced colors (WHCM) ────────────────────────────────────────────────────────────────────── */
|
|
198
|
+
|
|
199
|
+
@media (forced-colors: active) {
|
|
200
|
+
/* Ensure the idle ring is visible. */
|
|
201
|
+
:scope::before {
|
|
202
|
+
border-color: ButtonText;
|
|
203
|
+
}
|
|
204
|
+
/* Checked: ring + fill map to Highlight (matches the switch/slider checked-fill forced-colors
|
|
205
|
+
pattern); the dot (::after) maps to HighlightText — the system's own paired "on-fill" colour,
|
|
206
|
+
guaranteed distinct from Highlight by the platform (same pairing as the switch's checked thumb). */
|
|
207
|
+
:scope:is([checked])::before,
|
|
208
|
+
:scope:state(checked)::before {
|
|
209
|
+
forced-color-adjust: none;
|
|
210
|
+
border-color: Highlight;
|
|
211
|
+
background: Highlight;
|
|
212
|
+
}
|
|
213
|
+
:scope:is([checked])::after,
|
|
214
|
+
:scope:state(checked)::after {
|
|
215
|
+
forced-color-adjust: none;
|
|
216
|
+
background: HighlightText;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { UIIndicatorElement } from '../_base/indicator-element.js';
|
|
2
|
+
export declare class UIRadioElement extends UIIndicatorElement {
|
|
3
|
+
/**
|
|
4
|
+
* LLD-C2: ARIA role for this leaf — `internals.role` is set by the UIIndicatorElement base
|
|
5
|
+
* from this declaration in `connected()`.
|
|
6
|
+
*/
|
|
7
|
+
static role: string;
|
|
8
|
+
/**
|
|
9
|
+
* LLD-C5: group wiring hook — called from `connected()` after all base wiring is in place.
|
|
10
|
+
* When this radio is nested inside a `ui-radio-group`, registers a capture-phase click guard:
|
|
11
|
+
* if the radio is already checked, the click is stopped (via stopImmediatePropagation) before it
|
|
12
|
+
* reaches the base's bubble-phase toggle handler. This enforces the radio-button invariant that a
|
|
13
|
+
* selected radio cannot be deselected by clicking — only by selecting another radio in the group.
|
|
14
|
+
*
|
|
15
|
+
* Without a group parent, this is a no-op and the base toggle runs normally (full checkbox semantics:
|
|
16
|
+
* click toggles unchecked ↔ checked).
|
|
17
|
+
*/
|
|
18
|
+
protected grouped(): void;
|
|
19
|
+
}
|