Merge sidebar stability fix into dnd kit
This commit is contained in:
4 files changed
+51
-67
No files matched your search
@@ -134,8 +134,8 @@ For example, a 12-column layout with a two-column left rail leaves ten columns f
|
||||
Enabling or widening a rail reflows canvas items into the remaining columns.
|
||||
Disabling a rail moves its contents back into the next available canvas positions.
|
||||
|
||||
Rails remain visible while the page scrolls.
|
||||
Apps, widgets, and Containers inside them still use the same fixed `200 × 200` cells and can be rearranged in edit mode.
|
||||
Rails fill the available window height and remain fixed while the page scrolls.
|
||||
Apps, widgets, and Containers inside them still use the same fixed `200 × 200` cells and can be rearranged in edit mode, where each rail shows its name and boundary.
|
||||
|
||||
#### Breakpoint
|
||||
|
||||
|
||||
@@ -9,14 +9,46 @@
|
||||
}
|
||||
|
||||
.gutter {
|
||||
position: relative;
|
||||
top: 0;
|
||||
--board-gutter-sticky-top: calc((var(--app-shell-header-offset, 0px) + 1rem) * var(--board-canvas-inverse-scale, 1));
|
||||
--board-gutter-viewport-height: calc(
|
||||
(100dvh - var(--app-shell-header-offset, 0px) - 1rem) * var(--board-canvas-inverse-scale, 1)
|
||||
);
|
||||
|
||||
position: sticky;
|
||||
top: var(--board-gutter-sticky-top);
|
||||
align-self: start;
|
||||
box-sizing: border-box;
|
||||
height: var(--board-gutter-viewport-height);
|
||||
padding-top: 24px;
|
||||
margin-top: -24px;
|
||||
max-height: calc(100dvh - var(--app-shell-header-offset, 0px) - var(--mantine-spacing-md));
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-gutter: stable;
|
||||
transform: translateY(var(--board-gutter-sticky-offset, 0px));
|
||||
}
|
||||
|
||||
.gutter[data-board-editing="true"] {
|
||||
outline: 1px dashed var(--mantine-color-default-border);
|
||||
outline-offset: -1px;
|
||||
border-radius: var(--mantine-radius-md);
|
||||
}
|
||||
|
||||
.gutter[data-board-editing="true"]::before {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: block;
|
||||
width: fit-content;
|
||||
height: 24px;
|
||||
padding-inline: var(--mantine-spacing-xs);
|
||||
margin-top: -24px;
|
||||
overflow: hidden;
|
||||
font-size: var(--mantine-font-size-xs);
|
||||
line-height: 24px;
|
||||
color: var(--mantine-color-dimmed);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
content: attr(aria-label);
|
||||
background: color-mix(in srgb, var(--mantine-color-body) 88%, transparent);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box } from "@mantine/core";
|
||||
|
||||
import { useCurrentLayout, useInitialViewportWidth, useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import {
|
||||
@@ -23,66 +23,12 @@ import classes from "./_client.module.css";
|
||||
|
||||
const APP_SHELL_INLINE_PADDING = 32;
|
||||
|
||||
const useFixedBoardGutters = () => {
|
||||
const [columns, setColumns] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!columns) return;
|
||||
|
||||
let frame = 0;
|
||||
const update = () => {
|
||||
frame = 0;
|
||||
const columnsRect = columns.getBoundingClientRect();
|
||||
const headerBottom = document.querySelector("[data-app-shell-header]")?.getBoundingClientRect().bottom ?? 0;
|
||||
const stickyTop = Math.max(0, headerBottom + 16);
|
||||
|
||||
for (const gutter of columns.querySelectorAll<HTMLElement>("[data-board-gutter]")) {
|
||||
const rect = gutter.getBoundingClientRect();
|
||||
if (gutter.offsetWidth <= 0) continue;
|
||||
|
||||
const effectiveScale = rect.width / gutter.offsetWidth;
|
||||
if (!Number.isFinite(effectiveScale) || effectiveScale <= 0) continue;
|
||||
|
||||
const previousOffset = Number(gutter.dataset.stickyOffset ?? 0);
|
||||
const uncorrectedTop = rect.top - previousOffset * effectiveScale;
|
||||
const maximumTop = columnsRect.bottom - rect.height;
|
||||
const targetTop = Math.min(stickyTop, maximumTop);
|
||||
const nextOffset = Math.max(0, targetTop - uncorrectedTop) / effectiveScale;
|
||||
|
||||
gutter.dataset.stickyOffset = String(nextOffset);
|
||||
gutter.style.setProperty("--board-gutter-sticky-offset", `${nextOffset}px`);
|
||||
}
|
||||
};
|
||||
const scheduleUpdate = () => {
|
||||
if (frame !== 0) return;
|
||||
frame = window.requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(scheduleUpdate);
|
||||
resizeObserver.observe(columns);
|
||||
document.addEventListener("scroll", scheduleUpdate, { capture: true, passive: true });
|
||||
window.addEventListener("scroll", scheduleUpdate, { passive: true });
|
||||
window.addEventListener("resize", scheduleUpdate, { passive: true });
|
||||
scheduleUpdate();
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(frame);
|
||||
resizeObserver.disconnect();
|
||||
document.removeEventListener("scroll", scheduleUpdate, { capture: true });
|
||||
window.removeEventListener("scroll", scheduleUpdate);
|
||||
window.removeEventListener("resize", scheduleUpdate);
|
||||
};
|
||||
}, [columns]);
|
||||
|
||||
return setColumns;
|
||||
};
|
||||
|
||||
export const ClientBoard = () => {
|
||||
const board = useRequiredBoard();
|
||||
const t = useScopedI18n("board.landmark");
|
||||
const [isEditMode] = useEditMode();
|
||||
const currentLayoutId = useCurrentLayout();
|
||||
const initialViewportWidth = useInitialViewportWidth();
|
||||
const columnsRef = useFixedBoardGutters();
|
||||
const currentLayout = board.layouts.find((layout) => layout.id === currentLayoutId) ?? board.layouts.at(0);
|
||||
if (!currentLayout) throw new Error("Expected the board to contain a layout");
|
||||
|
||||
@@ -122,16 +68,13 @@ export const ClientBoard = () => {
|
||||
>
|
||||
<BoardGridEditorBoundary key={currentLayoutId}>
|
||||
<BoardGridPortalRenderer />
|
||||
<div
|
||||
ref={columnsRef}
|
||||
className={classes.columns}
|
||||
style={{ gridTemplateColumns, paddingTop: LOGICAL_GRID_GAP }}
|
||||
>
|
||||
<div className={classes.columns} style={{ gridTemplateColumns, paddingTop: LOGICAL_GRID_GAP }}>
|
||||
{leftColumnCount > 0 && leftSection && (
|
||||
<aside
|
||||
className={`${classes.lane} ${classes.gutter}`}
|
||||
aria-label={t("leftRail")}
|
||||
data-board-gutter="left"
|
||||
data-board-editing={isEditMode ? "true" : undefined}
|
||||
>
|
||||
<BoardEmptySection
|
||||
key={`${currentLayoutId}-${leftSection.id}`}
|
||||
@@ -157,6 +100,7 @@ export const ClientBoard = () => {
|
||||
className={`${classes.lane} ${classes.gutter}`}
|
||||
aria-label={t("rightRail")}
|
||||
data-board-gutter="right"
|
||||
data-board-editing={isEditMode ? "true" : undefined}
|
||||
>
|
||||
<BoardEmptySection
|
||||
key={`${currentLayoutId}-${rightSection.id}`}
|
||||
|
||||
@@ -245,6 +245,14 @@ describe("Board grid", () => {
|
||||
await expect(page.locator(`[data-grid-runtime="${gridRuntimeMarker}"]`)).toHaveCount(3);
|
||||
await expect(page.getByTestId("board-grid-editor-loading")).toHaveCount(0);
|
||||
await expect(page.getByTestId("board-canvas-row-count-button")).toHaveCount(0);
|
||||
await expect(rail).toHaveAttribute("data-board-editing", "true");
|
||||
await expect(rail).toHaveCSS("position", "sticky");
|
||||
await expect(rail).toHaveCSS("transform", "none");
|
||||
const railBox = await expectBoundingBoxAsync(rail);
|
||||
expect(railBox.height + railBox.y).toBeCloseTo(page.viewportSize()?.height ?? 0, 0);
|
||||
expect(await rail.evaluate((element) => getComputedStyle(element, "::before").content.replaceAll('"', ""))).toBe(
|
||||
"Left dashboard rail",
|
||||
);
|
||||
await expect(logicalTile).toHaveCSS("overflow-x", "hidden");
|
||||
await expect(logicalTile).toHaveCSS("overflow-y", "auto");
|
||||
await logicalTile.evaluate((element) => {
|
||||
|
||||
Reference in new issue
Block a user