diff --git a/.changeset/fix-cui-308-clickable-cards.md b/.changeset/fix-cui-308-clickable-cards.md new file mode 100644 index 000000000..628efc109 --- /dev/null +++ b/.changeset/fix-cui-308-clickable-cards.md @@ -0,0 +1,5 @@ +--- +'@clickhouse/click-ui': patch +--- + +`CardPrimary` and `CardSecondary` with `onClick` are exposed as buttons and activate with Enter and Space, unless the card renders its own `infoText` button (`CardPrimary`) or `infoUrl` link (`CardSecondary`); then the card gets no button role and that control keeps Enter and Space. A card that is not a button is no longer a Tab stop, on all three cards; display-only cards keep the default key behavior. A disabled `CardPrimary`, `CardSecondary` or `CardHorizontal` no longer calls `onClick`. Do not put links or buttons inside the title, description or children of a clickable card. diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 8995126cc..0c450b270 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -7,16 +7,6 @@ "count": 1 } }, - "src/components/CardPrimary/CardPrimary.tsx": { - "jsx-a11y/no-noninteractive-tabindex": { - "count": 1 - } - }, - "src/components/CardSecondary/CardSecondary.tsx": { - "jsx-a11y/no-noninteractive-tabindex": { - "count": 1 - } - }, "src/components/Collapsible/Collapsible.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 diff --git a/src/components/CardHorizontal/CardHorizontal.test.tsx b/src/components/CardHorizontal/CardHorizontal.test.tsx index 1a6d8e4f1..84065019c 100644 --- a/src/components/CardHorizontal/CardHorizontal.test.tsx +++ b/src/components/CardHorizontal/CardHorizontal.test.tsx @@ -123,6 +123,7 @@ describe('CardHorizontal Component', () => { const { container } = renderCard({ title: 'Test Card', disabled: true, + onClick: () => undefined, }); const wrapper = container.firstChild; @@ -133,12 +134,25 @@ describe('CardHorizontal Component', () => { const { container } = renderCard({ title: 'Test Card', disabled: false, + onClick: () => undefined, }); const wrapper = container.firstChild; expect(wrapper).toHaveAttribute('tabIndex', '0'); }); + it('should not be a Tab stop without a body action or with an inner button', () => { + const plain = renderCard({ title: 'Summary' }).container.firstChild; + expect(plain).not.toHaveAttribute('tabIndex'); + + const withButton = renderCard({ + title: 'Test Card', + infoText: 'Continue', + onButtonClick: () => undefined, + }).container.firstChild; + expect(withButton).not.toHaveAttribute('tabIndex'); + }); + it('should not call onClick when disabled', () => { const onClickMock = vitest.fn(); const { container } = renderCard({ @@ -225,6 +239,22 @@ describe('CardHorizontal Component', () => { expect(onButtonClick).toHaveBeenCalledTimes(1); }); + it('should not run onButtonClick on a body click when onClick is explicitly undefined', async () => { + const onButtonClick = vitest.fn(); + const { container, getByRole } = renderCard({ + title: 'Test Card', + infoText: 'Continue', + onClick: undefined, + onButtonClick, + }); + + fireEvent.click(container.firstChild as HTMLElement); + expect(onButtonClick).not.toHaveBeenCalled(); + + await userEvent.click(getByRole('button', { name: 'Continue' })); + expect(onButtonClick).toHaveBeenCalledTimes(1); + }); + it('should open infoUrl only once when the inner button is clicked', async () => { const windowOpenSpy = vitest.spyOn(window, 'open').mockImplementation(() => null); const { getByRole } = renderCard({ @@ -450,7 +480,11 @@ describe('CardHorizontal Component', () => { it("should call the consumer's onKeyDown", async () => { const onKeyDown = vitest.fn(); - const { container } = renderCard({ title: 'Test Card', onKeyDown }); + const { container } = renderCard({ + title: 'Test Card', + onClick: () => undefined, + onKeyDown, + }); (container.firstChild as HTMLElement).focus(); await userEvent.keyboard('{Enter}'); @@ -497,5 +531,14 @@ describe('CardHorizontal Component', () => { expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); + + it('should not run a consumer onClick on a disabled card', () => { + const onClick = vitest.fn(); + const { container } = renderCard({ title: 'Test Card', disabled: true, onClick }); + + fireEvent.click(container.firstChild as HTMLElement); + + expect(onClick).not.toHaveBeenCalled(); + }); }); }); diff --git a/src/components/CardHorizontal/CardHorizontal.tsx b/src/components/CardHorizontal/CardHorizontal.tsx index d6693ac86..e04ca5392 100644 --- a/src/components/CardHorizontal/CardHorizontal.tsx +++ b/src/components/CardHorizontal/CardHorizontal.tsx @@ -3,6 +3,7 @@ import { Button } from '@/components/Button'; import { Container } from '@/components/Container'; import { Icon } from '@/components/Icon'; import { cn, cva } from '@/lib/cva'; +import { clickOnActivationKey } from '@/utils/keyboard'; import { openInNewTab } from '@/utils/url'; import { CardHorizontalProps } from './CardHorizontal.types'; import styles from './CardHorizontal.module.css'; @@ -82,14 +83,12 @@ export const CardHorizontal = ({ onButtonClick, onKeyDown, className, - ...props + ...rest }: CardHorizontalProps) => { - const handleClick = (e: React.MouseEvent) => { - if (disabled) { - e.preventDefault(); - return; - } - + const { onClick, ...props } = rest; + // An explicit onClick={undefined} turns the card body off; consumers use it to keep only the inner button. + const hasOnClickProp = 'onClick' in rest; + const fireAction = (e: React.MouseEvent) => { if (typeof onButtonClick === 'function') { onButtonClick(e); } @@ -97,38 +96,39 @@ export const CardHorizontal = ({ openInNewTab(infoUrl); } }; + const handleClick = (e: React.MouseEvent) => { + if (disabled) { + e.preventDefault(); + return; + } + if (hasOnClickProp) { + onClick?.(e); + return; + } + fireAction(e); + }; const handleButtonClick = (e: React.MouseEvent) => { e.stopPropagation(); - handleClick(e); + if (disabled) { + e.preventDefault(); + return; + } + fireAction(e); }; - const isActionable = - typeof props.onClick === 'function' || - typeof onButtonClick === 'function' || - Boolean(infoUrl); + const isActionable = hasOnClickProp + ? typeof onClick === 'function' + : typeof onButtonClick === 'function' || Boolean(infoUrl); // With an inner info button, that button is the keyboard action, so the card leaves Enter and Space alone. const isButton = isActionable && !infoText; const handleKeyDown = (e: React.KeyboardEvent) => { - if (typeof onKeyDown === 'function') { - onKeyDown(e); - } - // Keys from nested controls bubble up here and must not activate the card. - const shouldSkipActivation = - !isButton || e.defaultPrevented || disabled || e.target !== e.currentTarget; - if (shouldSkipActivation) { - return; - } - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - // A held key repeats keydown, so only the first press activates the card. - if (!e.repeat) { - // Runs whichever onClick is attached. - e.currentTarget.click(); - } + onKeyDown?.(e); + if (isButton && !disabled) { + clickOnActivationKey(e); } }; return (
= { @@ -101,3 +102,20 @@ export const WithoutButton: Story = { alignContent: 'center', }, }; + +export const Keyboard: Story = { + args: { + ...WithoutButton.args, + onClick: fn(), + }, + play: async ({ canvasElement, args }) => { + const card = within(canvasElement).getByRole('button', { name: /Card title/ }); + + await userEvent.tab(); + await expect(card).toHaveFocus(); + + await userEvent.keyboard('{Enter}'); + await userEvent.keyboard(' '); + await expect(args.onClick).toHaveBeenCalledTimes(2); + }, +}; diff --git a/src/components/CardPrimary/CardPrimary.test.tsx b/src/components/CardPrimary/CardPrimary.test.tsx index b97f31199..6b729234d 100644 --- a/src/components/CardPrimary/CardPrimary.test.tsx +++ b/src/components/CardPrimary/CardPrimary.test.tsx @@ -1,4 +1,4 @@ -import { screen } from '@testing-library/react'; +import { fireEvent, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderCUI } from '@/utils/test-utils'; import { CardPrimary, CardPrimaryProps } from '@/components/CardPrimary'; @@ -170,4 +170,113 @@ describe('CardPrimary Component', () => { expect(warnSpy.mock.calls[0][0]).toContain('javascript:alert(1)'); }); }); + + describe('keyboard', () => { + const renderCard = (props: CardPrimaryProps) => renderCUI(); + const getCard = (container: HTMLElement) => + container.querySelector('[aria-disabled]') as HTMLElement; + + it.each(['{Enter}', ' '])( + 'should call onClick on "%s" and expose a button', + async key => { + const onClick = vitest.fn(); + const { container } = renderCard({ title: 'Test Card', onClick }); + + const card = screen.getByRole('button', { name: /Test Card/ }); + expect(card).toBe(getCard(container)); + card.focus(); + await userEvent.keyboard(key); + + expect(onClick).toHaveBeenCalledTimes(1); + } + ); + + it('should activate only once while Space is held and keep the page from scrolling', () => { + const onClick = vitest.fn(); + const { container } = renderCard({ title: 'Test Card', onClick }); + + const card = getCard(container); + expect(fireEvent.keyDown(card, { key: ' ' })).toBe(false); + expect(fireEvent.keyDown(card, { key: ' ', repeat: true })).toBe(false); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should leave other keys and keys from controls inside the card alone', () => { + const onClick = vitest.fn(); + const { container } = renderCard({ + title: 'Test Card', + onClick, + children: , + }); + + expect(fireEvent.keyDown(getCard(container), { key: 'Tab' })).toBe(true); + expect(fireEvent.keyDown(screen.getByRole('textbox'), { key: ' ' })).toBe(true); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('should run the consumer onKeyDown and still activate', () => { + const onClick = vitest.fn(); + const onKeyDown = vitest.fn(); + const { container } = renderCard({ title: 'Test Card', onClick, onKeyDown }); + + fireEvent.keyDown(getCard(container), { key: 'Enter' }); + + expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it.each([ + ['infoText only', { infoText: 'Go' }], + ['infoUrl without infoText', { infoUrl: 'https://example.com' }], + ['onButtonClick without infoText', { onButtonClick: vitest.fn() }], + ])('should be the only button when it has %s', (_name, extra) => { + const onClick = vitest.fn(); + renderCard({ title: 'Test Card', onClick, ...extra }); + + fireEvent.keyDown(screen.getByRole('button', { name: /Test Card/ }), { + key: 'Enter', + }); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should not run a consumer onClick on a disabled card', () => { + const onClick = vitest.fn(); + const { container } = renderCard({ title: 'Test Card', onClick, disabled: true }); + + fireEvent.click(getCard(container)); + + expect(onClick).not.toHaveBeenCalled(); + }); + + it('should leave the keys to the inner button and not be a button itself', () => { + const onClick = vitest.fn(); + const { container } = renderCard({ + title: 'Test Card', + infoText: 'Go', + onButtonClick: vitest.fn(), + onClick, + }); + + const card = getCard(container); + expect(card).not.toHaveAttribute('role'); + expect(fireEvent.keyDown(card, { key: 'Enter' })).toBe(true); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('should keep the default key behavior on a display-only or disabled card', () => { + const onClick = vitest.fn(); + const plain = getCard(renderCard({ title: 'Summary' }).container); + expect(plain).not.toHaveAttribute('role'); + expect(plain).not.toHaveAttribute('tabIndex'); + expect(fireEvent.keyDown(plain, { key: ' ' })).toBe(true); + + const disabled = getCard( + renderCard({ title: 'Test Card', onClick, disabled: true }).container + ); + fireEvent.keyDown(disabled, { key: 'Enter' }); + expect(onClick).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/components/CardPrimary/CardPrimary.tsx b/src/components/CardPrimary/CardPrimary.tsx index 942a5387e..5fde2122e 100644 --- a/src/components/CardPrimary/CardPrimary.tsx +++ b/src/components/CardPrimary/CardPrimary.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/Button'; import { Icon } from '@/components/Icon'; import { Spacer } from '@/components/Spacer'; import { cn, cva } from '@/lib/cva'; +import { clickOnActivationKey } from '@/utils/keyboard'; import { openInNewTab } from '@/utils/url'; import { CardPrimaryProps } from './CardPrimary.types'; import styles from './CardPrimary.module.css'; @@ -95,6 +96,8 @@ const Card = ({ onButtonClick, isSelected, children, + onClick, + onKeyDown, className, ...props }: CardPrimaryProps) => { @@ -106,12 +109,31 @@ const Card = ({ openInNewTab(infoUrl); } }; + const handleCardClick = (e: React.MouseEvent) => { + if (disabled) { + e.preventDefault(); + return; + } + onClick?.(e); + }; - const Component = !!infoUrl || typeof onButtonClick === 'function' ? Button : 'div'; + const hasInnerButton = !!infoUrl || typeof onButtonClick === 'function'; + const Component = hasInnerButton ? Button : 'div'; + // With an inner info button, that button is the keyboard action, so the card leaves Enter and Space alone. + const isButton = typeof onClick === 'function' && !(infoText && hasInnerButton); + const handleKeyDown = (e: React.KeyboardEvent) => { + onKeyDown?.(e); + if (isButton && !disabled) { + clickOnActivationKey(e); + } + }; return (
= { @@ -63,3 +64,23 @@ export const Disabled: Story = { disabled: true, }, }; + +export const Keyboard: Story = { + args: { + title: baseArgs.title, + icon: baseArgs.icon, + description: baseArgs.description, + infoText: baseArgs.infoText, + onClick: fn(), + }, + play: async ({ canvasElement, args }) => { + const card = within(canvasElement).getByRole('button', { name: /Card title/ }); + + await userEvent.tab(); + await expect(card).toHaveFocus(); + + await userEvent.keyboard('{Enter}'); + await userEvent.keyboard(' '); + await expect(args.onClick).toHaveBeenCalledTimes(2); + }, +}; diff --git a/src/components/CardSecondary/CardSecondary.test.tsx b/src/components/CardSecondary/CardSecondary.test.tsx index e04e17b74..9f2fd82a2 100644 --- a/src/components/CardSecondary/CardSecondary.test.tsx +++ b/src/components/CardSecondary/CardSecondary.test.tsx @@ -1,4 +1,5 @@ -import { screen } from '@testing-library/react'; +import { fireEvent, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { CardSecondary, CardSecondaryProps } from '@/components/CardSecondary'; import { renderCUI } from '@/utils/test-utils'; @@ -97,4 +98,129 @@ describe('CardSecondary Component', () => { expect(screen.getAllByLabelText('chevron-right').length).toEqual(1); }); + + describe('keyboard', () => { + const getCard = (container: HTMLElement) => container.firstChild as HTMLElement; + + it.each(['{Enter}', ' '])( + 'should call onClick on "%s" and expose a button', + async key => { + const onClick = vitest.fn(); + const { container } = renderCard({ + title: 'Test Card', + description: 'Desc', + onClick, + }); + + const card = screen.getByRole('button', { name: /Test Card/ }); + expect(card).toBe(getCard(container)); + card.focus(); + await userEvent.keyboard(key); + + expect(onClick).toHaveBeenCalledTimes(1); + } + ); + + it('should run the consumer onKeyDown and still activate', () => { + const onClick = vitest.fn(); + const onKeyDown = vitest.fn(); + const { container } = renderCard({ + title: 'Test Card', + description: 'Desc', + onClick, + onKeyDown, + }); + + fireEvent.keyDown(getCard(container), { key: 'Enter' }); + + expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should stay a link, not a button, with infoUrl and no infoText', () => { + const { container } = renderCard({ + title: 'Test Card', + description: 'Desc', + infoUrl: 'https://example.com', + onClick: vitest.fn(), + }); + + expect(getCard(container)).not.toHaveAttribute('role'); + expect(screen.getByRole('link')).toBeInTheDocument(); + }); + + it('should hide the info icon from the button name', () => { + renderCard({ + title: 'Test Card', + description: 'Desc', + infoText: 'Read more', + onClick: vitest.fn(), + }); + + expect(screen.getByRole('button')).toHaveAccessibleName('Test Card Desc Read more'); + }); + + it('should not run a consumer onClick on a disabled card', () => { + const onClick = vitest.fn(); + const { container } = renderCard({ + title: 'Test Card', + description: 'Desc', + onClick, + disabled: true, + }); + + fireEvent.click(getCard(container)); + + expect(onClick).not.toHaveBeenCalled(); + }); + + it('should activate only once while Space is held and keep the page from scrolling', () => { + const onClick = vitest.fn(); + const { container } = renderCard({ + title: 'Test Card', + description: 'Desc', + onClick, + }); + + const card = getCard(container); + expect(fireEvent.keyDown(card, { key: ' ' })).toBe(false); + expect(fireEvent.keyDown(card, { key: ' ', repeat: true })).toBe(false); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should leave the keys to the info link and not be a button itself', () => { + const onClick = vitest.fn(); + const { container } = renderCard({ + title: 'Test Card', + description: 'Desc', + infoUrl: 'https://example.com', + infoText: 'Read more', + onClick, + }); + + const card = getCard(container); + expect(card).not.toHaveAttribute('role'); + expect(screen.getByRole('link')).toBeInTheDocument(); + expect(fireEvent.keyDown(card, { key: 'Enter' })).toBe(true); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('should keep the default key behavior on a display-only or disabled card', () => { + const onClick = vitest.fn(); + const plain = getCard( + renderCard({ title: 'Summary', description: 'Desc' }).container + ); + expect(plain).not.toHaveAttribute('role'); + expect(plain).not.toHaveAttribute('tabIndex'); + expect(fireEvent.keyDown(plain, { key: ' ' })).toBe(true); + + const disabled = getCard( + renderCard({ title: 'Test Card', description: 'Desc', onClick, disabled: true }) + .container + ); + fireEvent.keyDown(disabled, { key: 'Enter' }); + expect(onClick).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/components/CardSecondary/CardSecondary.tsx b/src/components/CardSecondary/CardSecondary.tsx index 31007d122..d77c63072 100644 --- a/src/components/CardSecondary/CardSecondary.tsx +++ b/src/components/CardSecondary/CardSecondary.tsx @@ -3,6 +3,7 @@ import { Icon } from '@/components/Icon'; import { Title } from '@/components/Title'; import { Text } from '@/components/Text'; import { cn, cva } from '@/lib/cva'; +import { clickOnActivationKey } from '@/utils/keyboard'; import { CardSecondaryProps } from './CardSecondary.types'; import styles from './CardSecondary.module.css'; @@ -35,15 +36,35 @@ export const CardSecondary = ({ infoText, infoIcon = 'chevron-right', infoAssetSize = 'md', + onClick, + onKeyDown, className, ...props }: CardSecondaryProps) => { const isInfoLink = !disabled && !!infoUrl && infoUrl.length > 0; const InfoComponent = isInfoLink ? 'a' : 'div'; + // With an info link, that link is the keyboard action, so the card leaves Enter and Space alone. + const isButton = typeof onClick === 'function' && !infoUrl; + const handleCardClick = (e: React.MouseEvent) => { + if (disabled) { + e.preventDefault(); + return; + } + onClick?.(e); + }; + const handleKeyDown = (e: React.KeyboardEvent) => { + onKeyDown?.(e); + if (isButton && !disabled) { + clickOnActivationKey(e); + } + }; return (
@@ -88,6 +109,7 @@ export const CardSecondary = ({ className={styles.infolink__icon} size={infoAssetSize} name={infoIcon} + aria-hidden={infoText ? true : undefined} /> )} diff --git a/src/utils/keyboard.ts b/src/utils/keyboard.ts new file mode 100644 index 000000000..3c2580285 --- /dev/null +++ b/src/utils/keyboard.ts @@ -0,0 +1,16 @@ +import type { KeyboardEvent } from 'react'; + +/** Enter and Space click the element itself, once per press; keys from a focusable descendant are left alone. */ +export const clickOnActivationKey = (event: KeyboardEvent): void => { + if (event.defaultPrevented || event.target !== event.currentTarget) { + return; + } + if (event.key !== 'Enter' && event.key !== ' ') { + return; + } + event.preventDefault(); + if (event.repeat) { + return; + } + event.currentTarget.click(); +}; diff --git a/tests/cards/cardprimary.spec.ts b/tests/cards/cardprimary.spec.ts index 3b7b63522..e2e520962 100644 --- a/tests/cards/cardprimary.spec.ts +++ b/tests/cards/cardprimary.spec.ts @@ -153,7 +153,8 @@ describe('CardPrimary Visual Regression', () => { }); const card = page.locator(cardLocator).first(); await expect(card).toBeVisible({ timeout: 10000 }); - await card.focus(); + await page.locator('body').click(); + await page.keyboard.press('Tab'); await page.waitForTimeout(100); await expect(card).toHaveScreenshot('cardprimary-focus-light.png', { maxDiffPixels: 100, @@ -308,7 +309,8 @@ describe('CardPrimary Visual Regression', () => { }); const card = page.locator(cardLocator).first(); await expect(card).toBeVisible({ timeout: 10000 }); - await card.focus(); + await page.locator('body').click(); + await page.keyboard.press('Tab'); await page.waitForTimeout(100); await expect(card).toHaveScreenshot('cardprimary-focus-dark.png', { maxDiffPixels: 100, @@ -319,11 +321,12 @@ describe('CardPrimary Visual Regression', () => { describe('Accessibility', () => { it('card is focusable via Tab', async ({ page }) => { - await page.goto(getStoryUrl('cards-primary-card--default', 'light'), { + await page.goto(getStoryUrl('cards-primary-card--keyboard', 'light'), { waitUntil: 'networkidle', }); const card = page.locator(cardLocator).first(); await expect(card).toBeVisible({ timeout: 10000 }); + await expect(card).toHaveAttribute('role', 'button'); await page.locator('body').click(); await page.keyboard.press('Tab'); diff --git a/tests/cards/cardprimary.spec.ts-snapshots/cardprimary-focus-dark-chromium-linux.png b/tests/cards/cardprimary.spec.ts-snapshots/cardprimary-focus-dark-chromium-linux.png index bcbba1f64..2fb366b76 100644 Binary files a/tests/cards/cardprimary.spec.ts-snapshots/cardprimary-focus-dark-chromium-linux.png and b/tests/cards/cardprimary.spec.ts-snapshots/cardprimary-focus-dark-chromium-linux.png differ diff --git a/tests/cards/cardprimary.spec.ts-snapshots/cardprimary-focus-light-chromium-linux.png b/tests/cards/cardprimary.spec.ts-snapshots/cardprimary-focus-light-chromium-linux.png index 086f544c3..51efa14ca 100644 Binary files a/tests/cards/cardprimary.spec.ts-snapshots/cardprimary-focus-light-chromium-linux.png and b/tests/cards/cardprimary.spec.ts-snapshots/cardprimary-focus-light-chromium-linux.png differ diff --git a/tests/cards/cardsecondary.spec.ts b/tests/cards/cardsecondary.spec.ts index 1262b4302..9bbbd10e7 100644 --- a/tests/cards/cardsecondary.spec.ts +++ b/tests/cards/cardsecondary.spec.ts @@ -88,7 +88,8 @@ describe('CardSecondary Visual Regression', () => { }); const card = page.locator(cardLocator).first(); await expect(card).toBeVisible({ timeout: 10000 }); - await card.focus(); + await page.locator('body').click(); + await page.keyboard.press('Tab'); await page.waitForTimeout(100); await expect(card).toHaveScreenshot('cardsecondary-focus-light.png', { maxDiffPixels: 100, @@ -178,7 +179,8 @@ describe('CardSecondary Visual Regression', () => { }); const card = page.locator(cardLocator).first(); await expect(card).toBeVisible({ timeout: 10000 }); - await card.focus(); + await page.locator('body').click(); + await page.keyboard.press('Tab'); await page.waitForTimeout(100); await expect(card).toHaveScreenshot('cardsecondary-focus-dark.png', { maxDiffPixels: 100, @@ -189,11 +191,12 @@ describe('CardSecondary Visual Regression', () => { describe('Accessibility', () => { it('enabled card is focusable via Tab', async ({ page }) => { - await page.goto(getStoryUrl('cards-secondary-card--default', 'light'), { + await page.goto(getStoryUrl('cards-secondary-card--keyboard', 'light'), { waitUntil: 'networkidle', }); const card = page.locator(cardLocator).first(); await expect(card).toBeVisible({ timeout: 10000 }); + await expect(card).toHaveAttribute('role', 'button'); await page.locator('body').click(); await page.keyboard.press('Tab'); diff --git a/tests/cards/cardsecondary.spec.ts-snapshots/cardsecondary-focus-dark-chromium-linux.png b/tests/cards/cardsecondary.spec.ts-snapshots/cardsecondary-focus-dark-chromium-linux.png index 8c4fe1f33..7d1e5b48d 100644 Binary files a/tests/cards/cardsecondary.spec.ts-snapshots/cardsecondary-focus-dark-chromium-linux.png and b/tests/cards/cardsecondary.spec.ts-snapshots/cardsecondary-focus-dark-chromium-linux.png differ diff --git a/tests/cards/cardsecondary.spec.ts-snapshots/cardsecondary-focus-light-chromium-linux.png b/tests/cards/cardsecondary.spec.ts-snapshots/cardsecondary-focus-light-chromium-linux.png index d0297ffd3..ee814c329 100644 Binary files a/tests/cards/cardsecondary.spec.ts-snapshots/cardsecondary-focus-light-chromium-linux.png and b/tests/cards/cardsecondary.spec.ts-snapshots/cardsecondary-focus-light-chromium-linux.png differ