mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-07 21:08:34 +03:00
style(webui): float in conversation highlight
This commit is contained in:
@@ -107,6 +107,8 @@ export const ChatList = memo(function ChatList({
|
|||||||
const listContentRef = useRef<HTMLDivElement>(null);
|
const listContentRef = useRef<HTMLDivElement>(null);
|
||||||
const activeRowRef = useRef<HTMLDivElement>(null);
|
const activeRowRef = useRef<HTMLDivElement>(null);
|
||||||
const activeHighlightRef = useRef<HTMLDivElement>(null);
|
const activeHighlightRef = useRef<HTMLDivElement>(null);
|
||||||
|
const activeHighlightSurfaceRef = useRef<HTMLDivElement>(null);
|
||||||
|
const highlightVisibleRef = useRef(false);
|
||||||
const labels = useMemo<ChatGroupLabels>(() => ({
|
const labels = useMemo<ChatGroupLabels>(() => ({
|
||||||
pinned: t("chat.groups.pinned"),
|
pinned: t("chat.groups.pinned"),
|
||||||
all: t("chat.groups.all"),
|
all: t("chat.groups.all"),
|
||||||
@@ -162,17 +164,27 @@ export const ChatList = memo(function ChatList({
|
|||||||
}, [showArchived, sort]);
|
}, [showArchived, sort]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
let resetTransitionFrame: number | null = null;
|
||||||
|
|
||||||
const updateHighlight = () => {
|
const updateHighlight = () => {
|
||||||
const content = listContentRef.current;
|
const content = listContentRef.current;
|
||||||
const row = activeRowRef.current;
|
const row = activeRowRef.current;
|
||||||
const highlight = activeHighlightRef.current;
|
const highlight = activeHighlightRef.current;
|
||||||
|
const surface = activeHighlightSurfaceRef.current;
|
||||||
|
|
||||||
if (!highlight) return;
|
if (!highlight || !surface) return;
|
||||||
if (!content || !row) {
|
if (!content || !row) {
|
||||||
highlight.style.opacity = "0";
|
surface.style.opacity = "0";
|
||||||
|
surface.style.transform = "scale(0.97)";
|
||||||
|
highlightVisibleRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldFloatIn = !highlightVisibleRef.current;
|
||||||
|
if (shouldFloatIn) {
|
||||||
|
highlight.style.transitionProperty = "none";
|
||||||
|
}
|
||||||
|
|
||||||
const contentRect = content.getBoundingClientRect();
|
const contentRect = content.getBoundingClientRect();
|
||||||
const rowRect = row.getBoundingClientRect();
|
const rowRect = row.getBoundingClientRect();
|
||||||
highlight.style.width = `${rowRect.width}px`;
|
highlight.style.width = `${rowRect.width}px`;
|
||||||
@@ -180,7 +192,21 @@ export const ChatList = memo(function ChatList({
|
|||||||
highlight.style.transform = `translate3d(${rowRect.left - contentRect.left}px, ${
|
highlight.style.transform = `translate3d(${rowRect.left - contentRect.left}px, ${
|
||||||
rowRect.top - contentRect.top
|
rowRect.top - contentRect.top
|
||||||
}px, 0)`;
|
}px, 0)`;
|
||||||
highlight.style.opacity = "1";
|
|
||||||
|
if (shouldFloatIn) {
|
||||||
|
void highlight.offsetWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
surface.style.opacity = "1";
|
||||||
|
surface.style.transform = "scale(1)";
|
||||||
|
highlightVisibleRef.current = true;
|
||||||
|
|
||||||
|
if (shouldFloatIn) {
|
||||||
|
resetTransitionFrame = window.requestAnimationFrame(() => {
|
||||||
|
highlight.style.removeProperty("transition-property");
|
||||||
|
resetTransitionFrame = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateHighlight();
|
updateHighlight();
|
||||||
@@ -196,6 +222,10 @@ export const ChatList = memo(function ChatList({
|
|||||||
window.addEventListener("resize", updateHighlight);
|
window.addEventListener("resize", updateHighlight);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (resetTransitionFrame !== null) {
|
||||||
|
window.cancelAnimationFrame(resetTransitionFrame);
|
||||||
|
}
|
||||||
|
activeHighlightRef.current?.style.removeProperty("transition-property");
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
window.removeEventListener("resize", updateHighlight);
|
window.removeEventListener("resize", updateHighlight);
|
||||||
};
|
};
|
||||||
@@ -449,10 +479,16 @@ export const ChatList = memo(function ChatList({
|
|||||||
ref={activeHighlightRef}
|
ref={activeHighlightRef}
|
||||||
data-testid="active-chat-highlight"
|
data-testid="active-chat-highlight"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="pointer-events-none absolute left-0 top-0 z-0 !mt-0 rounded-xl bg-sidebar-foreground/[0.055] opacity-0 transition-[transform,width,height,opacity] duration-300 ease-out will-change-transform motion-reduce:transition-none dark:bg-white/[0.07]"
|
className="pointer-events-none absolute left-0 top-0 z-0 !mt-0 transition-[transform,width,height] duration-300 ease-out will-change-transform motion-reduce:transition-none"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={activeHighlightSurfaceRef}
|
||||||
|
data-testid="active-chat-highlight-surface"
|
||||||
|
className="h-full w-full scale-[0.97] rounded-xl bg-sidebar-foreground/[0.055] opacity-0 transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none dark:bg-white/[0.07]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,12 @@ describe("ChatList", () => {
|
|||||||
expect(within(chatsSection).queryByText("Project chat")).not.toBeInTheDocument();
|
expect(within(chatsSection).queryByText("Project chat")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("slides one borderless highlight between selected topics", () => {
|
it("floats a borderless highlight in, then slides it between selected topics", () => {
|
||||||
|
let revealFrame: FrameRequestCallback | null = null;
|
||||||
|
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
|
||||||
|
revealFrame = callback;
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
|
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
|
||||||
function (this: HTMLElement) {
|
function (this: HTMLElement) {
|
||||||
if (this.hasAttribute("data-chat-list-content")) {
|
if (this.hasAttribute("data-chat-list-content")) {
|
||||||
@@ -250,11 +255,26 @@ describe("ChatList", () => {
|
|||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
<ChatList
|
<ChatList
|
||||||
{...props}
|
{...props}
|
||||||
activeKey="websocket:active"
|
activeKey={null}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const highlight = screen.getByTestId("active-chat-highlight");
|
const highlight = screen.getByTestId("active-chat-highlight");
|
||||||
|
const surface = screen.getByTestId("active-chat-highlight-surface");
|
||||||
|
expect(surface).toHaveClass(
|
||||||
|
"bg-sidebar-foreground/[0.055]",
|
||||||
|
"transition-[opacity,transform]",
|
||||||
|
"motion-reduce:transition-none",
|
||||||
|
);
|
||||||
|
expect(surface).toHaveStyle("opacity: 0; transform: scale(0.97)");
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<ChatList
|
||||||
|
{...props}
|
||||||
|
activeKey="websocket:active"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
const activeButton = screen.getByTitle("Active topic");
|
const activeButton = screen.getByTitle("Active topic");
|
||||||
expect(activeButton).toHaveAttribute("aria-current", "page");
|
expect(activeButton).toHaveAttribute("aria-current", "page");
|
||||||
expect(activeButton.parentElement).not.toHaveClass(
|
expect(activeButton.parentElement).not.toHaveClass(
|
||||||
@@ -262,13 +282,16 @@ describe("ChatList", () => {
|
|||||||
"shadow-[inset_0_0_0_1px_hsl(var(--sidebar-border)/0.55)]",
|
"shadow-[inset_0_0_0_1px_hsl(var(--sidebar-border)/0.55)]",
|
||||||
);
|
);
|
||||||
expect(highlight).toHaveClass(
|
expect(highlight).toHaveClass(
|
||||||
"bg-sidebar-foreground/[0.055]",
|
"transition-[transform,width,height]",
|
||||||
"transition-[transform,width,height,opacity]",
|
|
||||||
"motion-reduce:transition-none",
|
"motion-reduce:transition-none",
|
||||||
);
|
);
|
||||||
expect(highlight).toHaveStyle(
|
expect(highlight).toHaveStyle(
|
||||||
"width: 284px; height: 32px; transform: translate3d(8px, 12px, 0); opacity: 1",
|
"width: 284px; height: 32px; transform: translate3d(8px, 12px, 0); transition-property: none",
|
||||||
);
|
);
|
||||||
|
expect(surface).toHaveStyle("opacity: 1; transform: scale(1)");
|
||||||
|
|
||||||
|
revealFrame?.(0);
|
||||||
|
expect(highlight.style.transitionProperty).toBe("");
|
||||||
|
|
||||||
rerender(
|
rerender(
|
||||||
<ChatList
|
<ChatList
|
||||||
@@ -280,7 +303,7 @@ describe("ChatList", () => {
|
|||||||
expect(screen.getByTitle("Active topic")).not.toHaveAttribute("aria-current");
|
expect(screen.getByTitle("Active topic")).not.toHaveAttribute("aria-current");
|
||||||
expect(screen.getByTitle("Inactive topic")).toHaveAttribute("aria-current", "page");
|
expect(screen.getByTitle("Inactive topic")).toHaveAttribute("aria-current", "page");
|
||||||
expect(highlight).toHaveStyle(
|
expect(highlight).toHaveStyle(
|
||||||
"width: 284px; height: 40px; transform: translate3d(8px, 48px, 0); opacity: 1",
|
"width: 284px; height: 40px; transform: translate3d(8px, 48px, 0)",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user