Files
sendou.ink/app/components/elements/floating-layer.test.ts

285 lines
7.6 KiB
TypeScript

import { describe, expect, test } from "vitest";
import * as FloatingLayer from "./floating-layer";
const BOUNDS: FloatingLayer.Bounds = {
top: 0,
right: 1000,
bottom: 800,
left: 0,
};
const VIEWPORT = { width: 1000, height: 800 };
const GAP = 8;
const PADDING = 12;
function anchorAt(top: number, left = 100): FloatingLayer.Rect {
return { top, left, width: 200, height: 40 };
}
describe("FloatingLayer.resolve", () => {
test.each([
{
why: "stays below when the content fits there",
anchorTop: 100,
height: 300,
side: "bottom",
availableHeight: 640,
},
{
why: "flips above when the content does not fit below but has more room there",
anchorTop: 700,
height: 300,
side: "top",
availableHeight: 680,
},
{
why: "takes the roomier side when the content fits on neither",
anchorTop: 700,
height: 900,
side: "top",
availableHeight: 680,
},
{
why: "keeps the asked side when it is the roomier one though the content fits on neither",
anchorTop: 100,
height: 900,
side: "bottom",
availableHeight: 640,
},
])("$why", ({ anchorTop, height, side, availableHeight }) => {
const resolution = FloatingLayer.resolve({
anchor: anchorAt(anchorTop),
floating: { width: 200, height },
bounds: BOUNDS,
placement: "bottom",
gap: GAP,
padding: PADDING,
rtl: false,
});
expect(resolution.side).toBe(side);
expect(resolution.availableHeight).toBe(availableHeight);
expect(resolution.availableWidth).toBe(976);
});
test("keeps a sticky header at the top of the bounds out of the room above", () => {
const resolution = FloatingLayer.resolve({
anchor: anchorAt(300),
floating: { width: 200, height: 250 },
bounds: { ...BOUNDS, top: 55 },
placement: "top",
gap: GAP,
padding: PADDING,
rtl: false,
});
expect(resolution.side).toBe("bottom");
expect(resolution.availableHeight).toBe(440);
});
test("puts a beside placement on the other side when the content has more room there", () => {
const resolution = FloatingLayer.resolve({
anchor: { top: 100, left: 800, width: 100, height: 40 },
floating: { width: 200, height: 40 },
bounds: BOUNDS,
placement: "right",
gap: GAP,
padding: PADDING,
rtl: false,
});
expect(resolution.side).toBe("left");
expect(resolution.availableWidth).toBe(780);
expect(resolution.availableHeight).toBe(776);
});
test.each([
{ placement: "bottom", rtl: false, align: "center", origin: "50% 0%" },
{ placement: "bottom start", rtl: false, align: "start", origin: "0% 0%" },
{ placement: "bottom end", rtl: false, align: "end", origin: "100% 0%" },
{ placement: "bottom end", rtl: true, align: "end", origin: "0% 0%" },
{ placement: "top", rtl: false, align: "center", origin: "50% 100%" },
{ placement: "right", rtl: false, align: "center", origin: "0% 50%" },
] as const)(
"$placement (rtl: $rtl) aligns $align with its origin at $origin",
({ placement, rtl, align, origin }) => {
const resolution = FloatingLayer.resolve({
anchor: anchorAt(100, 400),
floating: { width: 100, height: 50 },
bounds: BOUNDS,
placement,
gap: GAP,
padding: PADDING,
rtl,
});
expect(resolution.align).toBe(align);
expect(resolution.transformOrigin).toBe(origin);
},
);
});
describe("FloatingLayer.spaceAcross", () => {
test.each([
{ placement: "bottom", space: 976 },
{ placement: "top", space: 976 },
{ placement: "right", space: 776 },
] as const)("$placement has $space across", ({ placement, space }) => {
expect(FloatingLayer.spaceAcross(BOUNDS, placement, PADDING)).toBe(space);
});
});
describe("FloatingLayer.isVerticalPlacement", () => {
test.each([
{ placement: "bottom start", vertical: true },
{ placement: "top", vertical: true },
{ placement: "right", vertical: false },
] as const)("$placement -> $vertical", ({ placement, vertical }) => {
expect(FloatingLayer.isVerticalPlacement(placement)).toBe(vertical);
});
});
describe("FloatingLayer.insets", () => {
const box = { width: 100, height: 50 };
test.each([
{
why: "centers under the anchor",
anchor: anchorAt(100),
floating: box,
side: "bottom",
align: "center",
rtl: false,
expected: { top: 148, right: null, bottom: null, left: 150 },
},
{
why: "hangs from the edge facing the anchor above it",
anchor: anchorAt(100),
floating: box,
side: "top",
align: "center",
rtl: false,
expected: { top: null, right: null, bottom: 708, left: 150 },
},
{
why: "lines up with the start edge of the anchor",
anchor: anchorAt(100),
floating: box,
side: "bottom",
align: "start",
rtl: false,
expected: { top: 148, right: null, bottom: null, left: 100 },
},
{
why: "lines up with the end edge of the anchor",
anchor: anchorAt(100),
floating: box,
side: "bottom",
align: "end",
rtl: false,
expected: { top: 148, right: null, bottom: null, left: 200 },
},
{
why: "reads start as the right edge in rtl",
anchor: anchorAt(100),
floating: box,
side: "bottom",
align: "start",
rtl: true,
expected: { top: 148, right: null, bottom: null, left: 200 },
},
{
why: "shifts back inside the bounds on the right",
anchor: anchorAt(100, 900),
floating: box,
side: "bottom",
align: "center",
rtl: false,
expected: { top: 148, right: null, bottom: null, left: 888 },
},
{
why: "shifts back inside the bounds on the left",
anchor: { top: 100, left: 0, width: 50, height: 40 },
floating: box,
side: "bottom",
align: "center",
rtl: false,
expected: { top: 148, right: null, bottom: null, left: 12 },
},
{
why: "sits at the padding when wider than the bounds",
anchor: anchorAt(100),
floating: { width: 1200, height: 50 },
side: "bottom",
align: "center",
rtl: false,
expected: { top: 148, right: null, bottom: null, left: 12 },
},
{
why: "sits beside the anchor",
anchor: { top: 300, left: 100, width: 100, height: 40 },
floating: box,
side: "right",
align: "center",
rtl: false,
expected: { top: 295, right: null, bottom: null, left: 208 },
},
{
why: "hangs from the edge facing the anchor on its left",
anchor: { top: 300, left: 100, width: 100, height: 40 },
floating: box,
side: "left",
align: "center",
rtl: false,
expected: { top: 295, right: 908, bottom: null, left: null },
},
{
why: "shifts down inside the bounds beside a high anchor",
anchor: { top: 10, left: 100, width: 100, height: 40 },
floating: box,
side: "right",
align: "center",
rtl: false,
expected: { top: 12, right: null, bottom: null, left: 208 },
},
] as const)("$why", ({ anchor, floating, side, align, rtl, expected }) => {
expect(
FloatingLayer.insets({
anchor,
floating,
bounds: BOUNDS,
side,
align,
gap: GAP,
padding: PADDING,
rtl,
viewport: VIEWPORT,
}),
).toEqual(expected);
});
});
describe("FloatingLayer.documentInsets", () => {
test.each([
{
why: "moves the near edges down the document by the scroll offset",
insets: { top: 148, right: null, bottom: null, left: 150 },
scroll: { x: 0, y: 300 },
expected: { top: 448, right: null, bottom: null, left: 150 },
},
{
why: "moves the far edges the other way",
insets: { top: null, right: 908, bottom: 708, left: null },
scroll: { x: 40, y: 300 },
expected: { top: null, right: 868, bottom: 408, left: null },
},
{
why: "leaves an unscrolled page as it is",
insets: { top: 148, right: null, bottom: null, left: 150 },
scroll: { x: 0, y: 0 },
expected: { top: 148, right: null, bottom: null, left: 150 },
},
])("$why", ({ insets, scroll, expected }) => {
expect(FloatingLayer.documentInsets(insets, scroll)).toEqual(expected);
});
});