diff --git a/frontend/src/app/main/ui/ds/buttons/button.cljs b/frontend/src/app/main/ui/ds/buttons/button.cljs index c7ea4245ab..14985a6896 100644 --- a/frontend/src/app/main/ui/ds/buttons/button.cljs +++ b/frontend/src/app/main/ui/ds/buttons/button.cljs @@ -36,7 +36,8 @@ :button-destructive (= variant "destructive")) props (mf/spread-props props {:class [class internal-class] :href to - :type (d/nilv type "button") + ;; On an anchor, `type` is a MIME type hint. + :type (when-not to (d/nilv type "button")) :ref (fn [node] (when on-ref (on-ref node)))})] diff --git a/frontend/src/app/main/ui/ds/buttons/button.stories.jsx b/frontend/src/app/main/ui/ds/buttons/button.stories.jsx index f030431e12..fcdd86108d 100644 --- a/frontend/src/app/main/ui/ds/buttons/button.stories.jsx +++ b/frontend/src/app/main/ui/ds/buttons/button.stories.jsx @@ -6,6 +6,7 @@ import * as React from "react"; import Components from "@target/components"; +import { within, expect } from "storybook/test"; const { Button } = Components; const { icons } = Components.meta; @@ -67,3 +68,34 @@ export const Destructive = { variant: "destructive", }, }; + +// With `to`, the button renders an anchor: it keeps the button look but it is +// a real link (href, open in a new tab, announced as a link). +export const AsLink = { + args: { + variant: "secondary", + to: "https://penpot.app", + children: "Go to Penpot", + }, +}; + +export const TestAsLinkRendersAnchor = { + args: AsLink.args, + play: async ({ canvasElement }) => { + const link = within(canvasElement).getByRole("link", { + name: "Go to Penpot", + }); + await expect(link).toHaveAttribute("href", "https://penpot.app"); + // `type` on an anchor is a MIME type hint, not a button type. + await expect(link).not.toHaveAttribute("type"); + }, +}; + +export const TestButtonDefaultsToTypeButton = { + play: async ({ canvasElement }) => { + const button = within(canvasElement).getByRole("button", { + name: "Lorem ipsum", + }); + await expect(button).toHaveAttribute("type", "button"); + }, +}; diff --git a/frontend/src/app/main/ui/ds/buttons/buttons.mdx b/frontend/src/app/main/ui/ds/buttons/buttons.mdx index 5eaf8ca5b5..4bcbbd0156 100644 --- a/frontend/src/app/main/ui/ds/buttons/buttons.mdx +++ b/frontend/src/app/main/ui/ds/buttons/buttons.mdx @@ -71,8 +71,23 @@ Action buttons are a variant of `icon-button*` (`"action"`) but smaller. ### Semantics -In some cases, a button needs to be semantically an 'a' tag, but visually maintain the button style. -In that case, the button accepts a 'to' property with the expected 'href'. It will then be rendered as an anchor element. +When the control takes the user to another page or URL, it must be a link, even if it looks like a button. +Pass the destination in `to`: `button*` then renders an `` with the button style, so it can be +opened in a new tab and assistive technologies announce it as a link. It renders no `type` attribute, +which on an anchor would mean a MIME type. + +```clj +[:> button* {:variant "secondary" + :to (rt/resolve-uri router :auth-login {})} + (tr "profile.recovery.go-to-login")] +``` + +To keep the in-app navigation on a plain click, add an `on-click` that calls `prevent-default` (unless +Ctrl/Cmd is pressed) and emits the navigation event. Other anchor attributes, such as `target`, pass through. + + + +Use a plain `button*` (without `to`) for actions that do not change the URL, such as opening a modal. ### Accessibility