@reshaped/utilities 3.10.0-canary.9 → 3.10.0

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.
@@ -19,7 +19,7 @@ const applyPosition = (args) => {
19
19
  };
20
20
  contentClone.style.cssText = "";
21
21
  if (!triggerBounds)
22
- throw new Error("Trigger bounds are required");
22
+ return { position };
23
23
  const resolvedTriggerBounds = getRectFromCoordinates(triggerBounds);
24
24
  Object.keys(RESET_STYLES).forEach((_key) => {
25
25
  const key = _key;
@@ -0,0 +1,15 @@
1
+ import type { Position, Coordinates } from "../types";
2
+ type CreateSafeAreaArgs = {
3
+ coordinates: Coordinates;
4
+ content: HTMLElement;
5
+ trigger: HTMLElement;
6
+ position: Position;
7
+ existingSafeArea: HTMLElement | null;
8
+ };
9
+ /**
10
+ * Creates an SVG element representing the safe area triangle
11
+ * from the mouse coordinates to the closest side of the content.
12
+ * The safe area is limited by the trigger's boundaries.
13
+ */
14
+ declare const createSafeArea: (args: CreateSafeAreaArgs) => HTMLElement;
15
+ export default createSafeArea;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Creates an SVG element representing the safe area triangle
3
+ * from the mouse coordinates to the closest side of the content.
4
+ * The safe area is limited by the trigger's boundaries.
5
+ */
6
+ const createSafeArea = (args) => {
7
+ const { coordinates, content, trigger, position, existingSafeArea } = args;
8
+ const contentBounds = content.getBoundingClientRect();
9
+ const isHorizontal = position.startsWith("start") || position.startsWith("end");
10
+ const isLogicalStart = position.startsWith("start") || position.startsWith("top");
11
+ const BUFFER = 10;
12
+ let points;
13
+ if (isHorizontal) {
14
+ const originX = isLogicalStart ? coordinates.x + BUFFER : coordinates.x - BUFFER;
15
+ const x = isLogicalStart ? contentBounds.left + contentBounds.width : contentBounds.left;
16
+ const topCoordinates = { x, y: contentBounds.top };
17
+ const bottomCoordinates = { x, y: contentBounds.top + contentBounds.height };
18
+ points = `${originX},${coordinates.y} ${topCoordinates.x},${topCoordinates.y} ${bottomCoordinates.x},${bottomCoordinates.y}`;
19
+ }
20
+ else {
21
+ const originY = isLogicalStart ? coordinates.y + BUFFER : coordinates.y - BUFFER;
22
+ const y = isLogicalStart ? contentBounds.top + contentBounds.height : contentBounds.top;
23
+ const leftCoordinates = { x: contentBounds.left, y };
24
+ const rightCoordinates = { x: contentBounds.left + contentBounds.width, y };
25
+ points = `${coordinates.x},${originY} ${leftCoordinates.x},${leftCoordinates.y} ${rightCoordinates.x},${rightCoordinates.y}`;
26
+ }
27
+ // Calculate SVG viewBox to cover the triangle area
28
+ const pointCoords = points.split(" ").map((p) => p.split(",").map(Number));
29
+ const minX = Math.min(...pointCoords.map((p) => p[0]));
30
+ const maxX = Math.max(...pointCoords.map((p) => p[0]));
31
+ const minY = Math.min(...pointCoords.map((p) => p[1]));
32
+ const maxY = Math.max(...pointCoords.map((p) => p[1]));
33
+ const svgWidth = maxX - minX;
34
+ const svgHeight = maxY - minY;
35
+ // Adjust points relative to SVG origin
36
+ const adjustedPoints = pointCoords.map((p) => `${p[0] - minX},${p[1] - minY}`).join(" ");
37
+ const styles = `
38
+ position: fixed;
39
+ left: ${minX}px;
40
+ top: ${minY}px;
41
+ width: ${svgWidth}px;
42
+ height: ${svgHeight}px;
43
+ pointer-events: none;
44
+ z-index: 9999;
45
+ `;
46
+ const svgContent = `
47
+ <svg width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}">
48
+ <polygon points="${adjustedPoints}" fill="red" opacity="0.5" style="pointer-events: auto;" />
49
+ </svg>
50
+ `;
51
+ if (existingSafeArea) {
52
+ existingSafeArea.style.cssText = styles;
53
+ existingSafeArea.innerHTML = svgContent;
54
+ return existingSafeArea;
55
+ }
56
+ const container = document.createElement("div");
57
+ container.style.cssText = styles;
58
+ container.innerHTML = svgContent;
59
+ trigger.appendChild(container);
60
+ return container;
61
+ };
62
+ export default createSafeArea;
@@ -0,0 +1,5 @@
1
+ type Args = {
2
+ el: HTMLElement | null;
3
+ };
4
+ declare const _default: (args: Args) => HTMLElement;
5
+ export default _default;
@@ -0,0 +1,18 @@
1
+ import getShadowRoot from "../../dom/getShadowRoot.js";
2
+ const findClosestPositionContainer = (args) => {
3
+ const { el, iteration = 0 } = args;
4
+ const style = el && window.getComputedStyle(el);
5
+ const position = style?.position;
6
+ const isFixed = position === "fixed" || position === "sticky";
7
+ if (iteration === 0) {
8
+ const shadowRoot = getShadowRoot(el);
9
+ if (shadowRoot?.firstElementChild)
10
+ return shadowRoot.firstElementChild;
11
+ }
12
+ if (el === document.body || !el)
13
+ return document.body;
14
+ if (isFixed)
15
+ return el;
16
+ return findClosestPositionContainer({ el: el.parentElement, iteration: iteration + 1 });
17
+ };
18
+ export default (args) => findClosestPositionContainer({ ...args, iteration: 0 });
@@ -0,0 +1,5 @@
1
+ type Args = {
2
+ el: HTMLElement;
3
+ };
4
+ declare const _default: (args: Args) => HTMLElement | null;
5
+ export default _default;
@@ -0,0 +1,12 @@
1
+ const findClosestScrollableContainer = (args) => {
2
+ const { el, iteration } = args;
3
+ const style = el && window.getComputedStyle(el);
4
+ const overflowY = style.overflowY;
5
+ const isScrollable = overflowY.includes("scroll") || overflowY.includes("auto");
6
+ if (!el.parentElement)
7
+ return null;
8
+ if (isScrollable && el.scrollHeight > el.clientHeight)
9
+ return el;
10
+ return findClosestScrollableContainer({ el: el.parentElement, iteration: iteration + 1 });
11
+ };
12
+ export default (args) => findClosestScrollableContainer({ ...args, iteration: 0 });
@@ -0,0 +1,46 @@
1
+ import { expect, test, describe } from "vitest";
2
+ import findClosestFixedContainer from "../findClosestFixedContainer.js";
3
+ describe("flyout/findClosestFixedContainer", () => {
4
+ test("returns document.body when element is null", () => {
5
+ const result = findClosestFixedContainer({ el: null });
6
+ expect(result).toBe(document.body);
7
+ });
8
+ test("returns document.body when element is document.body", () => {
9
+ const result = findClosestFixedContainer({ el: document.body });
10
+ expect(result).toBe(document.body);
11
+ });
12
+ test("returns the element itself when it has position fixed", () => {
13
+ const fixedEl = document.createElement("div");
14
+ fixedEl.style.position = "fixed";
15
+ document.body.appendChild(fixedEl);
16
+ const result = findClosestFixedContainer({ el: fixedEl });
17
+ expect(result).toBe(fixedEl);
18
+ });
19
+ test("returns the element itself when it has position sticky", () => {
20
+ const stickyEl = document.createElement("div");
21
+ stickyEl.style.position = "sticky";
22
+ document.body.appendChild(stickyEl);
23
+ const result = findClosestFixedContainer({ el: stickyEl });
24
+ expect(result).toBe(stickyEl);
25
+ });
26
+ test("returns grandparent when it has position fixed", () => {
27
+ const fixedEl = document.createElement("div");
28
+ fixedEl.style.position = "fixed";
29
+ const childEl = document.createElement("div");
30
+ const grandChildEl = document.createElement("div");
31
+ childEl.appendChild(grandChildEl);
32
+ fixedEl.appendChild(childEl);
33
+ document.body.appendChild(fixedEl);
34
+ const result = findClosestFixedContainer({ el: grandChildEl });
35
+ expect(result).toBe(fixedEl);
36
+ });
37
+ test("returns document.body when no fixed container is found", () => {
38
+ const staticEl = document.createElement("div");
39
+ staticEl.style.position = "static";
40
+ const childEl = document.createElement("div");
41
+ staticEl.appendChild(childEl);
42
+ document.body.appendChild(staticEl);
43
+ const result = findClosestFixedContainer({ el: childEl });
44
+ expect(result).toBe(document.body);
45
+ });
46
+ });
@@ -0,0 +1,66 @@
1
+ import { expect, test, describe } from "vitest";
2
+ import findClosestScrollableContainer from "../findClosestScrollableContainer.js";
3
+ describe("flyout/findClosestScrollableContainer", () => {
4
+ test("returns null when element has no parent", () => {
5
+ const result = findClosestScrollableContainer({ el: document.documentElement });
6
+ expect(result).toBe(null);
7
+ });
8
+ test("returns the element itself has overflow auto", () => {
9
+ const scrollableEl = document.createElement("div");
10
+ scrollableEl.style.overflowY = "auto";
11
+ scrollableEl.style.height = "100px";
12
+ const childEl = document.createElement("div");
13
+ childEl.style.height = "200px";
14
+ scrollableEl.appendChild(childEl);
15
+ document.body.appendChild(scrollableEl);
16
+ const result = findClosestScrollableContainer({ el: scrollableEl });
17
+ expect(result).toBe(scrollableEl);
18
+ });
19
+ test("returns the element itself has overflow scroll", () => {
20
+ const scrollableEl = document.createElement("div");
21
+ scrollableEl.style.overflowY = "scroll";
22
+ scrollableEl.style.height = "100px";
23
+ const childEl = document.createElement("div");
24
+ childEl.style.height = "200px";
25
+ scrollableEl.appendChild(childEl);
26
+ document.body.appendChild(scrollableEl);
27
+ const result = findClosestScrollableContainer({ el: scrollableEl });
28
+ expect(result).toBe(scrollableEl);
29
+ });
30
+ test("returns grandparent when it is scrollable", () => {
31
+ const scrollableEl = document.createElement("div");
32
+ scrollableEl.style.overflowY = "auto";
33
+ scrollableEl.style.height = "100px";
34
+ const childEl = document.createElement("div");
35
+ childEl.style.height = "200px";
36
+ const grandChildEl = document.createElement("div");
37
+ grandChildEl.style.height = "100px";
38
+ childEl.appendChild(grandChildEl);
39
+ scrollableEl.appendChild(childEl);
40
+ document.body.appendChild(scrollableEl);
41
+ const result = findClosestScrollableContainer({ el: grandChildEl });
42
+ expect(result).toBe(scrollableEl);
43
+ });
44
+ test("returns null when no scrollable container is found", () => {
45
+ const scrollableEl = document.createElement("div");
46
+ scrollableEl.style.overflowY = "visible";
47
+ scrollableEl.style.height = "100px";
48
+ const childEl = document.createElement("div");
49
+ childEl.style.height = "200px";
50
+ scrollableEl.appendChild(childEl);
51
+ document.body.appendChild(scrollableEl);
52
+ const result = findClosestScrollableContainer({ el: childEl });
53
+ expect(result).toBe(null);
54
+ });
55
+ test("does not return element with overflow auto but scrollHeight <= clientHeight", () => {
56
+ const scrollableEl = document.createElement("div");
57
+ scrollableEl.style.overflowY = "auto";
58
+ scrollableEl.style.height = "100px";
59
+ const childEl = document.createElement("div");
60
+ childEl.style.height = "100px";
61
+ scrollableEl.appendChild(childEl);
62
+ document.body.appendChild(scrollableEl);
63
+ const result = findClosestScrollableContainer({ el: childEl });
64
+ expect(result).toBe(null);
65
+ });
66
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reshaped/utilities",
3
3
  "description": "Vanilla JS utilities for implementing common UI patterns",
4
- "version": "3.10.0-canary.9",
4
+ "version": "3.10.0",
5
5
  "license": "MIT",
6
6
  "homepage": "https://reshaped.so",
7
7
  "repository": {