mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 10:12:44 +00:00
Keep sidebar setup actions inside the empty-state card (#7312)
* fix(app): wrap sidebar empty-state actions so longer labels stay inside the card The "Add project" / "Import session" row in the sidebar's empty state is a non-wrapping flex row. With longer translations (e.g. Russian) the second button overflows the card at the default 320px sidebar width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(app): cover sidebar empty-state actions staying inside the card Renders the empty state in a real browser at the default sidebar width and checks both actions' bounds against the card, for Russian (fails without the wrap) and English. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(app): keep long sidebar actions inside narrow cards Wrapping the row still lets a single translated action exceed its available width. Bound each empty-state action to the row and allow its label to shrink and wrap. Co-authored-by: Vyacheslav Scherbinin <gentslava@mail.ru> --------- Co-authored-by: Vyacheslav Scherbinin <gentslava@mail.ru> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Vyacheslav Scherbinin
parent
fcda2f1cf0
commit
303cebec06
@@ -0,0 +1,85 @@
|
||||
import React, { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getAllByRole } from "@testing-library/dom";
|
||||
import { i18n as testI18n } from "@/i18n/i18next";
|
||||
import { DEFAULT_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH } from "@/stores/panel-store";
|
||||
import { SidebarProjectEmptyState } from "./empty-states";
|
||||
|
||||
// App sources compile against the classic JSX runtime, which expects React on the global.
|
||||
beforeEach(() => vi.stubGlobal("React", React));
|
||||
|
||||
interface Mounted {
|
||||
root: Root;
|
||||
container: HTMLDivElement;
|
||||
}
|
||||
|
||||
const mounted: Mounted[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
for (const entry of mounted.splice(0)) {
|
||||
act(() => entry.root.unmount());
|
||||
entry.container.remove();
|
||||
}
|
||||
await testI18n.changeLanguage("en");
|
||||
});
|
||||
|
||||
async function mountEmptyState(language: string, width: number): Promise<HTMLElement> {
|
||||
await testI18n.changeLanguage(language);
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.style.width = `${width}px`;
|
||||
// Match the workspace list's horizontal inset around the card.
|
||||
container.style.paddingInline = "6px";
|
||||
container.style.boxSizing = "border-box";
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
act(() => root.render(<SidebarProjectEmptyState />));
|
||||
mounted.push({ root, container });
|
||||
|
||||
const card = container.querySelector('[data-testid="sidebar-project-empty-state"]');
|
||||
if (!(card instanceof HTMLElement)) {
|
||||
throw new Error("SidebarProjectEmptyState did not render its card");
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
function expectActionsInsideCard(card: HTMLElement) {
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
const buttons = getAllByRole(card, "button");
|
||||
|
||||
expect(buttons).toHaveLength(2);
|
||||
for (const button of buttons) {
|
||||
const rect = button.getBoundingClientRect();
|
||||
expect(rect.left).toBeGreaterThanOrEqual(cardRect.left);
|
||||
expect(rect.right).toBeLessThanOrEqual(cardRect.right);
|
||||
|
||||
// Containing the button must also keep the whole label readable, without clipping.
|
||||
const label = document.createRange();
|
||||
label.selectNodeContents(button);
|
||||
for (const fragment of Array.from(label.getClientRects())) {
|
||||
expect(fragment.left).toBeGreaterThanOrEqual(rect.left);
|
||||
expect(fragment.right).toBeLessThanOrEqual(rect.right);
|
||||
expect(fragment.top).toBeGreaterThanOrEqual(rect.top);
|
||||
expect(fragment.bottom).toBeLessThanOrEqual(rect.bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("SidebarProjectEmptyState", () => {
|
||||
const cases = ["en", "ru", "fr", "ja", "es", "pt-BR", "ar", "ko", "zh-CN"].flatMap((language) =>
|
||||
[MIN_SIDEBAR_WIDTH, DEFAULT_SIDEBAR_WIDTH].map((width) => ({ language, width })),
|
||||
);
|
||||
it.each(cases)(
|
||||
"keeps every action inside the card ($language, $width px sidebar)",
|
||||
async ({ language, width }) => {
|
||||
expectActionsInsideCard(await mountEmptyState(language, width));
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps English actions on one row when they fit", async () => {
|
||||
const card = await mountEmptyState("en", DEFAULT_SIDEBAR_WIDTH);
|
||||
const [addProject, importSession] = getAllByRole(card, "button");
|
||||
expect(addProject.getBoundingClientRect().top).toBe(importSession.getBoundingClientRect().top);
|
||||
});
|
||||
});
|
||||
@@ -54,7 +54,13 @@ export function SidebarFilterEmptyState() {
|
||||
title={t("sidebar.filterEmpty.title")}
|
||||
description={t("sidebar.filterEmpty.description")}
|
||||
>
|
||||
<Button variant="secondary" size="xs" onPress={clearFilters}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
style={styles.action}
|
||||
textStyle={styles.actionText}
|
||||
onPress={clearFilters}
|
||||
>
|
||||
{t("sidebar.filterEmpty.clear")}
|
||||
</Button>
|
||||
</SidebarEmptyStateCard>
|
||||
@@ -76,10 +82,24 @@ export function SidebarProjectEmptyState({
|
||||
title={t("sidebar.project.empty.title")}
|
||||
description={t("sidebar.project.empty.description")}
|
||||
>
|
||||
<Button variant="secondary" size="xs" leftIcon={Plus} onPress={onAddProject}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
leftIcon={Plus}
|
||||
style={styles.action}
|
||||
textStyle={styles.actionText}
|
||||
onPress={onAddProject}
|
||||
>
|
||||
{t("sidebar.actions.addProject")}
|
||||
</Button>
|
||||
<Button variant="outline" size="xs" leftIcon={Import} onPress={onImportSession}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
leftIcon={Import}
|
||||
style={styles.action}
|
||||
textStyle={styles.actionText}
|
||||
onPress={onImportSession}
|
||||
>
|
||||
{t("importSession.title")}
|
||||
</Button>
|
||||
</SidebarEmptyStateCard>
|
||||
@@ -99,9 +119,16 @@ const styles = StyleSheet.create((theme) => ({
|
||||
},
|
||||
actions: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: theme.spacing[2],
|
||||
marginTop: theme.spacing[4],
|
||||
},
|
||||
action: {
|
||||
maxWidth: "100%",
|
||||
},
|
||||
actionText: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
title: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
|
||||
Reference in New Issue
Block a user