🎉 Add flyout and semantic improvements to main toolbar

This commit is contained in:
Xavier Julian 2026-05-12 14:35:07 +02:00 committed by Luis de Dios
parent 8cc99f80a5
commit 511c10fa4c
5 changed files with 196 additions and 60 deletions

View File

@ -108,8 +108,12 @@ export class WorkspacePage extends BaseWebSocketPage {
async waitForIdle(options) {
await this.page.evaluate(
(options) => new Promise(
(resolve) => globalThis.requestIdleCallback(resolve, options)), options);
(options) =>
new Promise((resolve) =>
globalThis.requestIdleCallback(resolve, options),
),
options,
);
}
};
@ -229,7 +233,7 @@ export class WorkspacePage extends BaseWebSocketPage {
async #waitForWebSocketReadiness(pageName) {
// TODO: find a better event to settle whether the app is ready to receive notifications via ws
await expect(this.pageName).toHaveText(pageName, { timeout: 30000 })
await expect(this.pageName).toHaveText(pageName, { timeout: 30000 });
}
async sendPresenceMessage(fixture) {
@ -448,6 +452,33 @@ export class WorkspacePage extends BaseWebSocketPage {
await pagesToggle.click();
}
async selectToolbarTool(workspacePage, toolName) {
await workspacePage.page
.getByRole("button", { name: toolName })
.first()
.click();
}
async selectToolFromFlyout(
workspacePage,
{ triggerToolName, targetToolName },
) {
const trigger = workspacePage.page
.getByRole("button", { name: triggerToolName })
.first();
const option = workspacePage.page
.getByRole("menuitemradio", { name: targetToolName })
.first();
await trigger.hover();
// Flyout opening is delayed by 350ms in the toolbar component.
await workspacePage.page.waitForTimeout(450);
await expect(trigger).toHaveAttribute("aria-expanded", "true");
await option.waitFor({ state: "visible" });
await option.click();
}
async moveSelectionToShape(name) {
await this.page.locator("rect.viewport-selrect").hover();
await this.page.mouse.down();

View File

@ -0,0 +1,88 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
import WorkspacePage from "../pages/WorkspacePage";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
});
const expectLayerNamed = async (workspacePage, name) => {
await expect(workspacePage.layers.getByText(name).last()).toBeVisible();
};
test("User creates a frame with the toolbar frame tool", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.selectToolbarTool(workspacePage, "Board (B)");
await workspacePage.clickWithDragViewportAt(100, 100, 180, 120);
await expectLayerNamed(workspacePage, "Board");
});
test("User creates a rectangle with the toolbar rect tool", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.selectToolbarTool(workspacePage, "Rectangle (R)");
await workspacePage.clickWithDragViewportAt(350, 100, 120, 80);
await expectLayerNamed(workspacePage, "Rectangle");
});
test("User creates an ellipse from the shapes flyout", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.selectToolFromFlyout(workspacePage, {
triggerToolName: "Rectangle (R)",
targetToolName: "Ellipse (E)",
});
await workspacePage.clickWithDragViewportAt(520, 100, 100, 100);
await expectLayerNamed(workspacePage, "Ellipse");
});
test("User creates a text shape with the toolbar text tool", async ({
page,
}) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.selectToolbarTool(workspacePage, "Text (T)");
await workspacePage.clickAndMove(120, 320, 300, 380);
await workspacePage.waitForSelectedShapeName("Text");
await workspacePage.page.keyboard.type("toolbar test");
await workspacePage.page.keyboard.press("Escape");
await expectLayerNamed(workspacePage, "Text");
});
test.skip("User creates a path with the toolbar path tool", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.selectToolbarTool(workspacePage, "Path (P)");
await workspacePage.clickAndMove(120, 320, 300, 380);
await workspacePage.page.keyboard.press("Enter");
await expectLayerNamed(workspacePage, "Path");
});
test("User creates a curve from the path flyout", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.selectToolFromFlyout(workspacePage, {
triggerToolName: "Path (P)",
targetToolName: "Curve (Shift+C)",
});
await workspacePage.clickAndMove(120, 320, 300, 380);
await workspacePage.page.keyboard.press("Enter");
await expectLayerNamed(workspacePage, "Path");
});

