♻️ Use DS icon buttons in the component options (#12394)

* ♻️ Use a DS icon button for the component pill menu

Replace the raw three dots button of the component pill with the DS
secondary icon button. It had no label, so it reuses the "Options" one,
shown in the DS tooltip above and to the left (it sits at the right edge
of the sidebar), and aria-pressed while the menu is open instead of the
selected class.

Its class drops the button placeholders and only keeps the pill segment
shape: full height and square corners on the left. The tooltip wrapper
fills the segment too.

Add a visual test that selects a component copy so the pill shows.

AI-assisted-by: claude-opus-5-5

* ♻️ Use DS icon buttons for the component annotation actions

The save/create, discard, edit and delete actions of a component
annotation were divs with a click handler: no keyboard focus and no button
role. Replace them with DS ghost icon buttons. Their title moves to the
label, shown in the DS tooltip above and to the left, and save is disabled
while the text is blank, which it already ignored.

The class that hides them until the title is hovered, or while editing or
creating, now goes on the tooltip wrapper. The green and red hover colors
and the fixed size go away with the DS look. While editing or creating,
the actions get a small padding so their hover background does not touch
the annotation border; the title row grows from 32 to 36px there.

Add a visual test that creates an annotation on a main component and
shows both sets of actions.

AI-assisted-by: claude-opus-5-5

* 💄 Open component options tooltips above and to the left

The variants help, create variant, add property and remove property icon
buttons sit at the right edge of the sidebar: open their tooltips above
and to the left, like the other actions of the component options.

AI-assisted-by: claude-opus-5-5

* ♻️ Move component options styles off the deprecated styles

Inline the dropdown placeholders of the component pill menu with DS
tokens and a local variable for the item colors that change on hover, and
give the item label its own class instead of styling the span. Resolve
the annotation title border alias to its DS token and delete the unused
.component-title-actions rule.

The item label uses the DS body-small typography; its line-height is 1.3
instead of 1.4, with no visible change since the item centers it.

component.scss no longer imports refactor/.

AI-assisted-by: claude-opus-5-5
This commit is contained in:
Eva Marco 2026-10-20 09:18:49 +02:00 committed by GitHub
parent 7e5e589744
commit 9904e50889
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 172 additions and 71 deletions

View File

@ -264,6 +264,72 @@ test.describe("Design tab", () => {
});
});
test.describe("Component options", () => {
test("Shows the component pill of a copy", async ({ page }) => {
const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9066.json");
await workspace.goToWorkspace({
fileId: "e179d9df-de35-80bf-8005-283bbd5516b0",
pageId: "e179d9df-de35-80bf-8005-283bbd5516b1",
});
await workspace.clickToggableLayer("Board");
await workspace.clickToggableLayer("Group");
await workspace.layers.getByText("A", { exact: true }).click();
await expect(
workspace.rightSidebar.getByTestId("component-pill-button"),
).toBeVisible();
// The component pill is a small part of the sidebar: compare every
// pixel.
await expect(workspace.rightSidebar).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0,
});
});
test("Shows the annotation actions of a main component", async ({ page }) => {
const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9066.json");
await workspace.goToWorkspace({
fileId: "e179d9df-de35-80bf-8005-283bbd5516b0",
pageId: "e179d9df-de35-80bf-8005-283bbd5516b1",
});
await workspace.clickToggableLayer("Board");
await workspace.clickToggableLayer("Group");
await workspace.layers.getByText("B", { exact: true }).click();
await workspace.rightSidebar
.getByRole("button", { name: "Options" })
.click();
await workspace.rightSidebar.getByText("Create annotation").click();
// Creating: save (disabled while the text is empty) and discard.
const annotation = workspace.rightSidebar.locator("#annotation-textarea");
await expect(annotation).toBeFocused();
await expect(workspace.rightSidebar).toHaveScreenshot(
"annotation-creating.png",
{ maxDiffPixelRatio: 0, threshold: 0 },
);
// Saved: edit and delete show when hovering the title.
await annotation.fill("An annotation");
await workspace.rightSidebar
.getByRole("button", { name: "Create", exact: true })
.click();
const title = workspace.rightSidebar.getByText("Annotation", {
exact: true,
});
await title.hover();
await expect(workspace.rightSidebar).toHaveScreenshot(
"annotation-saved.png",
{ maxDiffPixelRatio: 0, threshold: 0 },
);
});
});
test.describe("Assets tab", () => {
test("Shows the libraries modal correctly", async ({ page }) => {
const workspace = new WorkspacePage(page);

View File

@ -198,31 +198,35 @@
(if (or ^boolean editing?
^boolean creating?)
[:*
[:div {:title (if ^boolean creating?
(tr "labels.create")
(tr "labels.save"))
:on-click on-save
:class (stl/css :annotation-title-icon-action)}
[:> icon* {:icon-id i/tick
:class (stl/css-case :annotation-title-icon-ok true
:disabled invalid-text?)}]]
[:div {:class (stl/css :annotation-title-icon-action)
:title (tr "labels.discard")
:on-click on-discard}
[:> icon* {:icon-id i/close
:class (stl/css :annotation-title-icon-nok)}]]]
[:> icon-button* {:variant "ghost"
:aria-label (if ^boolean creating?
(tr "labels.create")
(tr "labels.save"))
:on-click on-save
:disabled invalid-text?
:tooltip-class (stl/css :annotation-title-icon-action)
:tooltip-placement "top-left"
:icon i/tick}]
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.discard")
:on-click on-discard
:tooltip-class (stl/css :annotation-title-icon-action)
:tooltip-placement "top-left"
:icon i/close}]]
[:*
[:div {:class (stl/css :annotation-title-icon-action)
:title (tr "labels.edit")
:on-click on-edit}
[:> icon* {:icon-id i/curve
:class (stl/css :annotation-title-icon-ok)}]]
[:div {:class (stl/css :annotation-title-icon-action)
:title (tr "labels.delete")
:on-click on-delete-annotation}
[:> icon* {:icon-id i/delete
:class (stl/css :annotation-title-icon-nok)}]]]))]]
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.edit")
:on-click on-edit
:tooltip-class (stl/css :annotation-title-icon-action)
:tooltip-placement "top-left"
:icon i/curve}]
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.delete")
:on-click on-delete-annotation
:tooltip-class (stl/css :annotation-title-icon-action)
:tooltip-placement "top-left"
:icon i/delete}]]))]]
[:div {:class (stl/css-case :annotation-body-hidden (not expanded?))}
[:div {:class (stl/css :annotation-body)}
@ -896,10 +900,14 @@
(when menu-entries?
[:div {:class (stl/css :pill-actions)}
[:button {:class (stl/css-case :pill-actions-btn true
:selected menu-open?)
:on-click on-menu-click}
[:> icon* {:icon-id i/menu}]]
[:> icon-button* {:variant "secondary"
:class (stl/css :pill-actions-btn)
:tooltip-class (stl/css :pill-actions-tooltip)
:aria-label (tr "labels.options")
:aria-pressed menu-open?
:tooltip-placement "top-left"
:on-click on-menu-click
:icon i/menu}]
[:& dropdown {:show menu-open?
:on-close on-menu-close}
@ -910,7 +918,7 @@
[:li {:key title
:class (stl/css :pill-actions-dropdown-item)
:on-click (partial do-action action)}
[:span title]]))]]])]))
[:span {:class (stl/css :pill-actions-dropdown-item-label)} title]]))]]])]))
(mf/defc component-menu*
[{:keys [shapes is-swap-opened]}]
@ -1039,12 +1047,14 @@
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.component.variants-help-modal.title")
:on-click on-click-variant-title-help
:tooltip-placement "top-left"
:icon i/help}])
(when main-instance?
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.shape.menu.add-variant")
:on-click (if is-variant? create-variant transform-into-variant)
:tooltip-placement "top-left"
:icon i/variant}])])]
(when open?
@ -1067,6 +1077,7 @@
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.shape.menu.add-variant-property")
:on-click add-new-property
:tooltip-placement "top-left"
:icon i/add}])]
(when is-swap-opened
@ -1151,7 +1162,8 @@
(tr "workspace.shape.menu.remove-variant-property.last-property")
(tr "workspace.shape.menu.remove-variant-property"))
:on-click on-remove
:disabled is-remove-disabled}]]]))
:disabled is-remove-disabled
:tooltip-placement "top-left"}]]]))
(mf/defc component-variant-main*
[{:keys [shapes]}]
@ -1283,10 +1295,12 @@
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.component.variants-help-modal.title")
:on-click on-click-variant-title-help
:tooltip-placement "top-left"
:icon i/help}]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.shape.menu.add-variant")
:on-click (partial create-variant "workspace:button-design-tab-component")
:tooltip-placement "top-left"
:icon i/variant}]]]
(when open?
@ -1301,6 +1315,7 @@
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.shape.menu.add-variant-property")
:on-click (partial add-new-property "workspace:button-design-tab-component")
:tooltip-placement "top-left"
:icon i/add}]]
(when-not multi?

View File

@ -10,7 +10,6 @@
@use "ds/spacing.scss" as *;
@use "ds/typography.scss" as t;
@use "../../../sidebar/common/sidebar.scss" as sidebar;
@use "refactor/common-refactor.scss" as deprecated;
.annotation {
@include t.use-typography("body-small");
@ -23,7 +22,12 @@
border: $b-1 solid var(--color-accent-primary);
.annotation-title {
border-bottom: $b-1 solid var(--entry-border-color-disabled);
border-bottom: $b-1 solid var(--color-background-quaternary);
}
// Keep the hover background of the actions off the border.
.annotation-title-actions {
padding: var(--sp-xxs);
}
.annotation-title-icon-action {
@ -46,6 +50,10 @@
display: flex;
}
.annotation-title-actions {
padding: var(--sp-xxs);
}
.annotation-title-name {
margin-inline-start: var(--sp-m);
}
@ -59,7 +67,7 @@
.annotation-title {
display: flex;
align-items: center;
block-size: $sz-32;
min-block-size: $sz-32;
&.expanded {
border-bottom: $b-1 solid var(--color-background-quaternary);
@ -90,29 +98,10 @@
line-height: 2.5;
}
.annotation-title-icon-action {
cursor: pointer;
// Tooltip wrapper of each action: hidden until the title is hovered or the
// annotation is being edited or created.
.annotation-title-actions .annotation-title-icon-action {
display: none;
justify-content: center;
align-items: center;
inline-size: $sz-28;
block-size: $sz-32;
border-radius: $br-8;
margin: 0;
padding: 0;
}
.annotation-title-icon-ok:hover {
color: var(--color-accent-success);
&.disabled {
cursor: default;
color: var(--color-foreground-secondary);
}
}
.annotation-title-icon-nok:hover {
color: var(--color-accent-error);
}
.annotation-body-hidden {
@ -489,11 +478,6 @@
color: var(--color-foreground-secondary);
}
.component-title-actions {
display: flex;
gap: var(--sp-xs);
}
.component-content {
grid-column: span 8;
display: flex;
@ -588,25 +572,35 @@
position: relative;
}
.pill-actions-btn {
@extend %button-secondary;
// The tooltip wrapper is the flex item: let it fill the pill segment.
.pill-actions-tooltip {
block-size: 100%;
inline-size: 100%;
}
cursor: unset;
// Right segment of the pill: full height and square corners on the left.
.pill-actions-btn {
block-size: 100%;
inline-size: 100%;
border-radius: 0 $br-8 $br-8 0;
&.selected {
@extend %button-icon-selected;
}
}
.pill-actions-dropdown {
@extend %dropdown-wrapper;
inline-size: $sz-252;
position: absolute;
inset-block-start: $sz-32;
inset-inline-end: 0;
inset-inline-start: unset;
inline-size: $sz-252;
max-block-size: var(--menu-max-height, px2rem(300));
padding: var(--sp-xxs);
margin: 0;
margin-block-start: px2rem(1);
border: $b-2 solid var(--color-background-quaternary);
border-radius: $br-8;
box-shadow: 0 0 px2rem(12) 0 var(--color-shadow-dark);
z-index: var(--z-index-dropdown);
overflow: hidden auto;
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
&.extended {
inset-block-start: $sz-48;
@ -614,7 +608,33 @@
}
.pill-actions-dropdown-item {
@extend %dropdown-element-base;
@include t.use-typography("body-small");
--pill-actions-dropdown-item-foreground-color: var(--color-foreground-secondary);
--pill-actions-dropdown-item-background-color: transparent;
display: flex;
align-items: center;
gap: var(--sp-s);
block-size: $sz-32;
padding: 0 var(--sp-s);
border-radius: $br-6;
cursor: pointer;
color: var(--pill-actions-dropdown-item-foreground-color);
background-color: var(--pill-actions-dropdown-item-background-color);
&:hover {
--pill-actions-dropdown-item-foreground-color: var(--color-foreground-primary);
--pill-actions-dropdown-item-background-color: var(--color-background-quaternary);
}
}
.pill-actions-dropdown-item-label {
display: block;
max-inline-size: 99%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.variant-property-list {