mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Navigation menu in Preferences (#2304)
This commit is contained in:
1 parent
8e1583a59c
commit
5c373a886c
20 files changed
+838
-254
No files matched your search
@@ -0,0 +1,186 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { ScrollSpy } from "../scroll-spy";
|
||||
import { RecursiveTreeView } from "../../tree-view";
|
||||
|
||||
const observe = jest.fn();
|
||||
|
||||
Object.defineProperty(window, "IntersectionObserver", {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(() => ({
|
||||
observe,
|
||||
unobserve: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
describe("<ScrollSpy/>", () => {
|
||||
it("renders w/o errors", () => {
|
||||
const { container } = render(<ScrollSpy render={() => (
|
||||
<div>
|
||||
<section id="application">
|
||||
<h1>Application</h1>
|
||||
</section>
|
||||
</div>
|
||||
)}/>);
|
||||
|
||||
expect(container).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
it("calls intersection observer", () => {
|
||||
render(<ScrollSpy render={() => (
|
||||
<div>
|
||||
<section id="application">
|
||||
<h1>Application</h1>
|
||||
</section>
|
||||
</div>
|
||||
)}/>);
|
||||
|
||||
expect(observe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders dataTree component", async () => {
|
||||
const { queryByTestId } = render(<ScrollSpy render={dataTree => (
|
||||
<div>
|
||||
<nav>
|
||||
<RecursiveTreeView data={dataTree}/>
|
||||
</nav>
|
||||
<section id="application">
|
||||
<h1>Application</h1>
|
||||
</section>
|
||||
</div>
|
||||
)}/>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTestId("TreeView")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("throws if no sections founded", () => {
|
||||
// Prevent writing to stderr during this render.
|
||||
const err = console.error;
|
||||
|
||||
console.error = jest.fn();
|
||||
|
||||
expect(() => render(<ScrollSpy render={() => (
|
||||
<div>
|
||||
Content
|
||||
</div>
|
||||
)}/>)).toThrow();
|
||||
|
||||
// Restore writing to stderr.
|
||||
console.error = err;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("<TreeView/> dataTree inside <ScrollSpy/>", () => {
|
||||
it("contains links to all sections", async () => {
|
||||
const { queryByTitle } = render(<ScrollSpy render={dataTree => (
|
||||
<div>
|
||||
<nav>
|
||||
<RecursiveTreeView data={dataTree}/>
|
||||
</nav>
|
||||
<section id="application">
|
||||
<h1>Application</h1>
|
||||
<section id="appearance">
|
||||
<h2>Appearance</h2>
|
||||
</section>
|
||||
<section id="theme">
|
||||
<h2>Theme</h2>
|
||||
<div>description</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
)}/>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTitle("Application")).toBeInTheDocument();
|
||||
expect(queryByTitle("Appearance")).toBeInTheDocument();
|
||||
expect(queryByTitle("Theme")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("not showing links to sections without id", async () => {
|
||||
const { queryByTitle } = render(<ScrollSpy render={dataTree => (
|
||||
<div>
|
||||
<nav>
|
||||
<RecursiveTreeView data={dataTree}/>
|
||||
</nav>
|
||||
<section id="application">
|
||||
<h1>Application</h1>
|
||||
<section>
|
||||
<h2>Kubectl</h2>
|
||||
</section>
|
||||
<section id="appearance">
|
||||
<h2>Appearance</h2>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
)}/>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTitle("Application")).toBeInTheDocument();
|
||||
expect(queryByTitle("Appearance")).toBeInTheDocument();
|
||||
expect(queryByTitle("Kubectl")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("expands parent sections", async () => {
|
||||
const { queryByTitle } = render(<ScrollSpy render={dataTree => (
|
||||
<div>
|
||||
<nav>
|
||||
<RecursiveTreeView data={dataTree}/>
|
||||
</nav>
|
||||
<section id="application">
|
||||
<h1>Application</h1>
|
||||
<section id="appearance">
|
||||
<h2>Appearance</h2>
|
||||
</section>
|
||||
<section id="theme">
|
||||
<h2>Theme</h2>
|
||||
<div>description</div>
|
||||
</section>
|
||||
</section>
|
||||
<section id="Kubernetes">
|
||||
<h1>Kubernetes</h1>
|
||||
<section id="kubectl">
|
||||
<h2>Kubectl</h2>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
)}/>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTitle("Application")).toHaveAttribute("aria-expanded");
|
||||
expect(queryByTitle("Kubernetes")).toHaveAttribute("aria-expanded");
|
||||
});
|
||||
|
||||
// console.log(prettyDOM());
|
||||
});
|
||||
|
||||
it("skips sections without headings", async () => {
|
||||
const { queryByTitle } = render(<ScrollSpy render={dataTree => (
|
||||
<div>
|
||||
<nav>
|
||||
<RecursiveTreeView data={dataTree}/>
|
||||
</nav>
|
||||
<section id="application">
|
||||
<h1>Application</h1>
|
||||
<section id="appearance">
|
||||
<p>Appearance</p>
|
||||
</section>
|
||||
<section id="theme">
|
||||
<h2>Theme</h2>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
)}/>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByTitle("Application")).toBeInTheDocument();
|
||||
expect(queryByTitle("appearance")).not.toBeInTheDocument();
|
||||
expect(queryByTitle("Appearance")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { observer } from "mobx-react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useMutationObserver } from "../../hooks";
|
||||
import { NavigationTree } from "../tree-view";
|
||||
|
||||
interface Props extends React.DOMAttributes<HTMLElement> {
|
||||
render: (data: NavigationTree[]) => JSX.Element
|
||||
htmlFor?: string // Id of the element to put observers on
|
||||
rootMargin?: string // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#creating_an_intersection_observer
|
||||
}
|
||||
|
||||
export const ScrollSpy = observer(({ render, htmlFor, rootMargin = "0px 0px -100% 0px" }: Props) => {
|
||||
const parent = useRef<HTMLDivElement>();
|
||||
const sections = useRef<NodeListOf<HTMLElement>>();
|
||||
const [tree, setTree] = useState<NavigationTree[]>([]);
|
||||
const [activeElementId, setActiveElementId] = useState("");
|
||||
|
||||
const setSections = () => {
|
||||
sections.current = parent.current.querySelectorAll("section");
|
||||
|
||||
if (!sections.current.length) {
|
||||
throw new Error("No <section/> tag founded! Content should be placed inside <section></section> elements to activate navigation.");
|
||||
}
|
||||
};
|
||||
|
||||
const getSectionsParentElement = () => {
|
||||
return sections.current?.[0].parentElement;
|
||||
};
|
||||
|
||||
const updateNavigation = () => {
|
||||
setTree(getNavigation(getSectionsParentElement()));
|
||||
};
|
||||
|
||||
const getNavigation = (element: Element) => {
|
||||
const sections = element.querySelectorAll(":scope > section"); // Searching only direct children of an element. Impossible without :scope
|
||||
const children: NavigationTree[] = [];
|
||||
|
||||
sections.forEach(section => {
|
||||
const id = section.getAttribute("id");
|
||||
const parentId = section.parentElement.id;
|
||||
const name = section.querySelector("h1, h2, h3, h4, h5, h6")?.textContent;
|
||||
const selected = id === activeElementId;
|
||||
|
||||
if (!name || !id) {
|
||||
return;
|
||||
}
|
||||
|
||||
children.push({
|
||||
id,
|
||||
parentId,
|
||||
name,
|
||||
selected,
|
||||
children: getNavigation(section)
|
||||
});
|
||||
});
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
const handleIntersect = ([entry]: IntersectionObserverEntry[]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveElementId(entry.target.closest("section[id]").id);
|
||||
}
|
||||
};
|
||||
|
||||
const observeSections = () => {
|
||||
const options: IntersectionObserverInit = {
|
||||
root: document.getElementById(htmlFor) || getSectionsParentElement(),
|
||||
rootMargin
|
||||
};
|
||||
|
||||
sections.current.forEach((section) => {
|
||||
const observer = new IntersectionObserver(handleIntersect, options);
|
||||
const target = section.querySelector("section") || section;
|
||||
|
||||
observer.observe(target);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSections();
|
||||
observeSections();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
updateNavigation();
|
||||
}, [activeElementId]);
|
||||
|
||||
useMutationObserver(getSectionsParentElement(), updateNavigation);
|
||||
|
||||
return (
|
||||
<div className="ScrollSpy" ref={parent}>
|
||||
{render(tree)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user