Skip to content

Commit

Permalink
DEVPROD-11363: Support scrolling to different sections on Build Infor…
Browse files Browse the repository at this point in the history
…mation page (#425)
  • Loading branch information
minnakt authored Oct 21, 2024
1 parent 379e8c2 commit e75bd0c
Show file tree
Hide file tree
Showing 17 changed files with 339 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,4 @@ describe("Github username banner", () => {
cy.visit("/");
cy.dataCy("github-username-banner").should("exist");
});
it("should not show the banner on other pages, even if user doesn't have a github username", () => {
cy.visit("/hosts");
cy.dataCy("github-username-banner").should("not.exist");
});
});
26 changes: 26 additions & 0 deletions apps/spruce/cypress/integration/image/build_information.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,29 @@ describe("build information", () => {
});
});
});

describe("side nav", () => {
beforeEach(() => {
cy.visit("/image/ubuntu2204/build-information");
cy.contains("ubuntu2204").should("be.visible");
cy.dataCy("table-loader-loading-row").should("not.exist");
});

it("highlights different sections as the user scrolls", () => {
cy.dataCy("general-card").scrollIntoView();
cy.dataCy("navitem-general").should("have.attr", "data-active", "true");
cy.dataCy("navitem-distros").should("have.attr", "data-active", "false");

cy.dataCy("distros-card").scrollIntoView();
cy.dataCy("navitem-general").should("have.attr", "data-active", "false");
cy.dataCy("navitem-distros").should("have.attr", "data-active", "true");
});

it("can click to navigate to different sections", () => {
cy.dataCy("navitem-packages").should("have.attr", "data-active", "false");
cy.dataCy("packages-card").should("not.be.visible");
cy.dataCy("navitem-packages").click();
cy.dataCy("navitem-packages").should("have.attr", "data-active", "true");
cy.dataCy("packages-card").should("be.visible");
});
});
4 changes: 4 additions & 0 deletions apps/spruce/cypress/integration/image/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ describe("/image/imageId/random redirect route", () => {
cy.location("pathname").should("not.contain", "/random");
cy.location("pathname").should("eq", "/image/imageId/build-information");
});
});

describe("image dropdown", () => {
it("navigates to the image when clicked", () => {
cy.visit("/image/amazon2/build-information");
cy.dataCy("images-select").should("be.visible").as("button");
Expand All @@ -24,7 +26,9 @@ describe("/image/imageId/random redirect route", () => {
});
});
});
});

