@threadlabs/looma 0.26.25 → 0.26.26

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.
@@ -23,6 +23,9 @@
23
23
  <prop name="anticipatory" type="boolean" default="false"
24
24
  >Keeps the button out of sight until it is wanted: at rest a fine pointer sees only a small guide dot, and the button appears on hover, on focus, or when
25
25
  the pointer comes near inside a ui-affordance-scope. Touch devices and the solid variant always show it.</prop>
26
+ <prop name="guide" type="keyword" values="dot, bar" default="dot"
27
+ >The idle marker of an anticipatory button: dot for contextual actions, or a slim neutral rounded bar for a panel-edge control. The icon appears on hover or
28
+ keyboard focus; touch always shows the icon.</prop>
26
29
  <prop name="round" type="boolean" default="false">Use a fully circular shape.</prop>
27
30
  </defs>
28
31
  <button
@@ -172,6 +175,13 @@
172
175
  pointer-events: none;
173
176
  }
174
177
 
178
+ :host([anticipatory][guide="bar"])::before {
179
+ inline-size: 2px;
180
+ block-size: 20px;
181
+ background: var(--ui-border-strong);
182
+ opacity: 1;
183
+ }
184
+
175
185
  :host([anticipatory]):disabled::before {
176
186
  opacity: 0;
177
187
  }
@@ -226,7 +236,7 @@
226
236
  border-color var(--ui-motion-fast) var(--ui-motion-ease),
227
237
  color var(--ui-motion-fast) var(--ui-motion-ease),
228
238
  box-shadow var(--ui-motion-base) var(--ui-motion-ease),
229
- transform var(--ui-motion-fast) var(--ui-motion-ease);
239
+ scale var(--ui-motion-fast) var(--ui-motion-ease);
230
240
  }
231
241
 
232
242
  @media (hover: none), (pointer: coarse) {
@@ -356,7 +366,7 @@
356
366
  }
357
367
 
358
368
  :host:active:enabled:not([aria-disabled="true"]) {
359
- transform: scale(var(--ui-icon-button-active-scale, 0.96));
369
+ scale: var(--ui-icon-button-active-scale, 0.96);
360
370
  }
361
371
 
362
372
  :is(:host([loading]), :host([pending])) {
@@ -10,7 +10,8 @@
10
10
  >Positions the description around its trigger. Start and end on top or bottom follow the writing direction.</prop>
11
11
  <prop name="trigger" type="keyword" values="hover, click, focus" default="hover"
12
12
  >How the trigger opens it. hover also opens on keyboard focus; click toggles it and suits a help button, where hovering a question mark says nothing; focus
13
- opens only while the trigger holds focus.</prop>
13
+ opens only on keyboard-visible focus. Action activation dismisses hover/focus hints without removing focus; a fresh hover or keyboard focus can open them
14
+ again.</prop>
14
15
  <prop name="showDelay" type="number" default="500">Waits this many milliseconds before showing after pointer entry.</prop>
15
16
  <prop name="hideDelay" type="number" default="100">Waits this many milliseconds before hiding after pointer exit.</prop>
16
17
  <prop name="inverse" type="boolean" default="false">Use the high-contrast inverse surface (dark on light themes, light on dark themes).</prop>
@@ -28,6 +28,7 @@ function connect(host) {
28
28
  const overlayId = element.id || `ui-tooltip-${Math.random().toString(36).slice(2, 11)}`;
29
29
  if (!element.id) element.id = overlayId;
30
30
  let focused = false;
31
+ let pointerFocused = false;
31
32
  let trigger = null;
32
33
  let surface = null;
33
34
  let showTimer = null;
@@ -71,6 +72,8 @@ function connect(host) {
71
72
  }, Math.max(0, Number(host.props.hideDelay.value ?? 100)));
72
73
  };
73
74
  const onFocusin = () => {
75
+ // A pointer click may focus the action, but only visible keyboard focus keeps its hint open.
76
+ if (pointerFocused || !trigger?.matches(":focus-visible")) return;
74
77
  focused = true;
75
78
  clearTimers();
76
79
  setOpen(true, "keyboard");
@@ -78,6 +81,7 @@ function connect(host) {
78
81
  const onFocusout = (event) => {
79
82
  if (event.relatedTarget && element.contains(event.relatedTarget)) return;
80
83
  focused = false;
84
+ pointerFocused = false;
81
85
  clearTimers();
82
86
  setOpen(false, "keyboard");
83
87
  };
@@ -88,6 +92,8 @@ function connect(host) {
88
92
  else trigger.removeAttribute("aria-describedby");
89
93
  trigger.removeEventListener("keydown", onKeydown);
90
94
  trigger.removeEventListener("click", onClick);
95
+ trigger.removeEventListener("click", onActivate);
96
+ trigger.removeEventListener("pointerdown", onPointerDown);
91
97
  trigger.removeAttribute("aria-expanded");
92
98
  trigger.removeEventListener("pointerenter", onEnter);
93
99
  trigger.removeEventListener("pointerleave", onLeave);
@@ -98,6 +104,18 @@ function connect(host) {
98
104
  clearTimers();
99
105
  setOpen(!host.state.internalOpen, "pointer");
100
106
  };
107
+ const onPointerDown = () => {
108
+ pointerFocused = true;
109
+ focused = false;
110
+ clearTimers();
111
+ setOpen(false, "pointer");
112
+ };
113
+ const onActivate = (event) => {
114
+ // Action hints are spent once the action runs; the next hover or keyboard focus opens anew.
115
+ clearTimers();
116
+ focused = false;
117
+ setOpen(false, event.detail === 0 ? "keyboard" : "pointer");
118
+ };
101
119
  const attach = () => {
102
120
  if (!trigger) return;
103
121
  const ids = new Set((trigger.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean));
@@ -111,6 +129,8 @@ function connect(host) {
111
129
  trigger.setAttribute("aria-expanded", String(Boolean(host.state.internalOpen)));
112
130
  return;
113
131
  }
132
+ trigger.addEventListener("pointerdown", onPointerDown);
133
+ trigger.addEventListener("click", onActivate);
114
134
  if (how === "hover") {
115
135
  trigger.addEventListener("pointerenter", onEnter);
116
136
  trigger.addEventListener("pointerleave", onLeave);