diff --git a/e2e/components/ProductiveCard/ProductiveCard-test.avt.e2e.js b/e2e/components/ProductiveCard/ProductiveCard-test.avt.e2e.js index 3eb31ac10e..b477b8467e 100644 --- a/e2e/components/ProductiveCard/ProductiveCard-test.avt.e2e.js +++ b/e2e/components/ProductiveCard/ProductiveCard-test.avt.e2e.js @@ -9,6 +9,7 @@ import { expect, test } from '@playwright/test'; import { visitStory } from '../../test-utils/storybook'; +import { carbon, pkg } from '../../../packages/ibm-products/src/settings'; test.describe('ProductiveCard @avt', () => { test('@avt-default-state', async ({ page }) => { @@ -23,4 +24,253 @@ test.describe('ProductiveCard @avt', () => { 'ProductiveCard @avt-default-state' ); }); + + test('@avt-with-caption', async ({ page }) => { + await visitStory(page, { + component: 'ProductiveCard', + id: 'ibm-products-components-cards-productivecard--with-caption', + globals: { + carbonTheme: 'white', + }, + }); + await expect(page).toHaveNoACViolations('ProductiveCard @avt-with-caption'); + }); + + // Disabled state test + test('@avt-disabled: validates disabled button state', async ({ page }) => { + await visitStory(page, { + component: 'ProductiveCard', + id: 'ibm-products-components-cards-productivecard--with-action-ghost-button', + }); + + await expect(page).toHaveNoACViolations('ProductiveCard @avt-disabled'); + const editButton = page.getByRole('button', { name: 'Edit' }); + const deleteButton = page.getByRole('button', { name: 'Delete' }); + const disabledButton = page.getByRole('button', { name: 'Read more' }); + expect(disabledButton.getAttribute('disabled')).not.toBeNull(); + + await page.keyboard.press('Tab'); + expect(editButton).toBeFocused(); + + await page.keyboard.press('Tab'); + expect(deleteButton).toBeFocused(); + // disabled button + await page.keyboard.press('Tab'); + expect( + await disabledButton.evaluate((btn) => document.activeElement !== btn) + ).toBe(true); + }); + + // Overflow menu open/close states test + test('@avt-overflow-menu: validates overflow menu interactions', async ({ + page, + }) => { + await visitStory(page, { + component: 'ProductiveCard', + id: 'ibm-products-components-cards-productivecard--with-overflow', + }); + + const menuButton = page.getByRole('button', { label: 'Option' }); + const menu = page.getByRole('menu'); + + // Check initial state + expect(await menuButton.getAttribute('aria-expanded')).toBe('false'); + + // Open the menu + await menuButton.click(); + + // Wait for menu to be visible + await expect(menu).toBeVisible(); + + expect(await menuButton.getAttribute('aria-expanded')).toBe('true'); + await expect(page).toHaveNoACViolations('ProductiveCard @menu-open'); + + // Close the menu with Escape + await page.keyboard.press('Escape'); + await expect(menu).not.toBeVisible(); + + expect(await menuButton.getAttribute('aria-expanded')).toBe('false'); + await expect(page).toHaveNoACViolations('ProductiveCard @menu-closed'); + + // Reopen the menu via keyboard + await page.keyboard.press('Tab'); + expect( + await menuButton.evaluate((btn) => document.activeElement === btn) + ).toBe(true); + + await page.keyboard.press('Enter'); + await expect(menu).toBeVisible(); + + // Check menu item count and focus + const menuItems = page.locator(`li.${carbon.prefix}--menu-item`); + expect(await menuItems.count()).toBeGreaterThan(0); + expect( + await menuItems.first().evaluate((btn) => document.activeElement === btn) + ).toBe(true); + expect(await menuButton.getAttribute('aria-expanded')).toBe('true'); + + // Ensure the menu is closed when pressing Escape + await page.keyboard.press('Escape'); + // Focus returns to menu button + expect( + await menuButton.evaluate((btn) => document.activeElement === btn) + ).toBe(true); + + // Check final state + await expect(menu).not.toBeVisible(); + }); + + test('@avt-keyboard: validates keyboard navigation for all interactive elements', async ({ + page, + }) => { + // Navigate to the "Supplemental Bottom Bar" story for ProductiveCard, that has all interactive elements + await visitStory(page, { + component: 'ProductiveCard', + id: 'ibm-products-components-cards-productivecard--supplemental-bottom-bar', + }); + + // Ensure no accessibility violations for the story + await expect(page).toHaveNoACViolations( + 'ProductiveCard @keyboard-navigation - Supplemental Bottom Bar' + ); + + // Move focus to the Edit button and validate + await page.keyboard.press('Tab'); + const editButton = page.getByLabel('Edit'); + await expect(editButton).toBeVisible(); + await expect(editButton).toBeFocused(); + await expect(page).toHaveNoACViolations( + 'ProductiveCard @keyboard-navigation - Edit Button' + ); + + // Move focus to the Delete button and validate + await page.keyboard.press('Tab'); + const deleteButton = page.getByLabel('Delete'); + await expect(deleteButton).toBeVisible(); + await expect(deleteButton).toBeFocused(); + await expect(page).toHaveNoACViolations( + 'ProductiveCard @keyboard-navigation - Delete Button' + ); + + // Move focus to the Read more button and validate + await page.keyboard.press('Tab'); + const readMoreButton1 = page.getByText('Read more'); + await expect(readMoreButton1).toBeVisible(); + await expect(readMoreButton1).toBeFocused(); + await expect(page).toHaveNoACViolations( + 'ProductiveCard @keyboard-navigation - Read more Button' + ); + + // Tab Navigation in "Clickable Card" story for ProductiveCard, (zone one is default, whole card recieves focus) + await visitStory(page, { + component: 'ProductiveCard', + id: 'ibm-products-components-cards-productivecard--clickable', + }); + + // Ensure no accessibility violations for the story + await expect(page).toHaveNoACViolations( + 'ProductiveCard @keyboard-navigation - Clickable Card' + ); + + // Move focus to the card element and validate + await page.keyboard.press('Tab'); + const zone1 = page.locator(`.${pkg.prefix}--card__clickable`); + await expect(zone1).toBeFocused(); + await expect(zone1).toHaveAttribute('role', 'button'); + + // Move focus to the Read more button and validate + await page.keyboard.press('Tab'); + const readMoreButton2 = page.getByText('Read more'); + await expect(readMoreButton2).toBeVisible(); + await expect(readMoreButton2).toBeFocused(); + + // Validate zone two focus + await visitStory(page, { + component: 'ProductiveCard', + id: 'ibm-products-components-cards-productivecard--clickable&args=clickZone:two', + }); + await page.keyboard.press('Tab'); + + const zone2 = page.locator(`.${pkg.prefix}--card__header-body-container`); + await expect(zone2).toBeFocused(); + await expect(zone2).toHaveAttribute('role', 'button'); + + // Move focus to the Read more button and validate + await page.keyboard.press('Tab'); + const readMoreButton3 = page.getByText('Read more'); + await expect(readMoreButton3).toBeVisible(); + await expect(readMoreButton3).toBeFocused(); + + // Validate zone three focus + await visitStory(page, { + component: 'ProductiveCard', + id: 'ibm-products-components-cards-productivecard--clickable&args=clickZone:three', + }); + await page.keyboard.press('Tab'); + const zone3 = page.locator(`.${pkg.prefix}--card__body`); + await expect(zone3).toBeFocused(); + await expect(zone3).toHaveAttribute('role', 'button'); + + // Move focus to the Read more button and validate + await page.keyboard.press('Tab'); + const readMoreButton4 = page.getByText('Read more'); + await expect(readMoreButton4).toBeVisible(); + await expect(readMoreButton4).toBeFocused(); + + // Navigate to the "button with href" story for ProductiveCard + await visitStory(page, { + component: 'ProductiveCard', + id: 'ibm-products-components-cards-productivecard--with-button-href', + }); + + // Ensure no accessibility violations for the story + await expect(page).toHaveNoACViolations( + 'ProductiveCard @keyboard-navigation - button with href' + ); + + // Move focus to the href button and validate + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + const hrefButton = page.getByText('Read more'); + await expect(hrefButton).toHaveAttribute('href', '#'); + await expect(hrefButton).toBeVisible(); + await expect(hrefButton).toBeFocused(); + await expect(page).toHaveNoACViolations( + 'ProductiveCard @keyboard-navigation - href Button' + ); + }); + + // hover states + test('@avt-hover: validates hover states', async ({ page }) => { + await visitStory(page, { + component: 'ProductiveCard', + id: 'ibm-products-components-cards-productivecard--with-overflow', + }); + const menuButton = page.getByRole('button', { label: 'Overflow menu' }); + const tooltip = page.getByRole('tooltip', { name: 'Overflow menu' }); + + await menuButton.hover(); + await expect(page).toHaveNoACViolations( + 'ProductiveCard @hover - with overflow' + ); + await expect(tooltip).toBeVisible(); + + await visitStory(page, { + component: 'ProductiveCard', + id: 'ibm-products-components-cards-productivecard--default', + }); + const editButton = page.getByLabel('Edit'); + const editTooltip = page.getByRole('tooltip', { name: 'Edit' }); + const deleteButton = page.getByLabel('Delete'); + const deleteTooltip = page.getByRole('tooltip', { name: 'Delete' }); + + await editButton.hover(); + await expect(page).toHaveNoACViolations('ProductiveCard @hover - default'); + await expect(editTooltip).toBeVisible(); + + await deleteButton.hover(); + await expect(page).toHaveNoACViolations('ProductiveCard @hover - default'); + await expect(deleteTooltip).toBeVisible(); + }); }); diff --git a/packages/ibm-products/src/components/Card/Card.tsx b/packages/ibm-products/src/components/Card/Card.tsx index f26e51bba2..ad2d9504c1 100644 --- a/packages/ibm-products/src/components/Card/Card.tsx +++ b/packages/ibm-products/src/components/Card/Card.tsx @@ -118,9 +118,9 @@ export const Card = forwardRef( onClick, onKeyDown, onPrimaryButtonClick, + onSecondaryButtonClick, overflowActions = Object.freeze([]), overflowAriaLabel, - onSecondaryButtonClick, pictogram: Pictogram, primaryButtonDisabled, primaryButtonHref, @@ -179,8 +179,7 @@ export const Card = forwardRef( autoAlign menuAlignment={pos} size={size} - aria-label={overflowAriaLabel} - label={iconDescription} + label={overflowAriaLabel || iconDescription} > {overflowActions.map(({ id, itemText, ...rest }) => ( diff --git a/packages/ibm-products/src/components/ProductiveCard/ProductiveCard.tsx b/packages/ibm-products/src/components/ProductiveCard/ProductiveCard.tsx index c1bd9dccaf..57edc3de02 100644 --- a/packages/ibm-products/src/components/ProductiveCard/ProductiveCard.tsx +++ b/packages/ibm-products/src/components/ProductiveCard/ProductiveCard.tsx @@ -92,7 +92,8 @@ export interface ProductiveCardProps extends PropsWithChildren { */ overflowActions?: overflowAction[]; /** - * Aria label prop required for OverflowMenu + * Sets the text for the OverflowMenu aria label and the OverflowMenu trigger button tooltip. + * Overrides `iconDescription` prop. */ overflowAriaLabel?: string; /** @@ -149,14 +150,17 @@ export interface ProductiveCardProps extends PropsWithChildren { titleSize?: 'default' | 'large'; /** - * Tooltip icon description + * Sets the text for the OverflowMenu trigger button tooltip and OverflowMenu aria label, + * gets overridden by the `overflowAriaLabel` prop. + * + * @deprecated Please use the `overflowAriaLabel` prop instead. */ iconDescription?: string; } export let ProductiveCard = forwardRef( ( - { actionsPlacement = 'top', iconDescription, ...rest }: ProductiveCardProps, + { actionsPlacement = 'top', ...rest }: ProductiveCardProps, ref: ForwardedRef ) => { const validProps = prepareProps(rest, [ @@ -171,7 +175,6 @@ export let ProductiveCard = forwardRef(