Fix library anchor navigation and scroll cleanup
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s

This commit is contained in:
Shinuwa 2026-09-12 08:54:09 +02:00
parent 99d916f6db
commit 75d4d99a87
4 changed files with 88 additions and 22 deletions

View file

@ -1,5 +1,5 @@
// Rôle : affiche la librairie des outils avec des exemples locaux non persistés.
import { Suspense, useEffect, useMemo, useRef, useState } from "react";
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Icon } from "../components/Icon.jsx";
import { LIBRARY_CATEGORY_DEFINITIONS, getToolboxModuleDefinition } from "../features/toolboxes/toolboxModuleRegistry.js";
import { MODULE_COMPONENTS } from "../features/toolboxes/modules/toolboxModuleComponents.jsx";
@ -62,7 +62,11 @@ function getLibraryCategoryAnchorId(category) {
function getCurrentLibraryAnchor() {
const match = location.hash.match(/^#\/library#(.+)$/);
return match ? decodeURIComponent(match[1]) : "";
try {
return match ? decodeURIComponent(match[1]) : "";
} catch {
return "";
}
}
function getLibraryAnchorOffset() {
@ -71,17 +75,20 @@ function getLibraryAnchorOffset() {
return topbarHeight + 16;
}
function scrollToLibraryTool(anchorId, replace = false, behavior = "smooth") {
if (!anchorId) return;
if (replace) history.replaceState(null, "", `#/library#${encodeURIComponent(anchorId)}`);
LIBRARY_ANCHOR_RETRY_DELAYS.forEach((delay, index) => {
window.setTimeout(() => {
function scrollToLibraryTool(anchorId, behavior) {
let timers = [];
const frame = requestAnimationFrame(() => {
timers = LIBRARY_ANCHOR_RETRY_DELAYS.map((delay, index) => window.setTimeout(() => {
const element = document.getElementById(anchorId);
if (!element) return;
const top = element.getBoundingClientRect().top + window.scrollY - getLibraryAnchorOffset();
window.scrollTo({ top: Math.max(0, top), behavior: index === 0 ? behavior : "auto" });
}, delay);
}, delay));
});
return () => {
cancelAnimationFrame(frame);
timers.forEach((timer) => window.clearTimeout(timer));
};
}
function findCategoryForAnchor(categories, anchorId) {
@ -114,9 +121,13 @@ export function LibraryPage({ siteContent, actions }) {
const [libraryPayload, setLibraryPayload] = useState(null);
const [libraryError, setLibraryError] = useState("");
const [moduleData, setModuleData] = useState({});
const [activeCategoryKey, setActiveCategoryKey] = useState("");
const [navigation, setNavigation] = useState(() => ({ anchorId: getCurrentLibraryAnchor(), behavior: "auto" }));
const libraryCategories = useMemo(() => getLibraryCategories(libraryPayload?.toolbox?.modules || [], content), [content, libraryPayload]);
const activeCategory = libraryCategories.find((category) => category.key === activeCategoryKey) || libraryCategories[0] || null;
const activeCategory = findCategoryForAnchor(libraryCategories, navigation.anchorId) || libraryCategories[0] || null;
const selectAnchor = useCallback((anchorId) => {
history.replaceState(null, "", `#/library#${encodeURIComponent(anchorId)}`);
setNavigation({ anchorId, behavior: "smooth" });
}, []);
useEffect(() => {
let cancelled = false;
@ -137,13 +148,15 @@ export function LibraryPage({ siteContent, actions }) {
}, []);
useEffect(() => {
if (!libraryPayload || !libraryCategories.length) return;
const anchorId = getCurrentLibraryAnchor();
const category = findCategoryForAnchor(libraryCategories, anchorId) || libraryCategories[0];
setActiveCategoryKey(category.key);
if (!anchorId) return;
requestAnimationFrame(() => scrollToLibraryTool(anchorId, false, "auto"));
}, [libraryPayload, libraryCategories]);
const onHashChange = () => setNavigation({ anchorId: getCurrentLibraryAnchor(), behavior: "auto" });
window.addEventListener("hashchange", onHashChange);
return () => window.removeEventListener("hashchange", onHashChange);
}, []);
useEffect(() => {
if (!activeCategory?.key || !navigation.anchorId) return undefined;
return scrollToLibraryTool(navigation.anchorId, navigation.behavior);
}, [activeCategory?.key, navigation]);
const moduleContext = useMemo(() => ({
getModuleData: (toolboxId, moduleId, fallback) => moduleData[`${toolboxId}:${moduleId}`] || fallback,
@ -199,9 +212,9 @@ export function LibraryPage({ siteContent, actions }) {
...current,
[`${LIBRARY_TOOLBOX_ID}:${annotationModule.id}`]: { image: dataUrl, markers: [], drawings: { strokes: [] } }
}));
requestAnimationFrame(() => scrollToLibraryTool(getLibraryAnchorId(annotationModule), true));
selectAnchor(getLibraryAnchorId(annotationModule));
}
}), [actions, libraryPayload, moduleData, siteContent.toolboxes.modules]);
}), [actions, libraryPayload, moduleData, selectAnchor, siteContent.toolboxes.modules]);
return (
<div className="library-page">
@ -235,8 +248,7 @@ export function LibraryPage({ siteContent, actions }) {
categories={libraryCategories}
activeCategoryKey={activeCategory?.key || ""}
onSelectCategory={(category, anchorId = getLibraryCategoryAnchorId(category)) => {
setActiveCategoryKey(category.key);
requestAnimationFrame(() => scrollToLibraryTool(anchorId, true));
selectAnchor(anchorId);
}}
/>
<section className="library-tools" aria-label={content.toolsLabel}>