From dd60a2e0e8551c8c1a4992fec7dc7c7f26ff6d53 Mon Sep 17 00:00:00 2001
From: Nandan Devadula <47176249+devadula-nandan@users.noreply.github.com>
Date: Wed, 11 Dec 2024 16:46:23 +0530
Subject: [PATCH 1/7] test(ProductiveCard): add avt test coverage (#6518)
* test(ProductiveCard): add avt test coverage
* chore: docs update
* test: add tests for all clickable zones and test focus
* fix: prefix
* fix: pr comment changes
* chore: add deprecation notice for iconDescription
* test: add mouse hover states
---
.../ProductiveCard-test.avt.e2e.js | 250 ++++++++++++++++++
.../ibm-products/src/components/Card/Card.tsx | 5 +-
.../ProductiveCard/ProductiveCard.tsx | 19 +-
3 files changed, 265 insertions(+), 9 deletions(-)
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(
Date: Thu, 12 Dec 2024 01:08:16 +0530
Subject: [PATCH 2/7] chore(HTTPErrors): include codemod run command (#6563)
* refactor(useFocus): refactor repeated useEffect code
* chore(HTTPError): include codemod run command
---
.../HTTPErrors/HTTPError403/HTTPError403.stories.jsx | 7 +++++--
.../components/HTTPErrors/HTTPError403/HTTPError403.tsx | 2 +-
.../HTTPErrors/HTTPError404/HTTPError404.stories.jsx | 7 +++++--
.../components/HTTPErrors/HTTPError404/HTTPError404.tsx | 2 +-
.../HTTPErrors/HTTPErrorOther/HTTPErrorOther.stories.jsx | 7 +++++--
.../HTTPErrors/HTTPErrorOther/HTTPErrorOther.tsx | 2 +-
6 files changed, 18 insertions(+), 9 deletions(-)
diff --git a/packages/ibm-products/src/components/HTTPErrors/HTTPError403/HTTPError403.stories.jsx b/packages/ibm-products/src/components/HTTPErrors/HTTPError403/HTTPError403.stories.jsx
index 570d12f0b1..0e546fc78f 100644
--- a/packages/ibm-products/src/components/HTTPErrors/HTTPError403/HTTPError403.stories.jsx
+++ b/packages/ibm-products/src/components/HTTPErrors/HTTPError403/HTTPError403.stories.jsx
@@ -38,8 +38,11 @@ const Template = (args) => {
version. Please migrate to{' '}
FullPageError
-
- .
+ {' '}
+ by running{' '}
+
+ npx @carbon/upgrade migrate ibm-products-update-http-errors --write
+
}
>
diff --git a/packages/ibm-products/src/components/HTTPErrors/HTTPError403/HTTPError403.tsx b/packages/ibm-products/src/components/HTTPErrors/HTTPError403/HTTPError403.tsx
index 79b62be23f..86eff4c912 100644
--- a/packages/ibm-products/src/components/HTTPErrors/HTTPError403/HTTPError403.tsx
+++ b/packages/ibm-products/src/components/HTTPErrors/HTTPError403/HTTPError403.tsx
@@ -86,7 +86,7 @@ export let HTTPError403 = React.forwardRef(
/**@ts-ignore*/
HTTPError403.deprecated = {
level: 'warn',
- details: `Please replace ${componentName} with FullPageError`,
+ details: `${componentName} is deprecated. Please migrate to FullPageError by running npx @carbon/upgrade migrate ibm-products-update-http-errors --write`,
};
// Return a placeholder if not released and not enabled by feature flag
diff --git a/packages/ibm-products/src/components/HTTPErrors/HTTPError404/HTTPError404.stories.jsx b/packages/ibm-products/src/components/HTTPErrors/HTTPError404/HTTPError404.stories.jsx
index 88af50aaae..af62418445 100644
--- a/packages/ibm-products/src/components/HTTPErrors/HTTPError404/HTTPError404.stories.jsx
+++ b/packages/ibm-products/src/components/HTTPErrors/HTTPError404/HTTPError404.stories.jsx
@@ -45,8 +45,11 @@ const Template = (args) => {
version. Please migrate to{' '}
FullPageError
-
- .
+ {' '}
+ by running{' '}
+
+ npx @carbon/upgrade migrate ibm-products-update-http-errors --write
+
}
>
diff --git a/packages/ibm-products/src/components/HTTPErrors/HTTPError404/HTTPError404.tsx b/packages/ibm-products/src/components/HTTPErrors/HTTPError404/HTTPError404.tsx
index 9a3d980936..c44b083c9e 100644
--- a/packages/ibm-products/src/components/HTTPErrors/HTTPError404/HTTPError404.tsx
+++ b/packages/ibm-products/src/components/HTTPErrors/HTTPError404/HTTPError404.tsx
@@ -76,7 +76,7 @@ export let HTTPError404 = React.forwardRef(
/**@ts-ignore*/
HTTPError404.deprecated = {
level: 'warn',
- details: `Please replace ${componentName} with FullPageError`,
+ details: `${componentName} is deprecated. Please migrate to FullPageError by running npx @carbon/upgrade migrate ibm-products-update-http-errors --write`,
};
// Return a placeholder if not released and not enabled by feature flag
diff --git a/packages/ibm-products/src/components/HTTPErrors/HTTPErrorOther/HTTPErrorOther.stories.jsx b/packages/ibm-products/src/components/HTTPErrors/HTTPErrorOther/HTTPErrorOther.stories.jsx
index 2c6160e919..f47e27fc7f 100644
--- a/packages/ibm-products/src/components/HTTPErrors/HTTPErrorOther/HTTPErrorOther.stories.jsx
+++ b/packages/ibm-products/src/components/HTTPErrors/HTTPErrorOther/HTTPErrorOther.stories.jsx
@@ -45,8 +45,11 @@ const Template = (args) => {
version. Please migrate to{' '}
FullPageError
-
- .
+ {' '}
+ by running{' '}
+
+ npx @carbon/upgrade migrate ibm-products-update-http-errors --write
+
}
>
diff --git a/packages/ibm-products/src/components/HTTPErrors/HTTPErrorOther/HTTPErrorOther.tsx b/packages/ibm-products/src/components/HTTPErrors/HTTPErrorOther/HTTPErrorOther.tsx
index c8d8fa3c6f..533b9687a6 100644
--- a/packages/ibm-products/src/components/HTTPErrors/HTTPErrorOther/HTTPErrorOther.tsx
+++ b/packages/ibm-products/src/components/HTTPErrors/HTTPErrorOther/HTTPErrorOther.tsx
@@ -86,7 +86,7 @@ export let HTTPErrorOther = React.forwardRef(
/**@ts-ignore*/
HTTPErrorOther.deprecated = {
level: 'warn',
- details: `Please replace ${componentName} with FullPageError`,
+ details: `${componentName} is deprecated. Please migrate to FullPageError by running npx @carbon/upgrade migrate ibm-products-update-http-errors --write`,
};
// Return a placeholder if not released and not enabled by feature flag
From 1c918d1605f2a370988d7ade503c1e57e0d43df1 Mon Sep 17 00:00:00 2001
From: Afsal K
Date: Thu, 12 Dec 2024 10:48:38 +0530
Subject: [PATCH 3/7] fix(tearsheet): resolve focusing elements multiple times
while rendering (#6513)
* refactor(useFocus): refactor repeated useEffect code
* fix(tearsheetshell): remove unwanted useEffect
* fix(tearsheet): resolve unwanted onblur call
* fix(createTearsheet): resolve focus out issue
* test(tearsheet): implement test for onBlur method
---
.../CreateTearsheet/CreateTearsheet.tsx | 1 +
.../components/Tearsheet/Tearsheet.test.js | 83 +++++++++++--------
.../components/Tearsheet/TearsheetShell.tsx | 69 ++++-----------
.../src/global/js/hooks/useFocus.js | 6 +-
4 files changed, 71 insertions(+), 88 deletions(-)
diff --git a/packages/ibm-products/src/components/CreateTearsheet/CreateTearsheet.tsx b/packages/ibm-products/src/components/CreateTearsheet/CreateTearsheet.tsx
index 97011dbb26..78cf0d3a3c 100644
--- a/packages/ibm-products/src/components/CreateTearsheet/CreateTearsheet.tsx
+++ b/packages/ibm-products/src/components/CreateTearsheet/CreateTearsheet.tsx
@@ -310,6 +310,7 @@ export let CreateTearsheet = forwardRef(
verticalPosition,
closeIconDescription: '',
}}
+ currentStep={currentStep}
>