fix: address board grid review
This commit is contained in:
1 parent
cddae6719e
commit
28ac23e66b
51 files changed
+647
-278
No files matched your search
@@ -92,6 +92,8 @@ describe("fixed grid item behavior", () => {
|
||||
mocks.editMode = true;
|
||||
mocks.maxRowCount = null;
|
||||
getWeatherMock().advancedOptions.title = null;
|
||||
getClockMock().xOffset = 1;
|
||||
getClockMock().yOffset = 0;
|
||||
container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
root = createRoot(container);
|
||||
@@ -200,6 +202,21 @@ describe("fixed grid item behavior", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("stops keyboard editing when the rendered grid state is invalid", () => {
|
||||
getClockMock().xOffset = 0;
|
||||
renderWeather(root);
|
||||
|
||||
const entry = getEditorEntry(container);
|
||||
startKeyboardEditing(entry);
|
||||
act(() => entry.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true })));
|
||||
|
||||
expect(entry.dataset.keyboardEditing).toBe("false");
|
||||
expect(mocks.commitSectionGrid).not.toHaveBeenCalled();
|
||||
expect(mocks.announce).toHaveBeenLastCalledWith(
|
||||
"widget.weather.name, column 1, row 1: item.moveResize.keyboard.stopped",
|
||||
);
|
||||
});
|
||||
|
||||
test("uses the whole tile as the focusable move surface with a visual drag affordance", () => {
|
||||
getWeatherMock().advancedOptions.title = " Balcony forecast ";
|
||||
renderWeather(root);
|
||||
@@ -286,6 +303,12 @@ const getWeatherMock = () => {
|
||||
return item;
|
||||
};
|
||||
|
||||
const getClockMock = () => {
|
||||
const item = mocks.items[1];
|
||||
if (!item) throw new Error("Expected the clock item mock to exist");
|
||||
return item;
|
||||
};
|
||||
|
||||
const getEditorEntry = (container: HTMLElement) => {
|
||||
const entry = container.querySelector<HTMLDivElement>("[data-editor-grid-entry]");
|
||||
if (!entry) throw new Error("Expected the fixed grid item to render an editor entry");
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { getGridDepth } from "../grid-depth";
|
||||
|
||||
describe("grid depth", () => {
|
||||
test("follows the registered parent chain for deeply nested grids", () => {
|
||||
const parents = new Map<string, string | null>([
|
||||
["root", null],
|
||||
["first", "root"],
|
||||
["second", "first"],
|
||||
["third", "second"],
|
||||
]);
|
||||
|
||||
expect(getGridDepth("root", parents)).toBe(0);
|
||||
expect(getGridDepth("third", parents)).toBe(3);
|
||||
});
|
||||
|
||||
test("stops safely when malformed parent data contains a cycle", () => {
|
||||
const parents = new Map<string, string | null>([
|
||||
["first", "second"],
|
||||
["second", "first"],
|
||||
]);
|
||||
|
||||
expect(getGridDepth("first", parents)).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ describe("board grid import boundary", () => {
|
||||
test("keeps every current dnd-kit runtime import inside the edit-only module", () => {
|
||||
const runtimeImports = getSourceFiles(nextjsSourceDirectory)
|
||||
.flatMap(getRuntimeImports)
|
||||
.filter(({ specifier }) => CURRENT_DND_KIT_PACKAGES.has(specifier))
|
||||
.filter(({ specifier }) => isCurrentDndKitImport(specifier))
|
||||
.map(({ filePath, kind, specifier }) => ({
|
||||
file: toSourceRelativePath(filePath),
|
||||
kind,
|
||||
@@ -40,6 +40,11 @@ describe("board grid import boundary", () => {
|
||||
kind: "static",
|
||||
specifier: "@dnd-kit/dom",
|
||||
},
|
||||
{
|
||||
file: "components/board/sections/grid/grid-editor.tsx",
|
||||
kind: "static",
|
||||
specifier: "@dnd-kit/dom/utilities",
|
||||
},
|
||||
{
|
||||
file: "components/board/sections/grid/grid-editor.tsx",
|
||||
kind: "static",
|
||||
@@ -104,7 +109,10 @@ describe("board grid import boundary", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const CURRENT_DND_KIT_PACKAGES = new Set(["@dnd-kit/abstract", "@dnd-kit/collision", "@dnd-kit/dom", "@dnd-kit/react"]);
|
||||
const CURRENT_DND_KIT_PACKAGES = ["@dnd-kit/abstract", "@dnd-kit/collision", "@dnd-kit/dom", "@dnd-kit/react"];
|
||||
|
||||
const isCurrentDndKitImport = (specifier: string) =>
|
||||
CURRENT_DND_KIT_PACKAGES.some((packageName) => specifier === packageName || specifier.startsWith(`${packageName}/`));
|
||||
|
||||
interface RuntimeImport {
|
||||
filePath: string;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { act, createElement, useLayoutEffect } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import type { Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { BoardGridPortalHost, useBoardGridPortalHost } from "../grid-portal-host";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
board: {
|
||||
items: [{ id: "weather", layouts: [{ layoutId: "desktop", sectionId: "root" }] }],
|
||||
sections: [],
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/auth/client", () => ({
|
||||
useIntegrations: () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/boards/context", () => ({
|
||||
useCurrentLayout: () => "desktop",
|
||||
useRequiredBoard: () => mocks.board,
|
||||
}));
|
||||
|
||||
describe("board grid portal host", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.board = {
|
||||
items: [{ id: "weather", layouts: [{ layoutId: "desktop", sectionId: "root" }] }],
|
||||
sections: [],
|
||||
};
|
||||
container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
});
|
||||
|
||||
test("keeps a portal across relocation and prunes it after permanent deletion", () => {
|
||||
const acquired: HTMLElement[] = [];
|
||||
|
||||
renderHost(root, true, 0, acquired);
|
||||
expect(getContainerCount(container)).toBe("1");
|
||||
|
||||
mocks.board = {
|
||||
items: [{ id: "weather", layouts: [{ layoutId: "desktop", sectionId: "other" }] }],
|
||||
sections: [],
|
||||
};
|
||||
renderHost(root, true, 1, acquired);
|
||||
expect(getContainerCount(container)).toBe("1");
|
||||
expect(new Set(acquired).size).toBe(1);
|
||||
|
||||
mocks.board = { items: [], sections: [] };
|
||||
renderHost(root, false, 2, acquired);
|
||||
expect(getContainerCount(container)).toBe("0");
|
||||
});
|
||||
});
|
||||
|
||||
const renderHost = (root: Root, acquire: boolean, revision: number, acquired: HTMLElement[]) => {
|
||||
act(() => {
|
||||
root.render(
|
||||
createElement(BoardGridPortalHost, null, createElement(PortalRegistryProbe, { acquire, revision, acquired })),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const PortalRegistryProbe = ({
|
||||
acquire,
|
||||
revision,
|
||||
acquired,
|
||||
}: {
|
||||
acquire: boolean;
|
||||
revision: number;
|
||||
acquired: HTMLElement[];
|
||||
}) => {
|
||||
const portalHost = useBoardGridPortalHost();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (acquire) acquired.push(portalHost.acquireContainer("weather"));
|
||||
}, [acquire, acquired, portalHost, revision]);
|
||||
|
||||
return createElement("span", { "data-container-count": String(portalHost.containers.size) });
|
||||
};
|
||||
|
||||
const getContainerCount = (container: HTMLElement) =>
|
||||
container.querySelector<HTMLElement>("[data-container-count]")?.dataset.containerCount;
|
||||
Reference in new issue
Block a user