@threadlabs/looma 0.23.18 → 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.18",
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",
@@ -1999,7 +1999,7 @@ var LoomaEditor = defineComponent({
1999
1999
  instance && props.editable && !mobile.value && (props.toolbarMode === "bubble" || props.toolbarMode === "popover" || props.toolbarMode === "contextual") ? h(BubbleMenu, {
2000
2000
  editor: instance,
2001
2001
  pluginKey: "looma-text-formatting-menu",
2002
- shouldShow: ({ editor: menuEditor, from, to }) => (props.toolbarMode !== "popover" || !props.toolbarOpen) && (props.toolbarMode === "contextual" ? menuEditor.isFocused || linkContextEditing.value : shouldShowTextFormattingToolbar(menuEditor, from, to)),
2002
+ shouldShow: ({ editor: menuEditor, from, to }) => (props.toolbarMode !== "popover" || !props.toolbarOpen) && (props.toolbarMode === "contextual" ? !slash.active && !mention.active && (menuEditor.isFocused || linkContextEditing.value) : shouldShowTextFormattingToolbar(menuEditor, from, to)),
2003
2003
  tippyOptions: {
2004
2004
  appendTo: () => root.value?.closest("dialog[open], [popover]") ?? document.body,
2005
2005
  onShow: () => announceOverlayOpen(root.value?.ownerDocument ?? document, root.value),