View File

@ -343,7 +343,9 @@ test("User drag and drop a variant outside the container", async ({ page }) => {
// and use it to calculate the target position
await workspacePage.clickWithDragViewportAt(600, 500, 0, 300);
await expect(workspacePage.layers.getByText("Rectangle / Value 1")).toBeVisible();
await expect(
workspacePage.layers.getByText("Rectangle / Value 1"),
).toBeVisible();
});
test("User cut paste a component inside a variant", async ({ page }) => {
@ -353,7 +355,10 @@ test("User cut paste a component inside a variant", async ({ page }) => {
const variant = await findVariant(workspacePage, 0);
//Create a component
await workspacePage.ellipseShapeButton.click();
await workspacePage.selectToolFromFlyout(workspacePage, {
triggerToolName: "Rectangle (R)",
targetToolName: "Ellipse (E)",
});
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
await workspacePage.clickLeafLayer("Ellipse");
await workspacePage.page.keyboard.press("ControlOrMeta+k");
@ -384,7 +389,10 @@ test("User cut paste a component with path inside a variant", async ({
const variant = await findVariant(workspacePage, 0);
// Create a component
await workspacePage.ellipseShapeButton.click();
await workspacePage.selectToolFromFlyout(workspacePage, {
triggerToolName: "Rectangle (R)",
targetToolName: "Ellipse (E)",
});
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
await workspacePage.clickLeafLayer("Ellipse");
await workspacePage.page.keyboard.press("ControlOrMeta+k");
@ -425,7 +433,10 @@ test("User drag and drop a component with path inside a variant", async ({
const variant = findVariantNoWait(workspacePage, 0);
//Create a component
await workspacePage.ellipseShapeButton.click();
await workspacePage.selectToolFromFlyout(workspacePage, {
triggerToolName: "Rectangle (R)",
targetToolName: "Ellipse (E)",
});
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
await workspacePage.clickLeafLayer("Ellipse");
await workspacePage.page.keyboard.press("ControlOrMeta+k");
@ -457,7 +468,10 @@ test("User cut paste a variant into another container", async ({ page }) => {
await setupVariantsFileWithVariant(workspacePage);
// Create anothe variant
await workspacePage.ellipseShapeButton.click();
await workspacePage.selectToolFromFlyout(workspacePage, {
triggerToolName: "Rectangle (R)",
targetToolName: "Ellipse (E)",
});
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
await workspacePage.clickLeafLayer("Ellipse");
await workspacePage.page.keyboard.press("ControlOrMeta+k");

View File

@ -217,6 +217,7 @@
plugins-enabled (features/active-feature? @st/state "plugins/runtime")
rulers-enabled (mf/deref refs/rulers?)
toolbar-hidden (mf/deref toolbar-hidden-ref)
read-only? (mf/use-ctx ctx/workspace-read-only?)
display-plugins-manager (mf/use-fn
(fn []
(st/emit!
@ -253,62 +254,64 @@
(dom/blur! (dom/get-target event))
(st/emit! (dwc/toggle-toolbar-visibility))))]
[:div {:role "toolbar"
:aria-label (tr "workspace.toolbar.label")
:tabindex "0"
:class (stl/css-case :main-toolbar true
:main-toolbar-no-rulers (not rulers-enabled)
:main-toolbar-hidden toolbar-hidden)}
[:ul {:class (stl/css :main-toolbar-options)}
[:li {:class (stl/css :main-toolbar-option)}
[:> tool-button* {:title (tr "workspace.toolbar.move" (sc/get-tooltip :move))
:selected (and (nil? selected-drawing-tool)
(not selected-edition))
:icon i/move
:on-click on-interrupt}]]
(when-not ^boolean read-only?
[:div {:role "toolbar"
:aria-label (tr "workspace.toolbar.label")
:tabindex "0"
:class (stl/css-case :main-toolbar true
:main-toolbar-no-rulers (not rulers-enabled)
:main-toolbar-hidden toolbar-hidden)}
[:ul {:class (stl/css :main-toolbar-options)
:data-testid "toolbar-options"}
[:li {:class (stl/css :main-toolbar-option)}
[:> tool-button* {:title (tr "workspace.toolbar.move" (sc/get-tooltip :move))
:selected (and (nil? selected-drawing-tool)
(not selected-edition))
:icon i/move
:on-click on-interrupt}]]
[:li {:class (stl/css :main-toolbar-option)}
[:> tool-button* {:title (tool-label :frame)
:selected (= selected-drawing-tool :frame)
:icon i/board
:on-click on-select-tool
:data-tool "frame"}]]
[:li {:class (stl/css :main-toolbar-option)}
[:> tool-button* {:title (tool-label :frame)
:selected (= selected-drawing-tool :frame)
:icon i/board
:on-click on-select-tool
:data-tool "frame"}]]
[:> grouped-tool-flyout* {:key :shapes
:group (get grouped-tools :shapes)
:drawtool selected-drawing-tool
:on-select-tool on-select-tool}]
[:> grouped-tool-flyout* {:key :shapes
:group (get grouped-tools :shapes)
:drawtool selected-drawing-tool
:on-select-tool on-select-tool}]
[:li {:class (stl/css :main-toolbar-option)}
[:> tool-button* {:title (tool-label :text)
:selected (= selected-drawing-tool :text)
:icon i/text
:on-click on-select-tool
:data-tool "text"}]]
[:li {:class (stl/css :main-toolbar-option)}
[:> tool-button* {:title (tool-label :text)
:selected (= selected-drawing-tool :text)
:icon i/text
:on-click on-select-tool
:data-tool "text"}]]
[:> image-upload-tool]
[:> image-upload-tool]
[:> grouped-tool-flyout* {:key :free-draw
:group (get grouped-tools :free-draw)
:drawtool selected-drawing-tool
:on-select-tool on-select-tool}]
[:> grouped-tool-flyout* {:key :free-draw
:group (get grouped-tools :free-draw)
:drawtool selected-drawing-tool
:on-select-tool on-select-tool}]
(when plugins-enabled
[:li {:class (stl/css :main-toolbar-option :main-toolbar-option-plugins)}
[:> tool-button* {:title (tool-label :plugins)
:icon i/puzzle
:on-click display-plugins-manager
:data-tool "plugins"}]])
(when plugins-enabled
[:li {:class (stl/css :main-toolbar-option :main-toolbar-option-plugins)}
[:> tool-button* {:title (tool-label :plugins)
:icon i/puzzle
:on-click display-plugins-manager
:data-tool "plugins"}]])
(when *assert*
[:li {:class (stl/css :main-toolbar-option :main-toolbar-option-debug)}
[:> tool-button* {:title (tool-label :debug)
:selected (contains? layout :debug-panel)
:icon i/bug
:on-click toggle-debug-panel}]])]
[:button {:title (tr "workspace.toolbar.toggle-toolbar")
:aria-label (tr "workspace.toolbar.toggle-toolbar")
:class (stl/css :toolbar-handler)
:on-click toggle-toolbar}
[:div {:class (stl/css :toolbar-handler-indicator)}]]]))
(when *assert*
[:li {:class (stl/css :main-toolbar-option :main-toolbar-option-debug)}
[:> tool-button* {:title (tool-label :debug)
:selected (contains? layout :debug-panel)
:icon i/bug
:on-click toggle-debug-panel}]])]
[:button {:title (tr "workspace.toolbar.toggle-toolbar")
:aria-label (tr "workspace.toolbar.toggle-toolbar")
:class (stl/css :toolbar-handler)
:on-click toggle-toolbar}
[:div {:class (stl/css :toolbar-handler-indicator)}]]])))

View File

@ -11,7 +11,7 @@
.main-toolbar {
--toolbar-position-y: #{$sz-28};
--toolbar-offset-y: 0;
--toolbar-offset-y: 0px;
--menu-border-color: var(--color-background-quaternary);
--menu-background-color: var(--color-background-primary);
--stroke-color: var(--color-foreground-secondary);