diff --git a/packages/css/src/components/button/button.scss b/packages/css/src/components/button/button.scss index 89d2e4ce04..baf5276da5 100644 --- a/packages/css/src/components/button/button.scss +++ b/packages/css/src/components/button/button.scss @@ -96,7 +96,7 @@ } } -.ams-button--icon-position-only { - padding-block: var(--ams-button-icon-position-only-padding-block); - padding-inline: var(--ams-button-icon-position-only-padding-inline); +.ams-button--icon-only { + padding-block: var(--ams-button-icon-only-padding-block); + padding-inline: var(--ams-button-icon-only-padding-inline); } diff --git a/packages/react/src/Button/Button.test.tsx b/packages/react/src/Button/Button.test.tsx index 60f1f8bdf0..1476bb0799 100644 --- a/packages/react/src/Button/Button.test.tsx +++ b/packages/react/src/Button/Button.test.tsx @@ -1,4 +1,4 @@ -import { ShareIcon } from '@amsterdam/design-system-react-icons' +import { CloseIcon } from '@amsterdam/design-system-react-icons' import { render, screen } from '@testing-library/react' import '@testing-library/jest-dom' import { createRef } from 'react' @@ -114,50 +114,51 @@ describe('Button', () => { it('renders a button with an icon at the end', () => { render( - , ) const button = screen.getByRole('button', { - name: 'Share', + name: 'Sluiten', }) + const icon = button.querySelector('.ams-icon:last-child') expect(button).toBeInTheDocument() - const icon = button.querySelector('.ams-icon:last-child') expect(icon).toBeInTheDocument() }) - it('renders a button with an icon at the start', () => { + it('renders a button with an icon before the label', () => { render( - , ) const button = screen.getByRole('button', { - name: 'Share', + name: 'Sluiten', }) + const icon = button.querySelector('.ams-icon:first-child') expect(button).toBeInTheDocument() - const icon = button.querySelector('.ams-icon:first-child') expect(icon).toBeInTheDocument() }) it('renders a button with an icon only', () => { render( - , ) const button = screen.getByRole('button', { - name: 'Share', + name: 'Sluiten', }) + const icon = button.querySelector('.ams-icon') + const label = button.querySelector('.ams-visually-hidden') expect(button).toBeInTheDocument() - expect(button).toHaveClass('ams-button--icon-position-only') - const label = button.querySelector('.ams-visually-hidden') - expect(label).toHaveTextContent('Share') + expect(icon).toBeInTheDocument() + expect(label).toBeInTheDocument() }) }) diff --git a/packages/react/src/Button/Button.tsx b/packages/react/src/Button/Button.tsx index 08a44f2d43..e9f9461062 100644 --- a/packages/react/src/Button/Button.tsx +++ b/packages/react/src/Button/Button.tsx @@ -5,20 +5,31 @@ import clsx from 'clsx' import { forwardRef } from 'react' -import type { ButtonHTMLAttributes, ForwardedRef, PropsWithChildren } from 'react' +import type { ButtonHTMLAttributes, ForwardedRef, PropsWithChildren, ReactNode } from 'react' import { Icon } from '../Icon' import type { IconProps } from '../Icon' +type IconBeforeProp = { + /** Shows the icon before the label. Requires a value for `icon`. Cannot be used together with `iconOnly`. */ + iconBefore?: boolean + iconOnly?: never +} + +type IconOnlyProp = { + iconBefore?: never + /** Shows the icon without the label. Requires a value for `icon`. Cannot be used together with `iconBefore`. */ + iconOnly?: boolean +} + type IconButtonProps = { - /** An icon to add to the button. */ + /** Adds an icon to the button, showing it after the label. */ icon: IconProps['svg'] - /** The position of the icon. The default is after the label. */ - iconPosition?: 'start' | 'only' -} +} & (IconBeforeProp | IconOnlyProp) type TextButtonProps = { icon?: never - iconPosition?: never + iconBefore?: never + iconOnly?: never } export type ButtonProps = { @@ -29,27 +40,41 @@ export type ButtonProps = { export const Button = forwardRef( ( - { children, className, disabled, icon, iconPosition, type, variant = 'primary', ...restProps }: ButtonProps, + { children, className, disabled, icon, iconBefore, iconOnly, type, variant = 'primary', ...restProps }: ButtonProps, ref: ForwardedRef, ) => { + const content = (): ReactNode => { + switch (true) { + case !icon: + return children + case iconBefore: + return [, children] + case iconOnly: + return [ + , + + {children} + , + ] + default: + return [children, ] + } + } + return ( ) }, diff --git a/proprietary/tokens/src/components/ams/button.tokens.json b/proprietary/tokens/src/components/ams/button.tokens.json index 58cd0e7929..fc23bdf712 100644 --- a/proprietary/tokens/src/components/ams/button.tokens.json +++ b/proprietary/tokens/src/components/ams/button.tokens.json @@ -57,7 +57,7 @@ "color": { "value": "{ams.color.neutral-grey2}" } } }, - "icon-position-only": { + "icon-only": { "padding-block": { "value": "{ams.space.sm}" }, "padding-inline": { "value": "{ams.space.sm}" } } diff --git a/storybook/src/components/Button/Button.docs.mdx b/storybook/src/components/Button/Button.docs.mdx index 633c6b8ed9..164e138739 100644 --- a/storybook/src/components/Button/Button.docs.mdx +++ b/storybook/src/components/Button/Button.docs.mdx @@ -34,20 +34,20 @@ They are also a good choice for buttons with an icon only. -### With an icon +### With icon Add an icon if it makes it easier or faster to understand its purpose. The icon will appear after the label. -### With an icon at the start +### With icon before The icon can also appear before the label. - + -### With an icon only +### With icon only A button can even consist of an icon only. Do this only for icons that are universally recognized. diff --git a/storybook/src/components/Button/Button.stories.tsx b/storybook/src/components/Button/Button.stories.tsx index 6f9310b2ea..34b74d1c51 100644 --- a/storybook/src/components/Button/Button.stories.tsx +++ b/storybook/src/components/Button/Button.stories.tsx @@ -13,6 +13,9 @@ const meta = { args: { children: 'Versturen', disabled: false, + icon: undefined, + iconBefore: false, + iconOnly: false, variant: 'primary', }, argTypes: { @@ -27,12 +30,21 @@ const meta = { options: [undefined, ...Object.keys(Icons)], mapping: Icons, }, - iconPosition: { + iconBefore: { control: { - type: 'inline-radio', - labels: { undefined: 'end', start: 'start', only: 'only' }, + type: 'boolean', + }, + if: { + arg: 'icon', + }, + }, + iconOnly: { + control: { + type: 'boolean', + }, + if: { + arg: 'icon', }, - options: [undefined, 'start', 'only'], }, }, } satisfies Meta @@ -64,11 +76,11 @@ export const WithIcon: Story = { }, } -export const WithIconAtStart: Story = { +export const WithIconBefore: Story = { args: { children: 'Sluiten', icon: Icons.CloseIcon, - iconPosition: 'start', + iconBefore: true, }, } @@ -76,7 +88,7 @@ export const WithIconOnly: Story = { args: { children: 'Sluiten', icon: Icons.CloseIcon, - iconPosition: 'only', + iconOnly: true, variant: 'tertiary', }, }