diff --git a/docs/pages/components/Button.mdx b/docs/pages/components/Button.mdx index 95afb211d..4a461e275 100644 --- a/docs/pages/components/Button.mdx +++ b/docs/pages/components/Button.mdx @@ -82,6 +82,7 @@ When you need a semantic button with the visual appearance of a link. ```jsx example + ``` ### Badge @@ -132,6 +133,12 @@ When you need a semantic button with the visual appearance of a link. name: 'buttonRef', type: 'function', description: 'Ref function for the button element' + }, + { + name: 'size', + type: ['default', 'small'], + defaultValue: 'default', + description: 'Size of the button. Only applicable when variant is "tag".' } ]} /> diff --git a/docs/pages/components/TagButton.mdx b/docs/pages/components/TagButton.mdx index f7174c2db..194ea8802 100644 --- a/docs/pages/components/TagButton.mdx +++ b/docs/pages/components/TagButton.mdx @@ -19,6 +19,13 @@ import { TagButton } from '@deque/cauldron-react'; onClick={() => console.log('test')} icon="pencil" /> + console.log('test')} + icon="pencil" + size="small" +/> ``` ## Props @@ -63,6 +70,12 @@ import { TagButton } from '@deque/cauldron-react'; type: 'string', required: true, description: 'Icon to use for the TagButton' + }, + { + name: 'size', + type: ['default', 'small'], + defaultValue: 'default', + description: 'Size of the tag button.' } ]} /> diff --git a/e2e/screenshots/button-variant-tag-size-small-.png b/e2e/screenshots/button-variant-tag-size-small-.png new file mode 100644 index 000000000..fd7b41f4d Binary files /dev/null and b/e2e/screenshots/button-variant-tag-size-small-.png differ diff --git a/e2e/screenshots/dark--button-variant-tag-size-small-.png b/e2e/screenshots/dark--button-variant-tag-size-small-.png new file mode 100644 index 000000000..ae581ff43 Binary files /dev/null and b/e2e/screenshots/dark--button-variant-tag-size-small-.png differ diff --git a/e2e/screenshots/dark--tag-button-size-small-.png b/e2e/screenshots/dark--tag-button-size-small-.png new file mode 100644 index 000000000..06d56e4bd Binary files /dev/null and b/e2e/screenshots/dark--tag-button-size-small-.png differ diff --git a/e2e/screenshots/tag-button-size-small-.png b/e2e/screenshots/tag-button-size-small-.png new file mode 100644 index 000000000..80f4a4f0d Binary files /dev/null and b/e2e/screenshots/tag-button-size-small-.png differ diff --git a/packages/react/src/components/Button/index.test.tsx b/packages/react/src/components/Button/index.test.tsx index 2bc63ddff..ab0fa8fcd 100644 --- a/packages/react/src/components/Button/index.test.tsx +++ b/packages/react/src/components/Button/index.test.tsx @@ -62,6 +62,28 @@ test('should render button as tag', () => { expect(TagButton).toHaveClass('Tag'); }); +test('should render button as small tag', () => { + render( + + ); + const TagButton = screen.getByRole('button', { name: 'small tag' }); + expect(TagButton).toHaveClass('Tag'); + expect(TagButton).toHaveClass('Tag--small'); +}); + +test('should not apply Tag--small class to non-tag variants', () => { + render( + // @ts-expect-error size is not valid for non-tag variants + + ); + const button = screen.getByRole('button', { name: 'primary' }); + expect(button).not.toHaveClass('Tag--small'); +}); + test('should render button as badge', () => { render(); const BadgeButton = screen.getByRole('button', { name: 'badge' }); diff --git a/packages/react/src/components/Button/index.tsx b/packages/react/src/components/Button/index.tsx index 217034a25..2c7bf9ac7 100644 --- a/packages/react/src/components/Button/index.tsx +++ b/packages/react/src/components/Button/index.tsx @@ -1,8 +1,18 @@ import React, { type ButtonHTMLAttributes, forwardRef, type Ref } from 'react'; import classNames from 'classnames'; +import type { TagSize } from '../Tag'; -export interface ButtonProps extends ButtonHTMLAttributes { +interface ButtonBaseProps extends ButtonHTMLAttributes { buttonRef?: Ref; + thin?: boolean; +} + +interface ButtonTagProps extends ButtonBaseProps { + variant: 'tag'; + size?: TagSize; +} + +interface ButtonNonTagProps extends ButtonBaseProps { variant?: | 'primary' | 'secondary' @@ -11,16 +21,18 @@ export interface ButtonProps extends ButtonHTMLAttributes { | 'danger' | 'danger-secondary' | 'link' - | 'tag' | 'badge'; - thin?: boolean; + size?: never; } +export type ButtonProps = ButtonTagProps | ButtonNonTagProps; + const Button = forwardRef( ( { variant = 'primary', thin, + size, children, className, buttonRef, @@ -40,6 +52,7 @@ const Button = forwardRef( Link: variant === 'link', Tag: variant === 'tag', 'Button--tag': variant === 'tag', + 'Tag--small': variant === 'tag' && size === 'small', 'Button--thin': thin, 'Button--badge': variant === 'badge' })} diff --git a/packages/react/src/components/Button/screenshots.e2e.tsx b/packages/react/src/components/Button/screenshots.e2e.tsx index c3da10d82..2ff4ecfa4 100644 --- a/packages/react/src/components/Button/screenshots.e2e.tsx +++ b/packages/react/src/components/Button/screenshots.e2e.tsx @@ -20,12 +20,12 @@ test('should have screenshot for Button[variant="primary"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); await component - .getByText('Active') + .getByRole('button', { name: 'Active' }) .press('Space', { delay: 1000, noWaitAfter: true }); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[variant=primary]'); await setTheme(page, 'dark'); @@ -57,9 +57,9 @@ test('should have screenshot for Button[thin][variant="primary"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[thin][variant=primary]'); await setTheme(page, 'dark'); @@ -85,9 +85,9 @@ test('should have screenshot for Button[variant="secondary"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[variant=secondary]'); await setTheme(page, 'dark'); @@ -119,9 +119,9 @@ test('should have screenshot for Button[thin][variant="secondary"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[thin][secondary]'); await setTheme(page, 'dark'); @@ -145,9 +145,9 @@ test('should have screenshot for Button[variant="tertiary"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[variant=tertiary]'); await setTheme(page, 'dark'); @@ -179,9 +179,9 @@ test('should have screenshot for Button[thin][variant="tertiary"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[thin][tertiary]'); await setTheme(page, 'dark'); @@ -213,9 +213,9 @@ test('should have screenshot for Button with leading icon', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button-leading-icon'); await setTheme(page, 'dark'); @@ -248,9 +248,9 @@ test('should have screenshot for Button with trailing icon', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button-trailing-icon'); await setTheme(page, 'dark'); @@ -282,9 +282,9 @@ test('should have screenshot for Button[thin] with leading icon', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[thin]-leading-icon'); await setTheme(page, 'dark'); @@ -317,9 +317,9 @@ test('should have screenshot for Button[thin] with trailing icon', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[thin]-trailing-icon'); await setTheme(page, 'dark'); @@ -343,9 +343,9 @@ test('should have screenshot for Button[variant="error"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[variant=error]'); await setTheme(page, 'dark'); @@ -377,9 +377,9 @@ test('should have screenshot for Button[thin][variant="error"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[thin][variant=error]'); await setTheme(page, 'dark'); @@ -403,9 +403,9 @@ test('should have screenshot for Button[variant="danger"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[variant=danger]'); await setTheme(page, 'dark'); @@ -437,9 +437,9 @@ test('should have screenshot for Button[thin][variant="danger"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[thin][variant=danger]'); await setTheme(page, 'dark'); @@ -465,9 +465,9 @@ test('should have screenshot for Button[variant="danger-secondary"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[variant=danger-secondary]'); await setTheme(page, 'dark'); @@ -501,9 +501,9 @@ test('should have screenshot for Button[thin][variant="danger-secondary"]', asyn ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot( 'button[thin][variant=danger-secondary]' @@ -531,9 +531,9 @@ test('should have screenshot for Button[variant="link"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[variant=link]'); await setTheme(page, 'dark'); @@ -557,15 +557,51 @@ test('should have screenshot for Button[variant="tag"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[variant=tag]'); await setTheme(page, 'dark'); await expect(component).toHaveScreenshot('dark--button[variant=tag]'); }); +test('should have screenshot for Button[variant="tag"][size="small"]', async ({ + mount, + page +}) => { + const component = await mount( +
+ + + + + +
+ ); + + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); + + await expect(component).toHaveScreenshot('button[variant=tag][size=small]'); + await setTheme(page, 'dark'); + await expect(component).toHaveScreenshot( + 'dark--button[variant=tag][size=small]' + ); +}); + test('should have screenshot for Button[variant="badge"]', async ({ mount, page @@ -583,9 +619,9 @@ test('should have screenshot for Button[variant="badge"]', async ({ ); - await component.getByText('Hover').hover(); - setActive(component.getByText('Active')); - await component.getByText('Focus').focus(); + await component.getByRole('button', { name: 'Hover' }).hover(); + setActive(component.getByRole('button', { name: 'Active' })); + await component.getByRole('button', { name: 'Focus' }).focus(); await expect(component).toHaveScreenshot('button[variant=badge]'); await setTheme(page, 'dark'); diff --git a/packages/react/src/components/CopyButton/index.tsx b/packages/react/src/components/CopyButton/index.tsx index 88dbcca33..e82153fe9 100644 --- a/packages/react/src/components/CopyButton/index.tsx +++ b/packages/react/src/components/CopyButton/index.tsx @@ -11,8 +11,10 @@ import copyTextToClipboard from '../../utils/copyTextToClipboard'; type ButtonProps = React.ComponentProps; -export interface CopyButtonProps - extends Omit { +export interface CopyButtonProps extends Omit< + ButtonProps, + 'onCopy' | 'onClick' | 'size' +> { value: string; variant?: Extract< ButtonProps['variant'], diff --git a/packages/react/src/components/Tag/index.tsx b/packages/react/src/components/Tag/index.tsx index 0ab3404e6..c5da97143 100644 --- a/packages/react/src/components/Tag/index.tsx +++ b/packages/react/src/components/Tag/index.tsx @@ -1,10 +1,12 @@ import React from 'react'; import classNames from 'classnames'; +export type TagSize = 'default' | 'small'; + interface TagProps { children: React.ReactNode; className?: string; - size?: 'default' | 'small'; + size?: TagSize; } export const TagLabel = ({ children, className, ...other }: TagProps) => ( diff --git a/packages/react/src/components/TagButton/index.test.tsx b/packages/react/src/components/TagButton/index.test.tsx index 7201e5490..6c0f18a67 100644 --- a/packages/react/src/components/TagButton/index.test.tsx +++ b/packages/react/src/components/TagButton/index.test.tsx @@ -49,6 +49,12 @@ test('should render an icon in the button', () => { ); }); +test('should support size prop', () => { + renderDefaultTagButton({ size: 'small' }); + + expect(screen.getByRole('button')).toHaveClass('Tag--small'); +}); + test('returns no axe violations', async () => { const { container } = renderDefaultTagButton(); diff --git a/packages/react/src/components/TagButton/index.tsx b/packages/react/src/components/TagButton/index.tsx index 27d9291c0..8b7f41fbf 100644 --- a/packages/react/src/components/TagButton/index.tsx +++ b/packages/react/src/components/TagButton/index.tsx @@ -1,26 +1,31 @@ -import React, { Ref } from 'react'; +import React, { type ButtonHTMLAttributes, Ref } from 'react'; import Icon, { IconType } from '../Icon'; import { ContentNode } from '../../types'; -import { TagLabel } from '../Tag'; +import { TagLabel, type TagSize } from '../Tag'; import classNames from 'classnames'; import Button from '../Button'; -interface TagButtonProps extends React.HTMLAttributes { +interface TagButtonProps extends Omit< + ButtonHTMLAttributes, + 'value' +> { label: ContentNode; value: ContentNode; icon: IconType; onClick: (e: React.MouseEvent) => void; + size?: TagSize; } const TagButton = React.forwardRef( ( - { label, value, icon, className, ...rest }: TagButtonProps, + { label, value, icon, className, size, ...rest }: TagButtonProps, ref: Ref ) => { return (