Merged in fix/BOOK-293-button-variants (pull request #3371)

fix(BOOK-293): changed variants and props on IconButton component

* fix(BOOK-293): changed variants and props on IconButton component

* fix(BOOK-293): inherit color for icon


Approved-by: Bianca Widstam
Approved-by: Christel Westerberg
This commit is contained in:
Erik Tiekstra
2025-12-19 12:32:52 +00:00
committed by Bianca Widstam
parent 2197ab2137
commit 3f632e6031
169 changed files with 665 additions and 944 deletions

View File

@@ -2,9 +2,8 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { expect, fn } from 'storybook/test'
import { MaterialIcon } from '../Icons/MaterialIcon'
import { config as typographyConfig } from '../Typography/variants'
import { Button } from './Button'
import { buttonIconNames } from './types'
import { config as buttonConfig } from './variants'
const meta: Meta<typeof Button> = {
@@ -13,12 +12,10 @@ const meta: Meta<typeof Button> = {
argTypes: {
onPress: {
table: {
disable: true,
type: { summary: 'function' },
defaultValue: { summary: 'undefined' },
},
},
typography: {
control: 'select',
options: Object.keys(typographyConfig.variants.variant),
description: 'Callback function to handle button press events.',
},
variant: {
control: 'select',
@@ -58,7 +55,7 @@ const meta: Meta<typeof Button> = {
},
},
wrapping: {
control: 'radio',
control: 'boolean',
options: Object.keys(buttonConfig.variants.wrapping),
type: 'boolean',
table: {
@@ -69,6 +66,47 @@ const meta: Meta<typeof Button> = {
description:
'Only applies to variant `Text`. If `false`, the button will use smaller padding.',
},
leadingIconName: {
control: 'select',
options: buttonIconNames,
table: {
type: { summary: buttonIconNames.join(' | ') },
defaultValue: { summary: 'undefined' },
},
description: 'Name of the Material Icon to use as leading icon.',
},
trailingIconName: {
control: 'select',
options: buttonIconNames,
table: {
type: { summary: buttonIconNames.join(' | ') },
defaultValue: { summary: 'undefined' },
},
description: 'Name of the Material Icon to use as trailing icon.',
},
isDisabled: {
control: 'boolean',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
isPending: {
control: 'boolean',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
fullWidth: {
control: 'boolean',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
description:
'By default, the button width adjusts to its content. Set to true to make the button take the full width of its container.',
},
},
}
@@ -83,7 +121,6 @@ export const Default: Story = {
args: {
onPress: fn(),
children: 'Button',
typography: 'Body/Paragraph/mdBold',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -95,7 +132,7 @@ export const PrimaryLarge: Story = {
args: {
...Default.args,
variant: 'Primary',
size: 'Large',
size: 'lg',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -106,7 +143,7 @@ export const PrimaryLarge: Story = {
export const PrimaryMedium: Story = {
args: {
...PrimaryLarge.args,
size: 'Medium',
size: 'md',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -117,8 +154,7 @@ export const PrimaryMedium: Story = {
export const PrimarySmall: Story = {
args: {
...PrimaryLarge.args,
typography: 'Body/Supporting text (caption)/smBold',
size: 'Small',
size: 'sm',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -154,7 +190,6 @@ export const PrimaryOnDarkBackground: Story = {
globals: globalStoryPropsInverted,
args: {
...PrimaryLarge.args,
onPress: fn(), // Fresh spy instance
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -166,7 +201,7 @@ export const PrimaryInvertedLarge: Story = {
globals: globalStoryPropsInverted,
args: {
...Default.args,
size: 'Large',
size: 'lg',
color: 'Inverted',
},
play: async ({ canvas, userEvent, args }) => {
@@ -179,7 +214,7 @@ export const PrimaryInvertedMedium: Story = {
globals: globalStoryPropsInverted,
args: {
...PrimaryInvertedLarge.args,
size: 'Medium',
size: 'md',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -191,8 +226,7 @@ export const PrimaryInvertedSmall: Story = {
globals: globalStoryPropsInverted,
args: {
...PrimaryInvertedLarge.args,
typography: 'Body/Supporting text (caption)/smBold',
size: 'Small',
size: 'sm',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -230,7 +264,7 @@ export const SecondaryLarge: Story = {
args: {
...Default.args,
variant: 'Secondary',
size: 'Large',
size: 'lg',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -241,7 +275,7 @@ export const SecondaryLarge: Story = {
export const SecondaryMedium: Story = {
args: {
...SecondaryLarge.args,
size: 'Medium',
size: 'md',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -252,8 +286,7 @@ export const SecondaryMedium: Story = {
export const SecondarySmall: Story = {
args: {
...SecondaryLarge.args,
typography: 'Body/Supporting text (caption)/smBold',
size: 'Small',
size: 'sm',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -291,7 +324,7 @@ export const SecondaryInvertedLarge: Story = {
...Default.args,
variant: 'Secondary',
color: 'Inverted',
size: 'Large',
size: 'lg',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -303,7 +336,7 @@ export const SecondaryInvertedMedium: Story = {
globals: globalStoryPropsInverted,
args: {
...SecondaryInvertedLarge.args,
size: 'Medium',
size: 'md',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -315,8 +348,7 @@ export const SecondaryInvertedSmall: Story = {
globals: globalStoryPropsInverted,
args: {
...SecondaryInvertedLarge.args,
typography: 'Body/Supporting text (caption)/smBold',
size: 'Small',
size: 'sm',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -354,7 +386,7 @@ export const TertiaryLarge: Story = {
args: {
...Default.args,
variant: 'Tertiary',
size: 'Large',
size: 'lg',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -365,7 +397,7 @@ export const TertiaryLarge: Story = {
export const TertiaryMedium: Story = {
args: {
...TertiaryLarge.args,
size: 'Medium',
size: 'md',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -376,8 +408,7 @@ export const TertiaryMedium: Story = {
export const TertiarySmall: Story = {
args: {
...TertiaryLarge.args,
typography: 'Body/Supporting text (caption)/smBold',
size: 'Small',
size: 'sm',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -413,7 +444,7 @@ export const TextLarge: Story = {
args: {
...Default.args,
variant: 'Text',
size: 'Large',
size: 'lg',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -424,7 +455,7 @@ export const TextLarge: Story = {
export const TextMedium: Story = {
args: {
...TextLarge.args,
size: 'Medium',
size: 'md',
},
play: async ({ canvas, userEvent, args }) => {
@@ -436,8 +467,7 @@ export const TextMedium: Story = {
export const TextSmall: Story = {
args: {
...TextLarge.args,
typography: 'Body/Supporting text (caption)/smBold',
size: 'Small',
size: 'sm',
},
play: async ({ canvas, userEvent, args }) => {
@@ -475,7 +505,7 @@ export const TextInvertedLarge: Story = {
...Default.args,
variant: 'Text',
color: 'Inverted',
size: 'Large',
size: 'lg',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -487,7 +517,7 @@ export const TextInvertedMedium: Story = {
globals: globalStoryPropsInverted,
args: {
...TextInvertedLarge.args,
size: 'Medium',
size: 'md',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -499,8 +529,7 @@ export const TextInvertedSmall: Story = {
globals: globalStoryPropsInverted,
args: {
...TextInvertedLarge.args,
typography: 'Body/Supporting text (caption)/smBold',
size: 'Small',
size: 'sm',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))
@@ -524,12 +553,8 @@ export const TextInvertedDisabled: Story = {
export const TextWithIcon: Story = {
args: {
...TextLarge.args,
children: (
<>
Text with icon
<MaterialIcon icon="chevron_right" size={24} color="CurrentColor" />
</>
),
children: 'Text with icon',
trailingIconName: 'chevron_right',
},
play: async ({ canvas, userEvent, args }) => {
await userEvent.click(await canvas.findByRole('button'))

View File

@@ -1,6 +1,8 @@
import { Button as ButtonRAC } from 'react-aria-components'
import { Loading, type LoadingProps } from '../Loading/Loading'
import { Loading } from '../Loading/Loading'
import { MaterialIcon } from '../Icons/MaterialIcon'
import { Typography } from '../Typography'
import type { ButtonProps } from './types'
import { variants } from './variants'
@@ -10,7 +12,8 @@ export function Button({
size,
wrapping,
fullWidth,
typography,
leadingIconName,
trailingIconName,
className,
children,
...props
@@ -20,32 +23,44 @@ export function Button({
color,
size,
wrapping,
typography,
fullWidth,
className,
})
return (
<ButtonRAC {...props} className={classNames}>
{({ isPending, isHovered }) => {
let loadingType: LoadingProps['type'] = 'White'
if (variant === 'Secondary') {
if (isHovered || color !== 'Inverted') {
loadingType = 'Dark'
}
} else {
if (color === 'Inverted') {
loadingType = 'Dark'
}
}
return (
<>
{children}
{isPending && <Loading size={20} type={loadingType} />}
</>
)
}}
</ButtonRAC>
<Typography
variant={
size === 'sm'
? 'Body/Supporting text (caption)/smBold'
: 'Body/Paragraph/mdBold'
}
>
<ButtonRAC {...props} className={classNames}>
{({ isPending }) => {
return (
<>
{leadingIconName && !isPending ? (
<MaterialIcon
icon={leadingIconName}
color="CurrentColor"
size={size === 'sm' ? 20 : 24}
/>
) : null}
{children}
{trailingIconName && !isPending ? (
<MaterialIcon
icon={trailingIconName}
color="CurrentColor"
size={size === 'sm' ? 20 : 24}
/>
) : null}
{isPending ? (
<Loading size={size === 'sm' ? 18 : 20} type="CurrentColor" />
) : null}
</>
)
}}
</ButtonRAC>
</Typography>
)
}

View File

@@ -37,15 +37,15 @@
}
}
.size-large {
.size-lg {
padding: calc(var(--Space-x2) - 2px) var(--Space-x3); /* Adjust for 2px border */
}
.size-medium {
.size-md {
padding: calc(var(--Space-x15) - 2px) var(--Space-x2); /* Adjust for 2px border */
}
.size-small {
.size-sm {
padding: var(--Space-x1) var(--Space-x2); /* Adjust for 2px border */
}

View File

@@ -3,8 +3,35 @@ import { Button } from 'react-aria-components'
import type { VariantProps } from 'class-variance-authority'
import type { ComponentProps } from 'react'
import type { SymbolCodepoints } from '../Icons/MaterialIcon/MaterialSymbol/types'
import type { variants } from './variants'
export const buttonIconNames = [
'add_circle',
'open_in_new',
'keyboard_arrow_down',
'keyboard_arrow_up',
'edit_square',
'location_on',
'link',
'mail',
'cancel',
'calendar_month',
'calendar_clock',
'edit_calendar',
'calendar_add_on',
'delete',
'chevron_right',
'chevron_left',
] as const
export type ButtonIconName = Extract<
SymbolCodepoints,
(typeof buttonIconNames)[number]
>
export interface ButtonProps
extends ComponentProps<typeof Button>,
VariantProps<typeof variants> {}
extends ComponentProps<typeof Button>, VariantProps<typeof variants> {
leadingIconName?: ButtonIconName | null
trailingIconName?: ButtonIconName | null
}

View File

@@ -1,9 +1,5 @@
import { cva } from 'class-variance-authority'
import {
config as typographyConfig,
withTypography,
} from '../Typography/variants'
import { deepmerge } from 'deepmerge-ts'
import styles from './button.module.css'
@@ -14,7 +10,6 @@ export const config = {
Primary: styles['variant-primary'],
Secondary: styles['variant-secondary'],
Tertiary: styles['variant-tertiary'],
Inverted: styles['variant-inverted'],
Text: styles['variant-text'],
},
color: {
@@ -22,9 +17,9 @@ export const config = {
Inverted: styles['color-inverted'],
},
size: {
Small: styles['size-small'],
Medium: styles['size-medium'],
Large: styles['size-large'],
sm: styles['size-sm'],
md: styles['size-md'],
lg: styles['size-lg'],
},
wrapping: {
true: undefined,
@@ -38,24 +33,21 @@ export const config = {
defaultVariants: {
variant: 'Primary',
color: 'Primary',
size: 'Large',
size: 'lg',
wrapping: true,
fullWidth: false,
},
} as const
const buttonConfig = {
variants: {
...config.variants,
typography: typographyConfig.variants.variant,
},
defaultVariants: {
...config.defaultVariants,
typography: 'Body/Paragraph/mdBold',
},
} as const
export const variants = cva(styles.button, withTypography(buttonConfig))
export const variants = cva(styles.button, buttonConfig)
export function withButton<T>(config: T) {
return deepmerge(buttonConfig, config)