@mlola-ui/behavior 1.0.11 → 1.0.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mlola-ui/behavior",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Framework-free behavior for Mlola UI markup: standard DOM, no runtime peers",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  clampToStep,
22
22
  focusTrapIndex,
23
23
  percentOf,
24
+ fitTooltip,
24
25
  placeFloating,
25
26
  rovingIndex,
26
27
  sliderValueForKey,
@@ -487,6 +488,7 @@ const behaviors = {
487
488
  const trigger = root.firstElementChild;
488
489
  if (!tip || !trigger) return [];
489
490
  const delay = Number(root.dataset.mlDelay ?? 200);
491
+ const side = tip.dataset.side || "top";
490
492
  let timer = null;
491
493
 
492
494
  const show = () => {
@@ -494,6 +496,13 @@ const behaviors = {
494
496
  timer = setTimeout(() => {
495
497
  tip.hidden = false;
496
498
  tip.dataset.state = "open";
499
+ // Measure from the side the markup asks for, then flip or slide to stay on screen.
500
+ tip.dataset.side = side;
501
+ tip.style.removeProperty("--ml-tooltip-shift");
502
+ const a = trigger.getBoundingClientRect();
503
+ const fit = fitTooltip({ x: a.left, y: a.top, width: a.width, height: a.height }, { width: tip.offsetWidth, height: tip.offsetHeight }, { width: document.documentElement.clientWidth, height: window.innerHeight }, side);
504
+ tip.dataset.side = fit.side;
505
+ tip.style.setProperty("--ml-tooltip-shift", `${fit.shift}px`);
497
506
  if (tip.id) trigger.setAttribute("aria-describedby", tip.id);
498
507
  }, Math.max(0, delay));
499
508
  };
@@ -166,3 +166,20 @@ export function placeFloating(anchor, floating, viewport, { side = "bottom", ali
166
166
  y = Math.min(Math.max(y, padding), Math.max(padding, viewport.height - floating.height - padding));
167
167
  return { x: Math.round(x), y: Math.round(y), side: chosen };
168
168
  }
169
+
170
+ /**
171
+ * Where a tooltip anchored beside its trigger by CSS should sit to stay on
172
+ * screen: the side it opens on (flipped when the other has more room, or
173
+ * moved above when neither side has room across), and how far to slide it
174
+ * along that edge. The arrow slides back by the same amount, so it still
175
+ * points at the trigger.
176
+ */
177
+ export function fitTooltip(anchor, tip, viewport, side = "top", { offset = 8, padding = 8 } = {}) {
178
+ const across = side === "left" || side === "right";
179
+ const roomAcross = Math.max(anchor.x, viewport.width - anchor.x - anchor.width) - padding;
180
+ const wanted = across && tip.width + offset > roomAcross ? "top" : side;
181
+ const placed = placeFloating(anchor, tip, viewport, { side: wanted, align: "center", offset, padding });
182
+ const vertical = placed.side === "top" || placed.side === "bottom";
183
+ const centered = vertical ? anchor.x + anchor.width / 2 - tip.width / 2 : anchor.y + anchor.height / 2 - tip.height / 2;
184
+ return { side: placed.side, shift: Math.round((vertical ? placed.x : placed.y) - centered) };
185
+ }
package/src/logic.d.ts CHANGED
@@ -134,3 +134,12 @@ export function placeFloating(
134
134
  viewport: { width: number; height: number },
135
135
  options?: { side?: Side; align?: Align; offset?: number; padding?: number },
136
136
  ): Placement;
137
+
138
+ /** A CSS-anchored tooltip's side and its slide along that edge to stay on screen. */
139
+ export function fitTooltip(
140
+ anchor: Rect,
141
+ tip: { width: number; height: number },
142
+ viewport: { width: number; height: number },
143
+ side?: Side,
144
+ options?: { offset?: number; padding?: number },
145
+ ): { side: Side; shift: number };