@threadlabs/looma 0.22.10 → 0.22.11

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.
@@ -14,6 +14,13 @@ function syncScrollLock(document, state) {
14
14
  else document.documentElement.style.removeProperty("overflow");
15
15
  }
16
16
 
17
+ function syncModalBackdrops(state) {
18
+ const topModal = state.records.findLast((record) => record.modal);
19
+ for (const record of state.records) {
20
+ (record.modalElement ?? record.element).toggleAttribute("data-ui-backdrop-hidden", Boolean(record.modal && record !== topModal));
21
+ }
22
+ }
23
+
17
24
  function requestClose(document, reason, trigger) {
18
25
  const record = stateFor(document).records.at(-1);
19
26
  if (!record || (record.dismissible === false && (reason === "escape" || reason === "light-dismiss")) || record.canClose?.(reason) === false) return false;
@@ -55,13 +62,36 @@ function removeListeners(document, state) {
55
62
  export function openOverlay(record) {
56
63
  const document = record.element.ownerDocument;
57
64
  const state = stateFor(document);
58
- closeOverlay(document, record.id);
65
+ const index = state.records.findIndex((entry) => entry.id === record.id);
66
+ if (index >= 0) {
67
+ const previous = state.records[index];
68
+ state.records[index] = record;
69
+ if (previous.modal !== record.modal) {
70
+ state.modalCount += record.modal ? 1 : -1;
71
+ syncScrollLock(document, state);
72
+ }
73
+ if (previous.modalElement && previous.modalElement !== record.modalElement) previous.modalElement.removeAttribute("data-ui-backdrop-hidden");
74
+ syncModalBackdrops(state);
75
+ return;
76
+ }
59
77
  state.records.push(record);
60
78
  if (record.modal) {
61
79
  state.modalCount += 1;
62
80
  syncScrollLock(document, state);
63
81
  }
82
+ syncModalBackdrops(state);
64
83
  ensureListeners(document, state);
84
+ announceOverlayOpen(document, record.id);
85
+ }
86
+
87
+ export function onOverlayOpen(document, listener) {
88
+ const onOpen = (event) => listener(event.detail.id);
89
+ document.addEventListener("ui-overlay-open", onOpen);
90
+ return () => document.removeEventListener("ui-overlay-open", onOpen);
91
+ }
92
+
93
+ export function announceOverlayOpen(document, id) {
94
+ document.dispatchEvent(new CustomEvent("ui-overlay-open", { detail: { id } }));
65
95
  }
66
96
 
67
97
  export function closeOverlay(document, id) {
@@ -69,10 +99,12 @@ export function closeOverlay(document, id) {
69
99
  const index = state.records.findIndex((record) => record.id === id);
70
100
  if (index >= 0) {
71
101
  const [record] = state.records.splice(index, 1);
102
+ (record.modalElement ?? record.element).removeAttribute("data-ui-backdrop-hidden");
72
103
  if (record?.modal) {
73
104
  state.modalCount = Math.max(0, state.modalCount - 1);
74
105
  syncScrollLock(document, state);
75
106
  }
107
+ syncModalBackdrops(state);
76
108
  }
77
109
  removeListeners(document, state);
78
110
  }
@@ -254,6 +254,12 @@
254
254
  animation: ui-dialog-backdrop-enter var(--_ui-dialog-duration, var(--ui-motion-base)) var(--ui-motion-ease);
255
255
  }
256
256
 
257
+ :host[open][data-ui-backdrop-hidden]::backdrop {
258
+ background-color: transparent;
259
+ animation: none;
260
+ transition: none;
261
+ }
262
+
257
263
  @keyframes ui-dialog-backdrop-enter {
258
264
  from {
259
265
  background-color: transparent;
@@ -72,7 +72,7 @@ export default function controller(host) {
72
72
  if (modal) dialog.showModal();
73
73
  else dialog.show();
74
74
  }
75
- openOverlay({ id: overlayId, modal, element, dismissible: true, canClose: (reason) => reason === "light-dismiss" ? closedBy === "any" : reason !== "escape" || closedBy !== "none", requestClose });
75
+ openOverlay({ id: overlayId, modal, element, modalElement: dialog, dismissible: true, canClose: (reason) => reason === "light-dismiss" ? closedBy === "any" : reason !== "escape" || closedBy !== "none", requestClose });
76
76
  } else {
77
77
  if (dialog.open) dialog.close();
78
78
  closeOverlay(document, overlayId);
@@ -83,7 +83,7 @@ export default function controller(host) {
83
83
  lastOpen = open;
84
84
  };
85
85
  const onClose = (event) => {
86
- if (event instanceof CustomEvent) return;
86
+ if (event instanceof CustomEvent || event.target !== dialog) return;
87
87
  // Consumers receive one Looma close event with a reason, not an additional native close event.
88
88
  event.stopImmediatePropagation();
89
89
  if (dialog.open || !host.state.internalOpen) return;
@@ -134,6 +134,20 @@
134
134
  white-space: nowrap;
135
135
  }
136
136
 
137
+ /* Fixed text is part of the value, but should not look editable. */
138
+ .affix:has([slot="prefix"]),
139
+ .affix:has([slot="suffix"]) {
140
+ background: var(--ui-surface-muted);
141
+ }
142
+
143
+ .affix:has([slot="prefix"]) {
144
+ border-inline-end: 1px solid var(--ui-border-divider);
145
+ }
146
+
147
+ .affix:has([slot="suffix"]) {
148
+ border-inline-start: 1px solid var(--ui-border-divider);
149
+ }
150
+
137
151
  /* Centred rather than padded above and below: the group is exactly a lone Input's height, so a
138
152
  small button sits inside it with the difference split evenly, and the field keeps its height. */
139
153
  .action {
@@ -105,6 +105,14 @@
105
105
  background: var(--ui-overlay-backdrop);
106
106
  }
107
107
 
108
+ dialog:modal {
109
+ background: transparent;
110
+ }
111
+
112
+ dialog[data-ui-backdrop-hidden]::backdrop {
113
+ background: transparent;
114
+ }
115
+
108
116
  .panel {
109
117
  position: relative;
110
118
  display: flex;
@@ -1,8 +1,11 @@
1
+ import { closeOverlay, openOverlay } from "../shared/overlay.js";
1
2
  import { trackTrigger } from "../shared/trigger.js";
2
3
 
3
4
  /** Owns native dialog state while leaving query and result state to the application. */
4
5
  export default function controller(host) {
5
6
  const dialog = host.refs.dialog;
7
+ const document = dialog.ownerDocument;
8
+ const overlayId = `ui-search-shell-${Math.random().toString(36).slice(2, 11)}`;
6
9
  const [trigger, stopTracking] = trackTrigger(host);
7
10
  let external = host.props.open.value;
8
11
  let activeModal = Boolean(host.props.modal.value);
@@ -31,8 +34,12 @@ export default function controller(host) {
31
34
  if (host.state.internalOpen && !dialog.open) {
32
35
  if (modal) dialog.showModal();
33
36
  else dialog.show();
34
- } else if (!host.state.internalOpen) {
37
+ }
38
+ if (host.state.internalOpen) {
39
+ openOverlay({ id: overlayId, modal, element: dialog, modalElement: dialog, dismissible: false, requestClose: () => {} });
40
+ } else {
35
41
  closeNative();
42
+ closeOverlay(document, overlayId);
36
43
  }
37
44
  });
38
45
 
@@ -69,6 +76,8 @@ export default function controller(host) {
69
76
  // Only the dialog's own close: a tooltip, menu, or popover inside it reports "close" too, and it bubbles.
70
77
  const onClose = (event) => {
71
78
  if (event.target !== dialog) return;
79
+ if (dialog.open) return;
80
+ closeOverlay(document, overlayId);
72
81
  if (!suppressNativeClose && host.state.internalOpen) dispatchClose("action", trigger());
73
82
  };
74
83
  dialog.addEventListener("cancel", onCancel);
@@ -83,5 +92,6 @@ export default function controller(host) {
83
92
  dialog.removeEventListener("click", onClick);
84
93
  dialog.removeEventListener("close", onClose);
85
94
  closeNative();
95
+ closeOverlay(document, overlayId);
86
96
  };
87
97
  }
@@ -1,3 +1,5 @@
1
+ import { announceOverlayOpen } from "../shared/overlay.js";
2
+
1
3
  const BREAKPOINTS = { sm: "40rem", md: "48rem", lg: "64rem" };
2
4
 
3
5
  function positive(value, fallback) {
@@ -134,6 +136,7 @@ export default function controller(host) {
134
136
  // report it again, forever. Only the drawer's own popover ToggleEvent, which has a newState, counts.
135
137
  const onPopoverToggle = (event) => {
136
138
  if (!host.state.drawer || event.target !== element || typeof event.newState !== "string") return;
139
+ if (event.newState === "open") announceOverlayOpen(document, element);
137
140
  host.dispatch("toggle", { open: event.newState === "open", mode: "drawer", trigger: "programmatic" });
138
141
  };
139
142
 
@@ -1,3 +1,4 @@
1
+ import { announceOverlayOpen } from "../shared/overlay.js";
1
2
  import { trackTrigger } from "../shared/trigger.js";
2
3
 
3
4
  const instances = new WeakMap();
@@ -16,8 +17,11 @@ export default function controller(host) {
16
17
  const [trigger, stopTracking] = trackTrigger(host);
17
18
  const toasts = () => host.state.toasts ?? [];
18
19
  const authored = () => Array.from(element.children).some((child) => !child.classList.contains("toast"));
20
+ let announcedVisible = false;
19
21
  const sync = () => {
20
22
  const visible = authored() || toasts().length > 0;
23
+ if (visible && !announcedVisible) announceOverlayOpen(element.ownerDocument, element);
24
+ announcedVisible = visible;
21
25
  if (visible && !element.matches(":popover-open")) element.showPopover();
22
26
  else if (!visible && element.matches(":popover-open")) element.hidePopover();
23
27
  };
@@ -1,4 +1,4 @@
1
- import { closeOverlay, createAnchoredSurface, createIdResolver, openOverlay } from "../shared/overlay.js";
1
+ import { closeOverlay, createAnchoredSurface, createIdResolver, onOverlayOpen, openOverlay } from "../shared/overlay.js";
2
2
 
3
3
  const warmTooltips = new WeakMap();
4
4
 
@@ -138,12 +138,22 @@ export default function controller(host) {
138
138
  surface = createAnchoredSurface(element, { anchor: trigger, placement: nextPlacement, gap: offset });
139
139
  };
140
140
  const close = (reason, input) => {
141
- if (reason !== "escape" && reason !== "light-dismiss") return;
141
+ if (reason !== "escape" && reason !== "light-dismiss" && reason !== "programmatic") return;
142
142
  clearTimers();
143
+ if (!host.state.internalOpen) return;
143
144
  host.state.internalOpen = false;
144
145
  trackVisible(false);
146
+ surface?.hide();
147
+ closeOverlay(document, overlayId);
148
+ if (trigger && host.props.trigger.value === "click") trigger.setAttribute("aria-expanded", "false");
145
149
  host.dispatch("close", { open: false, reason, trigger: input });
146
150
  };
151
+ const stopOverlayOpen = onOverlayOpen(document, (id) => {
152
+ if (id === overlayId) return;
153
+ clearTimers();
154
+ focused = false;
155
+ close("programmatic", "programmatic");
156
+ });
147
157
  const apply = () => {
148
158
  const externalOpen = Boolean(host.props.open.value);
149
159
  if (externalOpen !== lastExternalOpen) {
@@ -169,6 +179,7 @@ export default function controller(host) {
169
179
  apply();
170
180
  return () => {
171
181
  stop();
182
+ stopOverlayOpen();
172
183
  ids.stop();
173
184
  clearTimers();
174
185
  trackVisible(false);