diff --git a/gradle/changelog/added_iconbutton.yaml b/gradle/changelog/added_iconbutton.yaml new file mode 100644 index 0000000000..07fb9647ca --- /dev/null +++ b/gradle/changelog/added_iconbutton.yaml @@ -0,0 +1,2 @@ +- type: added + description: New component "icon button" in ui library diff --git a/scm-ui/ui-core/src/base/buttons/Button.stories.tsx b/scm-ui/ui-core/src/base/buttons/Button.stories.tsx index 48bc1ea507..a63cb45882 100644 --- a/scm-ui/ui-core/src/base/buttons/Button.stories.tsx +++ b/scm-ui/ui-core/src/base/buttons/Button.stories.tsx @@ -30,7 +30,9 @@ import { ButtonVariants, ExternalLinkButton as ExternalLinkButtonComponent, LinkButton as LinkButtonComponent, + IconButton as IconButtonComponent, } from "./Button"; +import Icon from "./Icon"; import StoryRouter from "storybook-react-router"; import { StoryFn } from "@storybook/react"; @@ -42,6 +44,7 @@ export default { Button: ButtonComponent, LinkButton: LinkButtonComponent, ExternalLinkButton: ExternalLinkButtonComponent, + IconButton: IconButtonComponent, }, argTypes: { variant: { @@ -64,6 +67,10 @@ const ExternalLinkButtonTemplate: StoryFn ); +const IconButtonTemplate: StoryFn> = (args) => ( + +); + export const Button = ButtonTemplate.bind({}); // More on args: https://storybook.js.org/docs/react/writing-stories/args Button.args = { @@ -87,3 +94,41 @@ ExternalLinkButton.args = { href: "https://scm-manager.org", variant: ButtonVariants.PRIMARY, }; + +const smallIcon = trash; +const mediumIcon = trash; + + /* + + Variant and size are defaulted to medium and colored and do not have to be explicitly added as parameters. + However for the sake of documentation here they are still passed in + + */ +export const IconButtonBorder = IconButtonTemplate.bind({}); +IconButtonBorder.args = { + children: mediumIcon, + variant: "colored", + size: "medium", +}; + + +export const IconButtonBorderDefault = IconButtonTemplate.bind({}); +IconButtonBorderDefault.args = { + children: mediumIcon, + variant: "default", + size: "medium", +}; + +export const IconButtonBorderlessSmall = IconButtonTemplate.bind({}); +IconButtonBorderlessSmall.args = { + children: smallIcon, + variant: "colored", + size: "small", +}; + +export const IconButtonBorderlessSmallDefault = IconButtonTemplate.bind({}); +IconButtonBorderlessSmallDefault.args = { + children: smallIcon, + variant: "default", + size: "small", +}; diff --git a/scm-ui/ui-core/src/base/buttons/Button.tsx b/scm-ui/ui-core/src/base/buttons/Button.tsx index b66883f405..4bae4f7109 100644 --- a/scm-ui/ui-core/src/base/buttons/Button.tsx +++ b/scm-ui/ui-core/src/base/buttons/Button.tsx @@ -22,10 +22,11 @@ * SOFTWARE. */ -import React, { AnchorHTMLAttributes, ButtonHTMLAttributes } from "react"; +import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from "react"; import { Link as ReactRouterLink, LinkProps as ReactRouterLinkProps } from "react-router-dom"; import classNames from "classnames"; import { createAttributesForTesting } from "../helpers"; +import styled from "styled-components"; /** * @beta @@ -141,3 +142,85 @@ export const ExternalLinkButton = React.forwardRef ) ); + +const StyledIconButton = styled.button` + border-radius: 6px !important; + min-width: 2.5rem; + height: 2.5rem; + padding: 0.5em; + &:not(:disabled) { + &:hover { + background-color: color-mix(in srgb, currentColor 10%, transparent); + } + &:active { + background-color: color-mix(in srgb, currentColor 25%, transparent); + } + } +`; + +const StyledIconButtonCircle = styled.button` + border-radius: 50% !important; + border: None; + min-width: 1.5rem; + height: 1.5rem; + padding: 0.5em; + &:not(:disabled) { + &:hover { + background-color: color-mix(in srgb, currentColor 10%, transparent); + } + &:active { + background-color: color-mix(in srgb, currentColor 25%, transparent); + } + } +`; + +export const IconButtonVariants = { + COLORED: "colored", + DEFAULT: "default", +} as const; + +export const IconSizes = { + SMALL: "small", + MEDIUM: "medium", +} as const; + +type IconButtonSize = typeof IconSizes[keyof typeof IconSizes]; + +type IconButtonVariant = typeof IconButtonVariants[keyof typeof IconButtonVariants]; + +const createIconButtonClasses = (variant?: IconButtonVariant) => + classNames("button", { + "is-primary is-outlined": variant === "colored", + }); + +type IconButtonProps = ButtonHTMLAttributes & { + testId?: string; + variant?: IconButtonVariant; + children: ReactNode; + size?: IconButtonSize; +}; + +/** + * Styled html button. + * + * An icon button has to declare an `aria-label`. + * + * @beta + * @since 3.2.0 + */ +export const IconButton = React.forwardRef( + ({ className, variant = "default", size = "medium", testId, type, children, ...props }, ref) => { + const elementAttributes = { + type: type ?? "button", + ...props, + className: classNames(createIconButtonClasses(variant), "button", "has-background-transparent"), + ref: ref, + ...createAttributesForTesting(testId), + }; + return size === "small" ? ( + {children} + ) : ( + {children} + ); + } +);