+ )}
>
)}
@@ -376,6 +422,20 @@ const downloadRequiredProps = (type) =>
// Return a placeholder if not released and not enabled by feature flag
APIKeyModal = pkg.checkComponentEnabled(APIKeyModal, componentName);
+export const deprecatedProps = {
+ /**
+ * deprecated
+ * title for a successful edit
+ */
+ editSuccessTitle: PropTypes.string,
+
+ /**
+ * deprecated
+ * title for a successful key generation
+ */
+ generateSuccessTitle: PropTypes.string,
+};
+
APIKeyModal.propTypes = {
/**
* the api key that's displayed to the user when a request to create is fulfilled.
@@ -436,7 +496,7 @@ APIKeyModal.propTypes = {
/**
* the content that appears that indicates the key is downloadable
*/
- downloadBodyText: downloadRequiredProps(PropTypes.string),
+ downloadBodyText: PropTypes.string,
/**
* designates the name of downloadable json file with the key. if not specified will default to 'apikey'
*/
@@ -464,7 +524,7 @@ APIKeyModal.propTypes = {
/**
* title for a successful edit
*/
- editSuccessTitle: editRequiredProps(PropTypes.string),
+ editSuccessMessage: editRequiredProps(PropTypes.string),
/**
* designates if the modal is in the edit mode
*/
@@ -490,7 +550,7 @@ APIKeyModal.propTypes = {
/**
* title for a successful key generation
*/
- generateSuccessTitle: PropTypes.string,
+ generateSuccessMessage: PropTypes.string,
/**
* default title for the modal in generate key mode
*/
@@ -503,6 +563,10 @@ APIKeyModal.propTypes = {
* designates if user is able to download the api key
*/
hasDownloadLink: PropTypes.bool,
+ /**
+ * helper text for password input
+ */
+ helperText: PropTypes.string,
/**
* label text that's displayed when hovering over visibility toggler to hide key
*/
@@ -582,6 +646,8 @@ APIKeyModal.propTypes = {
* label text that's displayed when hovering over visibility toggler to show key
*/
showAPIKeyLabel: PropTypes.string,
+
+ ...deprecatedProps,
};
APIKeyModal.displayName = componentName;
diff --git a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.types.ts b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.types.ts
index 200528760a..16233bbd7e 100644
--- a/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.types.ts
+++ b/packages/ibm-products/src/components/APIKeyModal/APIKeyModal.types.ts
@@ -75,9 +75,14 @@ interface APIKeyModalCommonProps {
*/
generateSuccessBody?: ReactNode;
/**
+ * * @deprecated use `generateSuccessMessage` instead
* title for a successful key generation
*/
generateSuccessTitle?: string;
+ /**
+ * success message for a successful key generation
+ */
+ generateSuccessMessage?: string;
/**
* default title for the modal in generate key mode
*/
@@ -160,6 +165,10 @@ interface APIKeyModalCommonProps {
* label text that's displayed when hovering over visibility toggler to show key
*/
showAPIKeyLabel?: string;
+ /**
+ * helper text for password input
+ */
+ helperText?: string;
}
type CustomStepConditionalProps = {
@@ -195,9 +204,14 @@ type EditingConditionalProps = {
*/
editSuccess: boolean;
/**
+ * * @deprecated use `editSuccessMessage` instead
* title for a successful edit
*/
- editSuccessTitle: string;
+ editSuccessTitle?: string;
+ /**
+ * success message for edit
+ */
+ editSuccessMessage: string;
};
type HasDownloadLinkProps = {
@@ -208,7 +222,7 @@ type HasDownloadLinkProps = {
/**
* the content that appears that indicates the key is downloadable
*/
- downloadBodyText: string;
+ downloadBodyText?: string;
/**
* designates the name of downloadable json file with the key. if not specified will default to 'apikey'
*/
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/ConditionBuilder/ConditionBuilderContent/ConditionBuilderContent.tsx b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContent/ConditionBuilderContent.tsx
index fd86105ec9..460c6235e9 100644
--- a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContent/ConditionBuilderContent.tsx
+++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContent/ConditionBuilderContent.tsx
@@ -99,8 +99,7 @@ const ConditionBuilderContent = ({
}, [actionState]);
useEffect(() => {
if (initialState?.enabledDefault) {
- setRootState?.(initialConditionState.current as ConditionBuilderState);
- initialConditionState.current = null;
+ setRootState?.(initialState.state);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialState]);
@@ -120,12 +119,17 @@ const ConditionBuilderContent = ({
const onRemove = useCallback(
(groupId) => {
+ const groups = rootState?.groups?.filter(
+ (group) => groupId !== group?.id
+ );
setRootState?.({
...rootState,
- groups: rootState
- ? rootState?.groups?.filter((group) => groupId !== group?.id)
- : [],
+ groups: rootState ? groups : [],
});
+ //set the initial state to empty.
+ if (groups?.length === 0) {
+ initialConditionState.current = null;
+ }
},
[setRootState, rootState]
);
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(
{
const [open, setOpen] = useState(false);
@@ -479,6 +493,7 @@ const SlideOverTemplate = ({
ref={testRef}
aiLabel={aiLabel && sampleAILabel}
slug={slug && sampleAILabel}
+ decorator={decorator && sampleAILabel}
launcherButtonRef={buttonRef}
>
{!minimalContent && }
@@ -492,6 +507,7 @@ const FirstElementDisabledTemplate = ({
actions,
aiLabel,
slug,
+ decorator,
...args
}) => {
const [open, setOpen] = useState(false);
@@ -515,6 +531,7 @@ const FirstElementDisabledTemplate = ({
ref={testRef}
aiLabel={aiLabel && sampleAILabel}
slug={slug && sampleAILabel}
+ decorator={decorator && sampleAILabel}
launcherButtonRef={buttonRef}
>
{!minimalContent && (
@@ -553,7 +570,7 @@ const FirstElementDisabledTemplate = ({
};
// eslint-disable-next-line react/prop-types
-const StepTemplate = ({ actions, aiLabel, slug, ...args }) => {
+const StepTemplate = ({ actions, aiLabel, slug, decorator, ...args }) => {
const [open, setOpen] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const buttonRef = useRef(undefined);
@@ -576,6 +593,7 @@ const StepTemplate = ({ actions, aiLabel, slug, ...args }) => {
actions={actionSets[actions]}
aiLabel={aiLabel && sampleAILabel}
slug={slug && sampleAILabel}
+ decorator={decorator && sampleAILabel}
launcherButtonRef={buttonRef}
>
{
};
// eslint-disable-next-line react/prop-types
-const SlideInTemplate = ({ actions, aiLabel, slug, ...args }) => {
+const SlideInTemplate = ({ actions, aiLabel, slug, decorator, ...args }) => {
const [open, setOpen] = useState(false);
const buttonRef = useRef(undefined);
@@ -612,6 +630,7 @@ const SlideInTemplate = ({ actions, aiLabel, slug, ...args }) => {
actions={actionSets[actions]}
aiLabel={aiLabel && sampleAILabel}
slug={slug && sampleAILabel}
+ decorator={decorator && sampleAILabel}
launcherButtonRef={buttonRef}
>
@@ -675,7 +694,7 @@ export const WithAILabel = SlideOverTemplate.bind({});
WithAILabel.args = {
includeOverlay: true,
actions: 0,
- aiLabel: 1,
+ decorator: 1,
...defaultStoryProps,
};
diff --git a/packages/ibm-products/src/components/SidePanel/SidePanel.test.js b/packages/ibm-products/src/components/SidePanel/SidePanel.test.js
index afcd189839..12cf981362 100644
--- a/packages/ibm-products/src/components/SidePanel/SidePanel.test.js
+++ b/packages/ibm-products/src/components/SidePanel/SidePanel.test.js
@@ -32,6 +32,26 @@ const selectorPageContentValue = '#side-panel-test-page-content';
const onRequestCloseFn = jest.fn();
const onUnmountFn = jest.fn();
+const sampleAILabel = (
+
+
+
+
AI Explained
+
84%
+
Confidence score
+
+ This is not really Lorem Ipsum but the spell checker did not like the
+ previous text with it's non-words which is why this unwieldy
+ sentence, should one choose to call it that, here.
+
) =>
render(
{
);
expect(navigationAction).toBeTruthy();
});
- it('should not have AI Label when it is not passed', () => {
+
+ it('should have AI Label when it is passed through slug', () => {
+ const { container } = renderSidePanel({
+ slug: sampleAILabel,
+ });
+ expect(container.querySelector('.aiLabel-container')).toBeTruthy();
+ });
+
+ it('should not have a ai label container when a it is not passed', () => {
const { container } = renderSidePanel();
expect(container.querySelector('.aiLabel-container')).toBe(null);
});
+
it('should have AI Label when it is passed', () => {
- const sampleAILabel = (
-
-
-
-
AI Explained
-
84%
-
Confidence score
-
- This is not really Lorem Ipsum but the spell checker did not like
- the previous text with it's non-words which is why this
- unwieldy sentence, should one choose to call it that, here.
-
-
-
Model type
-
Foundation model
-
-
-
- );
const { container } = renderSidePanel({
aiLabel: sampleAILabel,
});
expect(container.querySelector('.aiLabel-container')).toBeTruthy();
});
+
+ it('should have AI Label when it is passed to decorator', () => {
+ const { container } = renderSidePanel({
+ decorator: sampleAILabel,
+ });
+ expect(container.querySelector('.aiLabel-container')).toBeTruthy();
+ });
+
it('should throw console warning if labelText passed without Title', () => {
const consoleWarnSpy = jest
.spyOn(console, 'warn')
diff --git a/packages/ibm-products/src/components/SidePanel/SidePanel.tsx b/packages/ibm-products/src/components/SidePanel/SidePanel.tsx
index 76534937c3..94c020c21c 100644
--- a/packages/ibm-products/src/components/SidePanel/SidePanel.tsx
+++ b/packages/ibm-products/src/components/SidePanel/SidePanel.tsx
@@ -170,16 +170,22 @@ type SidePanelBaseProps = {
slideIn?: boolean;
/**
- * @deprecated please use the `aiLabel` prop
+ * @deprecated please use the `decorator` instead
* **Experimental:** Provide a `Slug` component to be rendered inside the `SidePanel` component
*/
slug?: ReactNode;
/**
+ * @deprecated please use the `decorator` instead
* Optional prop that is intended for any scenario where something is being generated by AI to reinforce AI transparency, accountability, and explainability at the UI level.
*/
aiLabel?: ReactNode;
+ /**
+ * Provide a `decorator` component to be rendered inside the `SidePanel` component
+ */
+ decorator?: ReactNode;
+
/**
* Sets the subtitle text
*/
@@ -247,6 +253,7 @@ export let SidePanel = React.forwardRef(
closeIconDescription = defaults.closeIconDescription,
condensedActions,
currentStep = defaults.currentStep,
+ decorator,
id = blockClass,
includeOverlay,
labelText,
@@ -670,7 +677,9 @@ export let SidePanel = React.forwardRef(
[`${blockClass}--right-placement`]: placement === 'right',
[`${blockClass}--left-placement`]: placement === 'left',
[`${blockClass}--slide-in`]: slideIn,
- [`${blockClass}--has-ai-label`]: !!aiLabel || !!slug,
+ [`${blockClass}--has-decorator`]: decorator,
+ [`${blockClass}--has-slug`]: slug,
+ [`${blockClass}--has-ai-label`]: aiLabel,
[`${blockClass}--condensed-actions`]: condensedActions,
[`${blockClass}--has-overlay`]: includeOverlay,
},
@@ -704,29 +713,39 @@ export let SidePanel = React.forwardRef(
);
const renderHeader = () => {
- const aiLabelCloseSize =
+ const closeSize =
actions && actions.length && /l/.test(size) ? 'md' : 'sm';
- let normalizedAILabel;
+ let normalizedDecorator;
/**
* slug is deprecated
* can remove this condition in future release
*/
- if (slug && slug['type']?.displayName === 'Slug') {
- normalizedAILabel = React.cloneElement(
+ if (slug && slug['type']?.displayName === 'AILabel') {
+ normalizedDecorator = React.cloneElement(
slug as React.ReactElement,
{
// slug size is sm unless actions and size > md
- size: aiLabelCloseSize,
+ size: closeSize,
}
);
}
if (aiLabel && aiLabel['type']?.displayName === 'AILabel') {
- normalizedAILabel = React.cloneElement(
+ normalizedDecorator = React.cloneElement(
aiLabel as React.ReactElement,
{
// aiLabel size is sm unless actions and size > md
- size: aiLabelCloseSize,
+ size: closeSize,
+ }
+ );
+ }
+
+ if (decorator?.['type']?.displayName === 'AILabel') {
+ normalizedDecorator = React.cloneElement(
+ decorator as React.ReactElement,
+ {
+ // decorator size is sm unless actions and size > md
+ size: closeSize,
}
);
}
@@ -745,7 +764,7 @@ export let SidePanel = React.forwardRef(
{currentStep > 0 && (