mirror of
https://github.com/kamranahmedse/driver.js.git
synced 2026-10-30 19:27:40 +08:00
Position the arrow with a continuous inline offset that points at the element's center, replacing the three fixed start/center/end CSS slots so it stays accurate when the element and popover differ in size. - Aim at the center of the element/popover overlap, and fall back to the configured align only when the element spans the whole popover edge - Flip the arrow to the perpendicular edge when the element scrolls clear of the popover, so it keeps pointing at the element instead of a corner - Move positioning into src/position.ts with pure, unit-tested helpers - Default popover side is now bottom instead of left - Drop the internal "over" value from the public Side type
108 lines
4.6 KiB
TypeScript
108 lines
4.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { calculateArrowOffset, calculateArrowTarget, resolveArrowSide } from "../src/position";
|
|
|
|
// These cover the pure geometry directly, without a DOM or the driver harness.
|
|
// The integration counterparts live in popover.test.ts.
|
|
|
|
describe("resolveArrowSide", () => {
|
|
// Popover box used across these cases.
|
|
const popover = { top: 100, bottom: 400, left: 600, right: 800 };
|
|
|
|
it("keeps the side edge when the element overlaps the popover vertically", () => {
|
|
const element = { top: 150, bottom: 250, left: 800, right: 900 };
|
|
expect(resolveArrowSide("left", element, popover)).toBe("left");
|
|
expect(resolveArrowSide("right", element, popover)).toBe("right");
|
|
});
|
|
|
|
it("points up when a side-placed element scrolls above the popover", () => {
|
|
// Element fully above the popover → arrow moves to the top edge (side "bottom").
|
|
const element = { top: -200, bottom: -50, left: 800, right: 900 };
|
|
expect(resolveArrowSide("left", element, popover)).toBe("bottom");
|
|
expect(resolveArrowSide("right", element, popover)).toBe("bottom");
|
|
});
|
|
|
|
it("points down when a side-placed element scrolls below the popover", () => {
|
|
// Element fully below the popover → arrow moves to the bottom edge (side "top").
|
|
const element = { top: 500, bottom: 600, left: 800, right: 900 };
|
|
expect(resolveArrowSide("left", element, popover)).toBe("top");
|
|
expect(resolveArrowSide("right", element, popover)).toBe("top");
|
|
});
|
|
|
|
it("keeps the edge when a top/bottom element overlaps the popover horizontally", () => {
|
|
const element = { top: 420, bottom: 520, left: 650, right: 750 };
|
|
expect(resolveArrowSide("top", element, popover)).toBe("top");
|
|
expect(resolveArrowSide("bottom", element, popover)).toBe("bottom");
|
|
});
|
|
|
|
it("points sideways when a top/bottom element scrolls clear horizontally", () => {
|
|
const toTheRight = { top: 420, bottom: 520, left: 900, right: 1000 };
|
|
expect(resolveArrowSide("bottom", toTheRight, popover)).toBe("left");
|
|
|
|
const toTheLeft = { top: 420, bottom: 520, left: 0, right: 100 };
|
|
expect(resolveArrowSide("bottom", toTheLeft, popover)).toBe("right");
|
|
});
|
|
});
|
|
|
|
describe("calculateArrowTarget", () => {
|
|
it("aims at the element's center when it sits inside the popover edge", () => {
|
|
// Element spans 200..260 (center 230) within a popover spanning 100..400.
|
|
expect(calculateArrowTarget(200, 260, 100, 400, "start")).toBe(130);
|
|
});
|
|
|
|
it("ignores alignment for an element that doesn't span the whole edge", () => {
|
|
// Same element; align "end" must not change where it points.
|
|
expect(calculateArrowTarget(200, 260, 100, 400, "end")).toBe(130);
|
|
});
|
|
|
|
it("aims at the center of the overlap when the element runs past one edge", () => {
|
|
// Element 300..500 against popover 100..400 overlaps on 300..400 → center 350.
|
|
expect(calculateArrowTarget(300, 500, 100, 400, "center")).toBe(250);
|
|
});
|
|
|
|
it("collapses onto the near edge when the element is entirely past the popover", () => {
|
|
// Entirely below → both endpoints clamp to the popover's far edge.
|
|
expect(calculateArrowTarget(500, 600, 100, 400, "center")).toBe(300);
|
|
// Entirely above → both clamp to the leading edge.
|
|
expect(calculateArrowTarget(-100, -50, 100, 400, "center")).toBe(0);
|
|
});
|
|
|
|
describe("when the element spans the whole popover edge", () => {
|
|
// Popover length 300. The element overlaps both ends, so alignment decides.
|
|
it("places the target at the start inset for align: start", () => {
|
|
expect(calculateArrowTarget(0, 500, 100, 400, "start")).toBe(20);
|
|
});
|
|
|
|
it("centers the target for align: center", () => {
|
|
expect(calculateArrowTarget(0, 500, 100, 400, "center")).toBe(150);
|
|
});
|
|
|
|
it("places the target at the end inset for align: end", () => {
|
|
expect(calculateArrowTarget(0, 500, 100, 400, "end")).toBe(280);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("calculateArrowOffset", () => {
|
|
it("centers the arrow box on the target", () => {
|
|
// Target 130 → box top 125 (arrow box is 10px wide).
|
|
expect(calculateArrowOffset(130, 300)).toBe(125);
|
|
});
|
|
|
|
it("clamps to the leading corner inset", () => {
|
|
expect(calculateArrowOffset(5, 300)).toBe(15);
|
|
});
|
|
|
|
it("clamps to the trailing corner inset", () => {
|
|
expect(calculateArrowOffset(295, 300)).toBe(275);
|
|
});
|
|
|
|
it("centers the arrow when the popover is too small for the insets", () => {
|
|
// length 30 can't honor a 15px inset on both sides → centered: (30-10)/2.
|
|
expect(calculateArrowOffset(20, 30)).toBe(10);
|
|
});
|
|
|
|
it("never returns a negative offset for a zero-length popover", () => {
|
|
expect(calculateArrowOffset(0, 0)).toBe(0);
|
|
});
|
|
});
|