diff --git a/frontend/playwright/ui/visual-specs/workspace.spec.js b/frontend/playwright/ui/visual-specs/workspace.spec.js index 632dea1f56..1c382e7c25 100644 --- a/frontend/playwright/ui/visual-specs/workspace.spec.js +++ b/frontend/playwright/ui/visual-specs/workspace.spec.js @@ -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); diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index 8d46f46a3f..00660a3dfb 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -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? diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss index ca8bbd7aab..acbe025003 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss @@ -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 {