feat(header): add opt-in auto-hide-on-scroll for the header (#6738)
* feat(header): add opt-in auto-hide-on-scroll for the header New per-user header preference (default off): when enabled, the header slides away while scrolling down and reappears on scroll-up or near the top of the page. Disabled while in board edit mode, and skipped entirely while focus is inside the header. Only applies when the header is set to visible - the existing hover-to-reveal floating controls already cover the hidden-header case. * chore(api): regenerate the checked-in OpenAPI schema check:openapi was failing in CI - the schema on disk hadn't been regenerated after the header preferences gained autoHideOnScroll. Add scripts/generate-openapi-schema.mts (and a generate:openapi package script) alongside the existing check script so this is a one-command fix next time the schema drifts. * docs(users): document the header studio's visibility and auto-hide options Header customization (drag-to-arrange controls, hide/show, hover-reveal and pin, auto-hide-on-scroll) had no documentation on the user general settings page.
This commit is contained in:
1 parent
ee08beef59
commit
e58c229664
11 files changed
+107
-2
No files matched your search
@@ -1,11 +1,16 @@
|
||||
.header {
|
||||
overflow: hidden;
|
||||
transition: transform 220ms ease;
|
||||
}
|
||||
|
||||
.header[data-visible="false"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header[data-auto-hidden="true"]:not(:focus-within) {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.desktopContent {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -176,6 +181,7 @@
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.header,
|
||||
.floatingControls {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IconBrandDocker, IconHome, IconReplace, IconRobot, IconSettings, IconSu
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useOptionalEditMode } from "@homarr/boards/edit-mode";
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
@@ -24,6 +25,7 @@ import { HeaderLogo } from "./header-logo";
|
||||
import { LazySpotlight } from "./lazy-spotlight";
|
||||
import { DesktopSearchInput, MobileSearchButton } from "./search";
|
||||
import { TourTarget } from "./tour-target";
|
||||
import { useHeaderAutoHide } from "./use-header-auto-hide";
|
||||
import { UserButtonClient } from "./user-client";
|
||||
import classes from "./configurable-header.module.css";
|
||||
|
||||
@@ -69,6 +71,10 @@ export const ConfigurableHeader = ({
|
||||
staleTime: 60_000,
|
||||
});
|
||||
const boardsById = useMemo(() => new Map(boards.map((board) => [board.id, board])), [boards]);
|
||||
const isEditMode = useOptionalEditMode();
|
||||
const isAutoHidden = useHeaderAutoHide(
|
||||
headerPreferences.visible && headerPreferences.autoHideOnScroll && !isEditMode,
|
||||
);
|
||||
|
||||
return (
|
||||
<BoardSwitcher>
|
||||
@@ -114,6 +120,7 @@ export const ConfigurableHeader = ({
|
||||
zIndex="var(--homarr-z-index-board-header)"
|
||||
className={classes.header}
|
||||
data-visible={headerPreferences.visible}
|
||||
data-auto-hidden={isAutoHidden || undefined}
|
||||
data-advanced-focus-background
|
||||
data-app-shell-header
|
||||
>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const SCROLL_HIDE_THRESHOLD = 24;
|
||||
const SCROLL_DIRECTION_HYSTERESIS = 6;
|
||||
|
||||
export const useHeaderAutoHide = (enabled: boolean) => {
|
||||
const [isHidden, setIsHidden] = useState(false);
|
||||
const lastScrollYRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
setIsHidden(false);
|
||||
return;
|
||||
}
|
||||
|
||||
lastScrollYRef.current = window.scrollY;
|
||||
let frame: number | null = null;
|
||||
|
||||
const handleScroll = () => {
|
||||
if (frame !== null) return;
|
||||
frame = window.requestAnimationFrame(() => {
|
||||
frame = null;
|
||||
const currentY = window.scrollY;
|
||||
const delta = currentY - lastScrollYRef.current;
|
||||
|
||||
if (currentY <= SCROLL_HIDE_THRESHOLD) {
|
||||
setIsHidden(false);
|
||||
} else if (delta > SCROLL_DIRECTION_HYSTERESIS) {
|
||||
setIsHidden(true);
|
||||
} else if (delta < -SCROLL_DIRECTION_HYSTERESIS) {
|
||||
setIsHidden(false);
|
||||
}
|
||||
|
||||
lastScrollYRef.current = currentY;
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
if (frame !== null) window.cancelAnimationFrame(frame);
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
return enabled && isHidden;
|
||||
};
|
||||
Reference in new issue
Block a user