describe("task metadata", () => {
it("shows the image visibility guide cue on task metadata", () => {
cy.setCookie(SEEN_IMAGE_VISIBILITY_GUIDE_CUE, "false");
cy.visit(
Expand Down
5 changes: 5 additions & 0 deletions apps/spruce/src/analytics/image/useImageAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ type Action =
name: "Changed tab";
tab: ImageTabRoutes;
}
| {
name: "Clicked section";
tab: ImageTabRoutes;
"tab.section": string;
}
| {
name: "Clicked 'Load more events' button";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface LoadingRowProps {
numColumns: number;
}
const LoadingRow: React.FC<LoadingRowProps> = ({ numColumns }) => (
<Row>
<Row data-cy="table-loader-loading-row">
{Array.from({ length: numColumns }, (_, i) => (
<Cell key={i}>
<Skeleton size="small" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@
<tbody>
<tr
class=""
data-cy="table-loader-loading-row"
data-lgid="lg-table-row"
>
<td
Expand Down Expand Up @@ -279,6 +280,7 @@
</tr>
<tr
class=""
data-cy="table-loader-loading-row"
data-lgid="lg-table-row"
>
<td
Expand Down Expand Up @@ -323,6 +325,7 @@
</tr>
<tr
class=""
data-cy="table-loader-loading-row"
data-lgid="lg-table-row"
>
<td
Expand Down Expand Up @@ -367,6 +370,7 @@
</tr>
<tr
class=""
data-cy="table-loader-loading-row"
data-lgid="lg-table-row"
>
<td
Expand Down Expand Up @@ -411,6 +415,7 @@
</tr>
<tr
class=""
data-cy="table-loader-loading-row"
data-lgid="lg-table-row"
>
<td
Expand Down
8 changes: 6 additions & 2 deletions apps/spruce/src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,12 @@ export const getProjectPatchesRoute = (projectIdentifier: string) =>
PageNames.Patches
}`;

export const getImageRoute = (imageId: string, tab?: ImageTabRoutes) =>
`${paths.image}/${imageId}/${tab || ImageTabRoutes.BuildInformation}`;
export const getImageRoute = (
imageId: string,
tab?: ImageTabRoutes,
anchor?: string,
) =>
`${paths.image}/${imageId}/${tab ?? ImageTabRoutes.BuildInformation}${anchor ? `#${anchor}` : ""}`;

export const getProjectSettingsRoute = (
projectId: string,
Expand Down
1 change: 1 addition & 0 deletions apps/spruce/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export { useFirstImage } from "./useFirstImage";
export { useBreadcrumbRoot } from "./useBreadcrumbRoot";
export { useResize } from "./useResize";
export { useRunningTime } from "./useRunningTime";
export { useTopmostVisibleElement } from "./useTopmostVisibleElement";
4 changes: 3 additions & 1 deletion apps/spruce/src/hooks/useScrollToAnchor/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useEffect, useRef } from "react";
import { useLocation } from "react-router-dom";

export const anchorScrollTime = 100;

/**
* `useScrollToAnchor` scrolls to an anchor element on the page if the URL contains an anchor.
*/
Expand All @@ -18,7 +20,7 @@ const useScrollToAnchor = () => {
if (element) {
element.scrollIntoView({ behavior: "smooth" });
}
}, 500);
}, anchorScrollTime);
}, [anchor]);

useEffect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe("useScrollToAnchor", () => {
);

renderHook(() => useScrollToAnchor(), { wrapper });
vi.advanceTimersByTime(500);
vi.runOnlyPendingTimers();

expect(document.getElementById).toHaveBeenCalledWith("test-anchor");
expect(mockElement.scrollIntoView).toHaveBeenCalledWith({
Expand All @@ -40,7 +40,7 @@ describe("useScrollToAnchor", () => {
);

renderHook(() => useScrollToAnchor(), { wrapper });
vi.advanceTimersByTime(500);
vi.runOnlyPendingTimers();

expect(document.getElementById).not.toHaveBeenCalled();
});
Expand All @@ -53,7 +53,7 @@ describe("useScrollToAnchor", () => {

const { unmount } = renderHook(() => useScrollToAnchor(), { wrapper });
unmount();
vi.advanceTimersByTime(500);
vi.runOnlyPendingTimers();

expect(mockElement.scrollIntoView).not.toHaveBeenCalled();
});
Expand Down
63 changes: 63 additions & 0 deletions apps/spruce/src/hooks/useTopmostVisibleElement/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect, useState } from "react";

const findTopmostVisibleElement = ({
elements,
scrollTop,
}: {
elements: HTMLElement[];
scrollTop: number;
}) => {
let minDistance = Number.MAX_VALUE;
let minDistanceId = "";

elements.forEach((el) => {
const currDistance = Math.abs(scrollTop - el.offsetTop);
if (currDistance < minDistance) {
minDistance = currDistance;
minDistanceId = el.id;
}
});

return minDistanceId;
};

/**
* `useTopmostVisibleElement` is used to track the ID of the element that is visible on the screen and closest
* to the top of the element with scrollContainerId.
* @param obj - object representing arguments to `useTopmostVisibleElement` hook
* @param obj.elements - list of elements from which to determine the topmost visible element
* @param obj.scrollContainerId - the ID of the scroll container. Referencing the scroll container is necessary
* because the hook listens on the "scroll" event.
* @returns the ID of the topmost visible element (string)
*/
export const useTopmostVisibleElement = ({
elements,
scrollContainerId,
}: {
elements: HTMLElement[];
scrollContainerId: string;
}) => {
const [topmostVisibleElementId, setTopmostVisibleElementId] = useState("");

useEffect(() => {
const scrollElement = document.getElementById(
scrollContainerId,
) as HTMLElement;

const handleScroll = () => {
const id = findTopmostVisibleElement({
scrollTop: scrollElement.scrollTop,
elements,
});
setTopmostVisibleElementId(id);
};

scrollElement.addEventListener("scroll", handleScroll);
return () => {
scrollElement.removeEventListener("scroll", handleScroll);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [elements]);

return topmostVisibleElementId;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { act, renderHook, render } from "@evg-ui/lib/test_utils";
import { useTopmostVisibleElement } from ".";

describe("useTopmostVisibleElement", () => {
const scrollPage = (scrollContainerId: string, height: number) => {
act(() => {
const scrollElement = document.getElementById(
scrollContainerId,
) as HTMLElement;
scrollElement.scrollTop = height;
scrollElement.dispatchEvent(new window.Event("scroll"));
});
};

it("should correctly detect the topmost visible element", async () => {
const scrollContainerId = "scroll-container-id";

render(
<div id={scrollContainerId}>
<span id="span-1" />
<span id="span-2" />
</div>,
);

// JSDom doesn't actually support layouting HTML so scroll/offset positions must be mocked.
const firstRefElement = document.getElementById("span-1") as HTMLElement;
vi.spyOn(firstRefElement, "offsetTop", "get").mockImplementation(() => 0);

const secondRefElement = document.getElementById("span-2") as HTMLElement;
vi.spyOn(secondRefElement, "offsetTop", "get").mockImplementation(
() => 500,
);

const elements = Array.from(
document.querySelectorAll("span"),
) as HTMLElement[];

const { result } = renderHook(() =>
useTopmostVisibleElement({
elements,
scrollContainerId,
}),
);

scrollPage(scrollContainerId, 408);
expect(result.current).toBe("span-2");

scrollPage(scrollContainerId, 121);
expect(result.current).toBe("span-1");
});
});
7 changes: 5 additions & 2 deletions apps/spruce/src/pages/image/GeneralTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ export const GeneralTable: React.FC<GeneralTableProps> = ({ imageId }) => {
const getDateCopy = useDateFormat();

const data = useMemo(() => {
const image = imageData?.image;
if (loading) {
return [];
}

const image = imageData?.image;
return [
{
property: "Last deployed",
Expand Down Expand Up @@ -83,7 +86,7 @@ export const GeneralTable: React.FC<GeneralTableProps> = ({ imageId }) => {
<BaseTable
data-cy-row="general-table-row"
loading={loading}
loadingRows={data.length}
loadingRows={4}
shouldAlternateRowColor
table={table}
/>
Expand Down
Loading

0 comments on commit e75bd0c

Please sign in to comment.