diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ca430ab0d1..51aa7c1f42 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -182,28 +182,6 @@ jobs:
name: playwright-avt-report
path: .playwright
- vrt-runner:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- - name: Setup Node.js
- uses: actions/setup-node@v2
- with:
- node-version: '20.x'
- cache: yarn
- - name: Install
- run: yarn
- - name: Install browsers
- run: yarn playwright install --with-deps
- - name: Build project
- run: yarn build
- - name: Run VRT
- working-directory: packages/core
- env:
- PERCY_TOKEN: web_d04495b0b413d61c2ea6b9118d1748b43f4fdd58d17ebe453ef8e0016b5766e4
- run: yarn percy storybook storybook-static
-
avt:
if: ${{ always() }}
runs-on: ubuntu-latest
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-styles/src/components/PageHeader/_page-header.scss b/packages/ibm-products-styles/src/components/PageHeader/_page-header.scss
index fa1adf8ebe..e2878df4b0 100644
--- a/packages/ibm-products-styles/src/components/PageHeader/_page-header.scss
+++ b/packages/ibm-products-styles/src/components/PageHeader/_page-header.scss
@@ -538,14 +538,8 @@ $right-section-alt-width: 100% - $left-section-alt-width;
}
.#{$block-class}__subtitle-row {
- display: -webkit-box;
- overflow: hidden;
- max-width: 100%;
margin-top: $spacing-03;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
-
@include breakpoint-up('md') {
max-width: $left-section-std-width;
}
@@ -559,6 +553,24 @@ $right-section-alt-width: 100% - $left-section-alt-width;
@include type.type-style('body-01');
}
+ .#{$block-class}__subtitle-tooltip .#{$carbon-prefix}--definition-term {
+ border-bottom: 0;
+ letter-spacing: inherit;
+ }
+
+ // overwrites the existing styles to make the popover bigger because in some cases the narrow space can be too constricting for the header
+ .#{$block-class}__subtitle-tooltip
+ .#{$carbon-prefix}--popover-content.#{$carbon-prefix}--definition-tooltip {
+ max-inline-size: fit-content;
+ }
+
+ .#{$block-class}__subtitle-text {
+ display: -webkit-box;
+ overflow: hidden;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ }
+
.#{$block-class}__available-row {
@include type.type-style('body-01');
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/Coachmark/Coachmark.test.js b/packages/ibm-products/src/components/Coachmark/Coachmark.test.js
index 5ec8084cb6..57fe87bd23 100644
--- a/packages/ibm-products/src/components/Coachmark/Coachmark.test.js
+++ b/packages/ibm-products/src/components/Coachmark/Coachmark.test.js
@@ -5,14 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
-import React from 'react';
-import {
- render,
- screen,
- act,
- waitFor,
- fireEvent,
-} from '@testing-library/react'; // https://testing-library.com/docs/react-testing-library/intro
+import React, { act } from 'react';
+import { render, screen, waitFor, fireEvent } from '@testing-library/react'; // https://testing-library.com/docs/react-testing-library/intro
+
import userEvent from '@testing-library/user-event';
import { pkg } from '../../settings';
import uuidv4 from '../../global/js/utils/uuidv4';
@@ -23,8 +18,14 @@ import {
CoachmarkOverlayElement,
CoachmarkOverlayElements,
} from '..';
-import { BEACON_KIND } from './utils/enums';
+import {
+ BEACON_KIND,
+ COACHMARK_ALIGNMENT,
+ COACHMARK_OVERLAY_KIND,
+} from './utils/enums';
import { CoachmarkDragbar } from './CoachmarkDragbar';
+import { getOffsetTune } from './utils/constants';
+import { clamp } from './utils/helpers';
const blockClass = `${pkg.prefix}--coachmark`;
const componentName = Coachmark.displayName;
@@ -74,6 +75,14 @@ describe(componentName, () => {
expect(screen.getByTestId(dataTestId)).toHaveClass(blockClass);
});
+ it('Check coachmark can be open by default', () => {
+ renderCoachmark({
+ 'data-testid': dataTestId,
+ isOpenByDefault: true,
+ });
+ expect(isCoachmarkVisible()).toBeTruthy();
+ });
+
it('has no accessibility violations', async () => {
const { container } = renderCoachmark();
await expect(container).toBeAccessible(componentName);
@@ -211,11 +220,108 @@ describe(componentName, () => {
);
});
- it('Check coachmark can be open by default', () => {
+ it('renders the theme prop', async () => {
renderCoachmark({
'data-testid': dataTestId,
- isOpenByDefault: true,
+ theme: 'dark',
});
- expect(isCoachmarkVisible()).toBeTruthy();
+
+ await expect(screen.getByTestId(dataTestId)).toHaveClass(
+ `${pkg.prefix}--coachmark__dark`
+ );
+ });
+
+ it('tests getOffsetTune util', async () => {
+ let result;
+ const distanceOffset = 24;
+ const coachmarkTarget = {
+ targetRect: {
+ width: 200,
+ height: 200,
+ },
+ align: COACHMARK_ALIGNMENT.TOP,
+ };
+
+ // Test case when it is a tooltip
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.TOOLTIP);
+ expect(result.left).toBe(0);
+ expect(result.top).toBe(0);
+
+ // Test top alignment
+ coachmarkTarget.align = COACHMARK_ALIGNMENT.TOP;
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.FLOATING);
+ expect(result.left).toBe(100);
+ expect(result.top).toBe(0);
+
+ // Test top left alignment
+ coachmarkTarget.align = COACHMARK_ALIGNMENT.TOP_LEFT;
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.FLOATING);
+ expect(result.left).toBe(distanceOffset);
+ expect(result.top).toBe(0);
+
+ // Test top right alignment
+ coachmarkTarget.align = COACHMARK_ALIGNMENT.TOP_RIGHT;
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.FLOATING);
+ expect(result.left).toBe(200 - distanceOffset);
+ expect(result.top).toBe(0);
+
+ // Test bottom alignment
+ coachmarkTarget.align = COACHMARK_ALIGNMENT.BOTTOM;
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.FLOATING);
+ expect(result.left).toBe(100);
+ expect(result.top).toBe(200);
+
+ // Test bottom left alignment
+ coachmarkTarget.align = COACHMARK_ALIGNMENT.BOTTOM_LEFT;
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.FLOATING);
+ expect(result.left).toBe(distanceOffset);
+ expect(result.top).toBe(200);
+
+ // Test bottom right alignment
+ coachmarkTarget.align = COACHMARK_ALIGNMENT.BOTTOM_RIGHT;
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.FLOATING);
+ expect(result.left).toBe(200 - distanceOffset);
+ expect(result.top).toBe(200);
+
+ // Test left alignment
+ coachmarkTarget.align = COACHMARK_ALIGNMENT.LEFT;
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.FLOATING);
+ expect(result.left).toBe(0);
+ expect(result.top).toBe(100);
+
+ // Test left top alignment
+ coachmarkTarget.align = COACHMARK_ALIGNMENT.LEFT_TOP;
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.FLOATING);
+ expect(result.left).toBe(0);
+ expect(result.top).toBe(distanceOffset);
+
+ // Test left bottom alignment
+ coachmarkTarget.align = COACHMARK_ALIGNMENT.LEFT_BOTTOM;
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.FLOATING);
+ expect(result.left).toBe(0);
+ expect(result.top).toBe(200 - distanceOffset);
+
+ // Test right alignment
+ coachmarkTarget.align = COACHMARK_ALIGNMENT.RIGHT;
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.FLOATING);
+ expect(result.left).toBe(200);
+ expect(result.top).toBe(100);
+
+ // Test right top alignment
+ coachmarkTarget.align = COACHMARK_ALIGNMENT.RIGHT_TOP;
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.FLOATING);
+ expect(result.left).toBe(200);
+ expect(result.top).toBe(distanceOffset);
+
+ // Test right bottom alignment
+ coachmarkTarget.align = COACHMARK_ALIGNMENT.RIGHT_BOTTOM;
+ result = getOffsetTune(coachmarkTarget, COACHMARK_OVERLAY_KIND.FLOATING);
+ expect(result.left).toBe(200);
+ expect(result.top).toBe(200 - distanceOffset);
+ });
+
+ it('tests clamp helper function', () => {
+ expect(clamp(100, 50, 20)).toBe(50);
+ expect(clamp(40, 10, 50)).toBe(40);
});
});
diff --git a/packages/ibm-products/src/components/Coachmark/CoachmarkOverlay.tsx b/packages/ibm-products/src/components/Coachmark/CoachmarkOverlay.tsx
index 0e0f29d3aa..4b20c4b738 100644
--- a/packages/ibm-products/src/components/Coachmark/CoachmarkOverlay.tsx
+++ b/packages/ibm-products/src/components/Coachmark/CoachmarkOverlay.tsx
@@ -105,6 +105,7 @@ export let CoachmarkOverlay = forwardRef(
const handleKeyPress = (event) => {
const { shiftKey, key } = event;
+ /* istanbul ignore next */
if (key === 'Enter' || key === ' ') {
setA11yDragMode((prevVal) => !prevVal);
} else if (a11yDragMode) {
@@ -151,6 +152,7 @@ export let CoachmarkOverlay = forwardRef(
return style;
}, [isBeacon, isDraggable, coachmark, kind]);
+ /* istanbul ignore next */
function handleDragBounds(x, y) {
let xRes = x;
let yRes = y;
@@ -254,6 +256,7 @@ const useWindowDimensions = () => {
);
useEffect(() => {
+ /* istanbul ignore next */
function handleResize() {
setWindowDimensions(getWindowDimensions());
}
diff --git a/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.test.js b/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.test.js
index d9cf59f68f..485911ba7a 100644
--- a/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.test.js
+++ b/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.test.js
@@ -22,12 +22,18 @@ const children = `hello, world (${uuidv4()})`;
const dataTestId = uuidv4();
const className = `class-${uuidv4()}`;
-const childrenContent = (
+const childrenContent = [
-);
+ />,
+ ,
+];
const renderCoachmarkWithOverlayElements = (
{ ...rest } = {},
@@ -165,4 +171,22 @@ describe(componentName, () => {
expect(screen.getByRole('img')).toBeInTheDocument();
});
+
+ it('calls onNext', async () => {
+ const user = userEvent.setup();
+ const onNext = jest.fn();
+ renderCoachmarkWithOverlayElements({
+ 'data-testid': dataTestId,
+ onNext,
+ });
+ const beaconOrButton = screen.getByRole('button', {
+ name: 'Show information',
+ });
+ await act(() => user.click(beaconOrButton));
+ const nextButton = screen.getByRole('button', {
+ name: 'Next',
+ });
+ await act(() => user.click(nextButton));
+ await expect(onNext).toHaveBeenCalled();
+ });
});
diff --git a/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.tsx b/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.tsx
index 2db4b39812..710db555aa 100644
--- a/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.tsx
+++ b/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.tsx
@@ -77,6 +77,18 @@ export interface CoachmarkOverlayElementsProps {
* The label for the Close button.
*/
closeButtonLabel?: string;
+ /**
+ * Callback called when clicking on the Next button.
+ */
+ onNext?: () => void;
+ /**
+ * Callback called when clicking on the Previous button.
+ */
+ onBack?: () => void;
+ /**
+ * Current step of the coachmarks.
+ */
+ currentStep?: number;
}
// NOTE: the component SCSS is not imported here: it is rolled up separately.
@@ -96,6 +108,9 @@ const defaults = {
nextButtonText: 'Next',
previousButtonLabel: 'Back',
closeButtonLabel: 'Got it',
+ onNext: undefined,
+ onBack: undefined,
+ currentStep: 0,
};
/**
* Composable container to allow for the displaying of CoachmarkOverlayElement
@@ -112,9 +127,12 @@ export let CoachmarkOverlayElements = React.forwardRef<
isVisible = defaults.isVisible,
media,
renderMedia,
+ currentStep = defaults.currentStep,
nextButtonText = defaults.nextButtonText,
previousButtonLabel = defaults.previousButtonLabel,
closeButtonLabel = defaults.closeButtonLabel,
+ onNext = defaults.onNext,
+ onBack = defaults.onBack,
// Collect any other property values passed in.
...rest
},
@@ -123,7 +141,7 @@ export let CoachmarkOverlayElements = React.forwardRef<
const buttonFocusRef = useRef | undefined>(undefined);
const scrollRef = useRef(undefined);
const [scrollPosition, setScrollPosition] = useState(0);
- const [currentProgStep, _setCurrentProgStep] = useState(0);
+ const [currentProgStep, _setCurrentProgStep] = useState(currentStep);
const coachmark = useCoachmark();
const hasMedia = media || renderMedia;
@@ -145,6 +163,16 @@ export let CoachmarkOverlayElements = React.forwardRef<
[currentProgStep, renderMedia]
);
+ useEffect(() => {
+ // When current step is set by props
+ // scroll to the appropriate view on the carrousel
+ const targetStep = clamp(currentStep, progStepFloor, progStepCeil);
+
+ scrollRef?.current?.scrollToView?.(targetStep);
+ // Avoid circular call to this hook
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [currentStep]);
+
useEffect(() => {
// On mount, one of the two primary buttons ("next" or "close")
// will be rendered and must have focus. (a11y)
@@ -222,7 +250,6 @@ export let CoachmarkOverlayElements = React.forwardRef<
) : (
<>
}
onScroll={(scrollPercent) => {
setScrollPosition(scrollPercent);
@@ -248,6 +275,7 @@ export let CoachmarkOverlayElements = React.forwardRef<
);
scrollRef?.current?.scrollToView?.(targetStep);
setCurrentProgStep(targetStep);
+ onBack?.();
}}
>
{previousButtonLabel}
@@ -268,6 +296,7 @@ export let CoachmarkOverlayElements = React.forwardRef<
);
scrollRef?.current?.scrollToView?.(targetStep);
setCurrentProgStep(targetStep);
+ onNext?.();
}}
>
{nextButtonText}
@@ -320,6 +349,10 @@ CoachmarkOverlayElements.propTypes = {
* The label for the Close button.
*/
closeButtonLabel: PropTypes.string,
+ /**
+ * Current step of the coachmarks
+ */
+ currentStep: PropTypes.number,
/**
* The visibility of CoachmarkOverlayElements is
* managed in the parent component.
@@ -344,6 +377,14 @@ CoachmarkOverlayElements.propTypes = {
* The label for the Next button.
*/
nextButtonText: PropTypes.string,
+ /**
+ * Optional callback called when clicking on the Previous button.
+ */
+ onBack: PropTypes.func,
+ /**
+ * Optional callback called when clicking on the Next button.
+ */
+ onNext: PropTypes.func,
/**
* The label for the Previous button.
*/
diff --git a/packages/ibm-products/src/components/CoachmarkStack/CoachmarkStack.test.js b/packages/ibm-products/src/components/CoachmarkStack/CoachmarkStack.test.js
index dd67b208f2..f46f0cdc3e 100644
--- a/packages/ibm-products/src/components/CoachmarkStack/CoachmarkStack.test.js
+++ b/packages/ibm-products/src/components/CoachmarkStack/CoachmarkStack.test.js
@@ -5,8 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
-import React from 'react';
-import { render, screen, act } from '@testing-library/react'; // https://testing-library.com/docs/react-testing-library/intro
+import React, { act } from 'react';
+import { render, screen } from '@testing-library/react'; // https://testing-library.com/docs/react-testing-library/intro
import { pkg } from '../../settings';
import uuidv4 from '../../global/js/utils/uuidv4';
@@ -131,4 +131,74 @@ describe(componentName, () => {
componentName
);
});
+
+ it('calls the onClose prop', async () => {
+ const onClose = jest.fn();
+ renderCoachmarkStack({
+ title: 'Coachmark Stack',
+ description: 'Coachmark Stack Description',
+ navLinkLabels: ['Label 1', 'Label 2', 'Label 3'],
+ tagline: 'Test Tagline',
+ 'data-testid': dataTestId,
+ onClose,
+ });
+ expect(onClose).not.toHaveBeenCalled();
+
+ const coachmarkStackButton = screen.getByRole('button', {
+ name: /Test Tagline/,
+ });
+
+ await act(() => userEvent.click(coachmarkStackButton));
+
+ const closeButton = screen.getAllByRole('button', {
+ name: /Close/,
+ })[0];
+
+ await act(() => userEvent.click(closeButton));
+
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it('opens a stacked coachmark', async () => {
+ const onClose = jest.fn();
+ renderCoachmarkStack({
+ title: 'Coachmark Stack',
+ description: 'Coachmark Stack Description',
+ navLinkLabels: ['Label 1', 'Label 2', 'Label 3'],
+ tagline: 'Test Tagline',
+ 'data-testid': dataTestId,
+ onClose,
+ });
+
+ // gets the trigger to open the overlay
+ const coachmarkStackButton = screen.getByRole('button', {
+ name: /Test Tagline/,
+ });
+ await act(() => userEvent.click(coachmarkStackButton));
+
+ // Gets the label button to open a stacked item
+ const labelButton = screen.getByRole('button', {
+ name: /Label 1/,
+ });
+ await act(() => userEvent.click(labelButton));
+
+ // Gets the overlay element
+ const coachmarkOverlay = document.querySelector(
+ `.${pkg.prefix}--coachmark-overlay`
+ );
+
+ // tests to see if the element has the is-stacked class
+ expect(coachmarkOverlay).toHaveClass(
+ `${pkg.prefix}--coachmark-stack-element--is-stacked`
+ );
+
+ // pressing escape should close the stacked item
+ await act(() => userEvent.keyboard('{Escape}'));
+
+ expect(coachmarkOverlay).not.toHaveClass(
+ `${pkg.prefix}--coachmark-stack-element--is-stacked`
+ );
+
+ await act(() => userEvent.keyboard('{Escape}'));
+ });
});
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}
>
}
>
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
diff --git a/packages/ibm-products/src/components/PageHeader/PageHeader.stories.jsx b/packages/ibm-products/src/components/PageHeader/PageHeader.stories.jsx
index 22e6b8fd6b..74ac52968c 100644
--- a/packages/ibm-products/src/components/PageHeader/PageHeader.stories.jsx
+++ b/packages/ibm-products/src/components/PageHeader/PageHeader.stories.jsx
@@ -486,7 +486,7 @@ const pageActionsOverflowLabel = 'Page actions...';
const subtitle = 'Optional subtitle if necessary';
const longSubtitle =
- 'Optional subtitle if necessary, which is very long in this case, but will need to be handled somehow. It just keeps going on and on and on and on and on.';
+ 'Optional subtitle if necessary, which is very long in this case, but will need to be handled somehow. It just keeps going on and on and on and on and on and on and on and on and on and on and on.';
const demoSubtitle = 'This report details the monthly authentication failures';
const dummyPageContent = (
diff --git a/packages/ibm-products/src/components/PageHeader/PageHeader.tsx b/packages/ibm-products/src/components/PageHeader/PageHeader.tsx
index aa2c4a6cc1..8d339ec5e2 100644
--- a/packages/ibm-products/src/components/PageHeader/PageHeader.tsx
+++ b/packages/ibm-products/src/components/PageHeader/PageHeader.tsx
@@ -15,6 +15,7 @@ import {
usePrefix,
ButtonProps,
PopoverAlignment,
+ DefinitionTooltip,
} from '@carbon/react';
import { TagProps } from '@carbon/react/lib/components/Tag/Tag';
import React, {
@@ -51,6 +52,7 @@ import cx from 'classnames';
import { getDevtoolsProps } from '../../global/js/utils/devtools';
import { pkg } from '../../settings';
import { useResizeObserver } from '../../global/js/hooks/useResizeObserver';
+import { checkHeightOverflow } from '../../global/js/utils/checkForOverflow';
const componentName = 'PageHeader';
@@ -901,12 +903,20 @@ export let PageHeader = React.forwardRef(
const displayedBreadcrumbs = getBreadcrumbs();
+ const subtitleRef = useRef(null);
+ const isOverflowing = checkHeightOverflow(subtitleRef.current);
+ const subtitleContent = (
+
+ {subtitle}
+
+ );
+
return (
<>
+ />
) : null}
- {subtitle ? (
+ {subtitle && (
- {subtitle}
+ {isOverflowing ? (
+
+ {subtitleContent}
+
+ ) : (
+ subtitleContent
+ )}
- ) : null}
+ )}
{children ? (
diff --git a/packages/ibm-products/src/components/PageHeader/PageHeaderTitle.js b/packages/ibm-products/src/components/PageHeader/PageHeaderTitle.js
index 188776d5f8..c0d8371e86 100644
--- a/packages/ibm-products/src/components/PageHeader/PageHeaderTitle.js
+++ b/packages/ibm-products/src/components/PageHeader/PageHeaderTitle.js
@@ -5,11 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
-import React, { useLayoutEffect, useRef, useState } from 'react';
+import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { DefinitionTooltip, SkeletonText } from '@carbon/react';
import { EditInPlace } from '../EditInPlace';
+import { checkWidthOverflow } from '../../global/js/utils/checkForOverflow';
/**
*
@@ -39,25 +40,8 @@ export const PageHeaderTitle = ({ blockClass, hasBreadcrumbRow, title }) => {
let titleText;
let isEditable = !!onSave;
- const [isEllipsisApplied, setIsEllipsisApplied] = useState();
- const longTitleRef = useRef(undefined);
- const titleRef = useRef(undefined);
-
- useLayoutEffect(() => {
- setIsEllipsisApplied(isEllipsisActive());
- }, [longTitleRef, titleRef, title]);
-
- const isEllipsisActive = () => {
- if (longTitleRef.current) {
- return (
- longTitleRef.current?.offsetWidth < longTitleRef.current?.scrollWidth
- );
- } else if (titleRef.current) {
- return titleRef.current?.offsetWidth < titleRef.current?.scrollWidth;
- }
-
- return false;
- };
+ const titleRef = useRef();
+ const isEllipsisApplied = checkWidthOverflow(titleRef.current);
if (text || !content) {
if (text === undefined && typeof title === 'string') {
@@ -66,6 +50,12 @@ export const PageHeaderTitle = ({ blockClass, hasBreadcrumbRow, title }) => {
}
const TitleIcon = icon;
+ const titleContent = (
+
+ {text}
+
+ );
+
titleInnards = (
<>
{icon && !loading ? (
@@ -97,18 +87,10 @@ export const PageHeaderTitle = ({ blockClass, hasBreadcrumbRow, title }) => {
definition={text}
className={`${blockClass}__tooltip`}
>
-
- {text}
-
+ {titleContent}
) : (
-
- {text}
-
+ titleContent
)}
>
);
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(
false);
const onCloseReturnsTrue = jest.fn(() => true);
+const onBlur = jest.fn();
const createButton = `Create ${uuidv4()}`;
const actions = [
@@ -92,6 +93,41 @@ const navigation = (
);
const title = `Title of the ${uuidv4()} tearsheet`;
+const mainText = 'Main content 1';
+const inputId = 'stacked-input-1';
+
+// eslint-disable-next-line react/prop-types
+const DummyComponent = ({ props, open }) => {
+ const buttonRef = React.useRef(undefined);
+
+ return (
+ <>
+
+
+
+ {mainText}
+
+
+
+ >
+ );
+};
+
// These are tests than apply to both Tearsheet and TearsheetNarrow
// and also (with extra props and omitting button tests) to CreateTearsheetNarrow
let tooManyButtonsTestedAlready = false;
@@ -262,40 +298,6 @@ const commonTests = (Ts, name, props, testActions) => {
});
it('should return focus to the launcher button', async () => {
- const mainText = 'Main content 1';
- const inputId = 'stacked-input-1';
-
- // eslint-disable-next-line react/prop-types
- const DummyComponent = ({ open }) => {
- const buttonRef = React.useRef(undefined);
-
- return (
- <>
-
-
-
- {mainText}
-
-
-
- >
- );
- };
-
const { rerender, getByText, getByTestId } = render(
);
@@ -320,6 +322,19 @@ const commonTests = (Ts, name, props, testActions) => {
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
expect(launchButtonEl).toHaveFocus();
});
+
+ it('should call onBlur only once', async () => {
+ const { getByTestId } = render();
+
+ const inputEl = getByTestId(inputId);
+ const closeButton = screen.getByRole('button', {
+ name: closeIconDescription,
+ });
+
+ expect(inputEl).toHaveFocus();
+ await act(() => userEvent.click(closeButton));
+ expect(onBlur).toHaveBeenCalledTimes(1);
+ });
}
it('is visible when open is true', async () => {
diff --git a/packages/ibm-products/src/components/Tearsheet/TearsheetShell.tsx b/packages/ibm-products/src/components/Tearsheet/TearsheetShell.tsx
index 74dd3e7376..707d62a460 100644
--- a/packages/ibm-products/src/components/Tearsheet/TearsheetShell.tsx
+++ b/packages/ibm-products/src/components/Tearsheet/TearsheetShell.tsx
@@ -65,6 +65,11 @@ interface TearsheetShellProps extends PropsWithChildren {
*/
className?: string;
+ /**
+ * Used to track the current step on components which use `StepsContext` and `TearsheetShell`
+ */
+ currentStep?: number;
+
/**
* A description of the flow, displayed in the header area of the tearsheet.
*/
@@ -199,13 +204,9 @@ export type CloseIconDescriptionTypes =
type stackTypes = {
open: Array<{
(a: number, b: number): void;
- checkFocus?: () => void;
- claimFocus?: () => void;
}>;
all: Array<{
(a: number, b: number): void;
- checkFocus?: () => void;
- claimFocus?: () => void;
}>;
sizes: Array;
};
@@ -242,6 +243,7 @@ export const TearsheetShell = React.forwardRef(
children,
className,
closeIconDescription,
+ currentStep,
description,
hasCloseIcon,
headerActions,
@@ -274,7 +276,7 @@ export const TearsheetShell = React.forwardRef(
const modalRef = (ref || localRef) as MutableRefObject;
const { width } = useResizeObserver(resizer);
const prevOpen = usePreviousValue(open);
- const { firstElement, keyDownListener, specifiedElement } = useFocus(
+ const { firstElement, keyDownListener } = useFocus(
modalRef,
selectorPrimaryFocus
);
@@ -309,29 +311,22 @@ export const TearsheetShell = React.forwardRef(
setDepth(newDepth);
setPosition(newPosition);
}
- handleStackChange.checkFocus = function () {
- // if we are now the topmost tearsheet, ensure we have focus
- if (
- open &&
- position === depth &&
- modalRefValue &&
- !modalRefValue.contains(document.activeElement)
- ) {
- handleStackChange.claimFocus();
- }
- };
-
- // Callback to give the tearsheet the opportunity to claim focus
- handleStackChange.claimFocus = function () {
- claimFocus(firstElement, modalRef, selectorPrimaryFocus);
- };
useEffect(() => {
- if (open) {
+ if (open && position === depth) {
// Focusing the first element or selectorPrimaryFocus element
claimFocus(firstElement, modalRef, selectorPrimaryFocus);
}
- }, [firstElement, modalRef, open, selectorPrimaryFocus]);
+ }, [
+ currentStep,
+ depth,
+ firstElement,
+ modalRef,
+ modalRefValue,
+ open,
+ position,
+ selectorPrimaryFocus,
+ ]);
useEffect(() => {
if (prevOpen && !open && launcherButtonRef) {
@@ -341,24 +336,6 @@ export const TearsheetShell = React.forwardRef(
}
}, [launcherButtonRef, open, prevOpen]);
- useEffect(() => {
- if (open && position !== depth) {
- setTimeout(() => {
- if (selectorPrimaryFocus) {
- return specifiedElement?.focus();
- }
- firstElement?.focus();
- }, 0);
- }
- }, [
- position,
- depth,
- firstElement,
- open,
- specifiedElement,
- selectorPrimaryFocus,
- ]);
-
useEffect(() => {
const notify = () =>
stack.all.forEach((handler) => {
@@ -366,7 +343,6 @@ export const TearsheetShell = React.forwardRef(
Math.min(stack.open.length, maxDepth),
stack.open.indexOf(handler) + 1
);
- handler.checkFocus?.();
});
// Register this tearsheet's stack change callback/listener.
@@ -401,14 +377,6 @@ export const TearsheetShell = React.forwardRef(
};
}, [open, size]);
- function handleFocus() {
- // If something within us is receiving focus but we are not the topmost
- // stacked tearsheet, transfer focus to the topmost tearsheet instead
- if (position < depth) {
- stack.open[stack.open.length - 1].claimFocus?.();
- }
- }
-
if (position <= depth) {
// Include a modal header if and only if one or more of these is given.
// We can't use a Wrap for the ModalHeader because ComposedModal requires
@@ -464,7 +432,6 @@ export const TearsheetShell = React.forwardRef(
!areAllSameSizeVariant(),
})}
{...{ onClose, open, selectorPrimaryFocus }}
- onFocus={handleFocus}
onKeyDown={keyDownListener}
preventCloseOnClickOutside={!isPassive}
ref={modalRef}
diff --git a/packages/ibm-products/src/global/js/hooks/useFocus.js b/packages/ibm-products/src/global/js/hooks/useFocus.js
index e6a7dd052a..b4cc99a601 100644
--- a/packages/ibm-products/src/global/js/hooks/useFocus.js
+++ b/packages/ibm-products/src/global/js/hooks/useFocus.js
@@ -122,9 +122,9 @@ export const claimFocus = (
specifiedEl &&
window?.getComputedStyle(specifiedEl)?.display !== 'none'
) {
- return specifiedEl.focus();
+ setTimeout(() => specifiedEl.focus(), 0);
}
+ } else {
+ setTimeout(() => firstElement?.focus(), 0);
}
-
- setTimeout(() => firstElement?.focus(), 0);
};
diff --git a/packages/ibm-products/src/global/js/utils/__tests__/checkForOverflow.test.js b/packages/ibm-products/src/global/js/utils/__tests__/checkForOverflow.test.js
new file mode 100644
index 0000000000..2144e23e88
--- /dev/null
+++ b/packages/ibm-products/src/global/js/utils/__tests__/checkForOverflow.test.js
@@ -0,0 +1,40 @@
+/**
+ * Copyright IBM Corp. 2024
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { checkWidthOverflow, checkHeightOverflow } from '../checkForOverflow';
+
+const normalElm = {
+ offsetWidth: 200,
+ scrollWidth: 100,
+ offsetHeight: 200,
+ scrollHeight: 100,
+};
+
+const overflowElm = {
+ offsetWidth: 100,
+ scrollWidth: 200,
+ offsetHeight: 100,
+ scrollHeight: 200,
+};
+
+describe('checkForOverflow', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('detects width overflow', () => {
+ expect(checkWidthOverflow(normalElm)).toBe(false);
+ expect(checkWidthOverflow(overflowElm)).toBe(true);
+ expect(checkWidthOverflow()).toBe(false);
+ });
+
+ it('detects height overflow', () => {
+ expect(checkHeightOverflow(normalElm)).toBe(false);
+ expect(checkHeightOverflow(overflowElm)).toBe(true);
+ expect(checkHeightOverflow()).toBe(false);
+ });
+});
diff --git a/packages/ibm-products/src/global/js/utils/checkForOverflow.ts b/packages/ibm-products/src/global/js/utils/checkForOverflow.ts
new file mode 100644
index 0000000000..386389d4b2
--- /dev/null
+++ b/packages/ibm-products/src/global/js/utils/checkForOverflow.ts
@@ -0,0 +1,24 @@
+//
+// Copyright IBM Corp. 2024, 2024
+//
+// This source code is licensed under the Apache-2.0 license found in the
+// LICENSE file in the root directory of this source tree.
+//
+
+/**
+ * used to calculate if a element is overflowing the width or height of an area
+ */
+
+export const checkWidthOverflow = (el: HTMLElement | null): boolean => {
+ if (el) {
+ return el?.offsetWidth < el?.scrollWidth;
+ }
+ return false;
+};
+
+export const checkHeightOverflow = (el: HTMLElement | null): boolean => {
+ if (el) {
+ return el?.offsetHeight < el?.scrollHeight;
+ }
+ return false;
+};