@threadlabs/looma 0.23.19 → 0.23.20

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.
@@ -0,0 +1,27 @@
1
+ const groups = new WeakMap();
2
+ const radios = new WeakMap();
3
+
4
+ // Native names own exclusivity and defaultChecked owns form reset. A group's model also has to
5
+ // update a child's reactive checked binding, which otherwise can overwrite the native property
6
+ // after controller startup. Coordinate through native elements, independently of the adapter.
7
+ export function checkRadio(input, checked) {
8
+ radios.get(input)?.(checked);
9
+ input.checked = checked;
10
+ }
11
+
12
+ export function radioGroupOwns(input) {
13
+ return groups.has(input.closest('[role="radiogroup"]'));
14
+ }
15
+
16
+ /** Register a group's selection callback until its controller disconnects. */
17
+ export function connectRadioGroup(element, apply) {
18
+ groups.set(element, apply);
19
+ return () => groups.delete(element);
20
+ }
21
+
22
+ /** Join a live group, or let its later startup find this control's checked binding. */
23
+ export function connectRadio(input, update) {
24
+ radios.set(input, update);
25
+ groups.get(input.closest('[role="radiogroup"]'))?.(input);
26
+ return () => radios.delete(input);
27
+ }
@@ -1,5 +1,6 @@
1
1
  import { afterFormReset } from "../shared/form-reset.js";
2
2
  import { describeInput } from "../shared/describe.js";
3
+ import { connectRadio, radioGroupOwns } from "../shared/radio-group.js";
3
4
  import { trackTrigger } from "../shared/trigger.js";
4
5
 
5
6
  // `checked` sets the control initially and whenever it changes; the user's changes update the state.
@@ -9,13 +10,15 @@ export default function controller(host) {
9
10
  const input = host.refs.input;
10
11
  describeInput(input, host.refs.description);
11
12
  const [trigger, stopTracking] = trackTrigger(host);
13
+ const stopRadio = connectRadio(input, (checked) => { host.state.internalChecked = checked; });
12
14
  let external = host.props.checked.value;
13
- host.state.internalChecked = Boolean(external);
15
+ if (!radioGroupOwns(input)) host.state.internalChecked = Boolean(external);
14
16
  const stop = host.effect(() => {
15
- input.defaultChecked = Boolean(host.props.checked.value);
17
+ const grouped = radioGroupOwns(input);
18
+ if (!grouped) input.defaultChecked = Boolean(host.props.checked.value);
16
19
  if (host.props.checked.value === external) return;
17
20
  external = host.props.checked.value;
18
- host.state.internalChecked = Boolean(external);
21
+ if (!grouped) host.state.internalChecked = Boolean(external);
19
22
  });
20
23
  const onChange = () => {
21
24
  host.state.internalChecked = input.checked;
@@ -25,12 +28,13 @@ export default function controller(host) {
25
28
  input.addEventListener("change", onChange);
26
29
  const stopReset = afterFormReset(input, () => {
27
30
  // Inside a ui-radio-group the group's value decides, whichever reset runs first.
28
- if (input.closest('[role="radiogroup"]')) return;
31
+ if (radioGroupOwns(input)) return;
29
32
  host.state.internalChecked = Boolean(external);
30
33
  input.checked = Boolean(external);
31
34
  });
32
35
  return () => {
33
36
  stop();
37
+ stopRadio();
34
38
  stopReset();
35
39
  stopTracking();
36
40
  input.removeEventListener("change", onChange);
@@ -1,4 +1,5 @@
1
1
  import { afterFormReset } from "../shared/form-reset.js";
2
+ import { checkRadio, connectRadioGroup } from "../shared/radio-group.js";
2
3
  import { trackTrigger } from "../shared/trigger.js";
3
4
 
4
5
  let groups = 0;
@@ -15,6 +16,14 @@ export default function controller(host) {
15
16
  // A radio's own required survives until the group's required changes, since either one makes the
16
17
  // whole name group required.
17
18
  let required = false;
19
+ const apply = (input, value = host.state.internalValue) => {
20
+ if (required) input.required = true;
21
+ input.name = name;
22
+ checkRadio(input, input.value === value);
23
+ // The value prop is the choice a native form reset returns to.
24
+ input.defaultChecked = input.value === String(external ?? "");
25
+ };
26
+ const stopGroup = connectRadioGroup(element, apply);
18
27
  host.state.internalValue = String(external ?? "");
19
28
  const stop = host.effect(() => {
20
29
  if (host.props.value.value !== external) {
@@ -26,10 +35,7 @@ export default function controller(host) {
26
35
  required = Boolean(host.props.required.value);
27
36
  for (const input of inputs()) {
28
37
  if (applyRequired) input.required = required;
29
- input.name = name;
30
- input.checked = input.value === value;
31
- // The value prop is the choice a form reset returns to.
32
- input.defaultChecked = input.value === String(external ?? "");
38
+ apply(input, value);
33
39
  }
34
40
  });
35
41
  const onChange = (event) => {
@@ -44,10 +50,11 @@ export default function controller(host) {
44
50
  element.addEventListener("change", onChange);
45
51
  const stopReset = afterFormReset(element, () => {
46
52
  host.state.internalValue = String(external ?? "");
47
- for (const input of inputs()) input.checked = input.value === host.state.internalValue;
53
+ for (const input of inputs()) checkRadio(input, input.value === host.state.internalValue);
48
54
  });
49
55
  return () => {
50
56
  stop();
57
+ stopGroup();
51
58
  stopReset();
52
59
  stopTracking();
53
60
  element.removeEventListener("change", onChange);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.23.19",
3
+ "version": "0.23.20",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",