diff --git a/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.stories.jsx b/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.stories.jsx index 3c57f0468b..8e7d2ddcf1 100644 --- a/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.stories.jsx +++ b/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.stories.jsx @@ -15,6 +15,7 @@ import { CoachmarkOverlayElements } from '.'; import mdx from './CoachmarkOverlayElements.mdx'; import styles from './_storybook-styles.scss?inline'; +import { SteppedAnimatedMedia } from '../SteppedAnimatedMedia'; export default { title: @@ -27,6 +28,7 @@ export default { }, media: { control: { type: null }, + description: 'Deprecated: Property replaced by "renderMedia"', }, }, parameters: { @@ -68,5 +70,7 @@ coachmarkOverlayElements.args = { nextButtonText: 'Next', previousButtonLabel: 'Back', className: 'myOverlayElements', - media: { filePaths: [Anim1, Anim2] }, + renderMedia: ({ playStep }) => ( + + ), }; diff --git a/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.test.js b/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.test.js index 0dcbac94e5..d9cf59f68f 100644 --- a/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.test.js +++ b/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.test.js @@ -139,4 +139,30 @@ describe(componentName, () => { componentName ); }); + it(`renders an image with media prop`, async () => { + const user = userEvent.setup(); + renderCoachmarkWithOverlayElements({ + 'data-testid': dataTestId, + media: { render: () => img }, + }); + const beaconOrButton = screen.getByRole('button', { + name: 'Show information', + }); + await act(() => user.click(beaconOrButton)); + + expect(screen.getByRole('img')).toBeInTheDocument(); + }); + it(`renders an image`, async () => { + const user = userEvent.setup(); + renderCoachmarkWithOverlayElements({ + 'data-testid': dataTestId, + renderMedia: () => img, + }); + const beaconOrButton = screen.getByRole('button', { + name: 'Show information', + }); + await act(() => user.click(beaconOrButton)); + + expect(screen.getByRole('img')).toBeInTheDocument(); + }); }); diff --git a/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.tsx b/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.tsx index 47d07b0610..2db4b39812 100644 --- a/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.tsx +++ b/packages/ibm-products/src/components/CoachmarkOverlayElements/CoachmarkOverlayElements.tsx @@ -13,6 +13,7 @@ import React, { ReactNode, RefObject, useEffect, + useMemo, useRef, useState, } from 'react'; @@ -54,11 +55,16 @@ export interface CoachmarkOverlayElementsProps { * The object describing an image in one of two shapes. * If a single media element is required, use `{render}`. * If a stepped animation is required, use `{filePaths}`. + * * @deprecated please use the `renderMedia` prop */ media?: { render?: () => ReactNode; filePaths?: string[]; }; + /** + * Optional prop to render any media like images or any animated media. + */ + renderMedia?: (params) => ReactNode; /** * The label for the Next button. */ @@ -105,6 +111,7 @@ export let CoachmarkOverlayElements = React.forwardRef< children, isVisible = defaults.isVisible, media, + renderMedia, nextButtonText = defaults.nextButtonText, previousButtonLabel = defaults.previousButtonLabel, closeButtonLabel = defaults.closeButtonLabel, @@ -118,6 +125,7 @@ export let CoachmarkOverlayElements = React.forwardRef< const [scrollPosition, setScrollPosition] = useState(0); const [currentProgStep, _setCurrentProgStep] = useState(0); const coachmark = useCoachmark(); + const hasMedia = media || renderMedia; const setCurrentProgStep = (value) => { if (currentProgStep > 0 && value === 0 && buttonFocusRef.current) { @@ -132,6 +140,11 @@ export let CoachmarkOverlayElements = React.forwardRef< const progStepFloor = 0; const progStepCeil = numProgSteps - 1; + const renderMediaContent = useMemo( + () => renderMedia?.({ playStep: currentProgStep }), + [currentProgStep, renderMedia] + ); + useEffect(() => { // On mount, one of the two primary buttons ("next" or "close") // will be rendered and must have focus. (a11y) @@ -172,16 +185,19 @@ export let CoachmarkOverlayElements = React.forwardRef< ref={ref} {...getDevtoolsProps(componentName)} > - {media && - (media.render ? ( - media.render() - ) : ( - - ))} + {hasMedia && media?.render && media.render()} + {hasMedia && media?.filePaths && ( + + )} + {hasMedia && renderMedia && ( +
+ {renderMediaContent} +
+ )} {numProgSteps === 1 ? ( <> @@ -313,6 +329,7 @@ CoachmarkOverlayElements.propTypes = { * The object describing an image in one of two shapes. * If a single media element is required, use `{render}`. * If a stepped animation is required, use `{filePaths}`. + * @deprecated please use the `renderMedia` prop */ /**@ts-ignore*/ media: PropTypes.oneOfType([ @@ -331,4 +348,8 @@ CoachmarkOverlayElements.propTypes = { * The label for the Previous button. */ previousButtonLabel: PropTypes.string, + /** + * Optional prop to render any media like images or animated media. + */ + renderMedia: PropTypes.func, }; diff --git a/packages/ibm-products/src/components/CoachmarkStack/CoachmarkStack.stories.jsx b/packages/ibm-products/src/components/CoachmarkStack/CoachmarkStack.stories.jsx index 2cb4168da4..53d395ec81 100644 --- a/packages/ibm-products/src/components/CoachmarkStack/CoachmarkStack.stories.jsx +++ b/packages/ibm-products/src/components/CoachmarkStack/CoachmarkStack.stories.jsx @@ -29,6 +29,7 @@ export default { }, media: { control: { type: null }, + description: 'Deprecated: Property replaced by "renderMedia"', }, portalTarget: { control: { type: null }, diff --git a/packages/ibm-products/src/components/CoachmarkStack/CoachmarkStackHome.tsx b/packages/ibm-products/src/components/CoachmarkStack/CoachmarkStackHome.tsx index 67cda9ddf7..6f69e96ae6 100644 --- a/packages/ibm-products/src/components/CoachmarkStack/CoachmarkStackHome.tsx +++ b/packages/ibm-products/src/components/CoachmarkStack/CoachmarkStackHome.tsx @@ -24,6 +24,7 @@ import { CoachmarkHeader } from '../Coachmark/CoachmarkHeader'; import { SteppedAnimatedMedia } from '../SteppedAnimatedMedia'; import { useIsomorphicEffect } from '../../global/js/hooks'; import { ButtonProps } from '@carbon/react'; +import { deprecateProp } from '../../global/js/utils/props-helper'; type Media = | { @@ -62,6 +63,10 @@ interface CoachmarkStackHomeProps { * @see {@link MEDIA_PROP_TYPE}. */ media?: Media; + /** + * Optional prop to render any media like images or any animated media. + */ + renderMedia?: (params) => ReactNode; /** * The labels used to link to the stackable Coachmarks. @@ -114,6 +119,7 @@ export let CoachmarkStackHome = forwardRef< description, isOpen, media, + renderMedia, navLinkLabels, onClickNavItem, onClose, @@ -126,6 +132,9 @@ export let CoachmarkStackHome = forwardRef< ) => { const buttonFocusRef = useRef | null>(null); const [linkFocusIndex, setLinkFocusIndex] = useState(0); + + const hasMedia = media || renderMedia; + useEffect(() => { setTimeout(() => { if (isOpen && buttonFocusRef.current) { @@ -190,20 +199,23 @@ export let CoachmarkStackHome = forwardRef< />
- {!media && ( + {!hasMedia && ( )} - {media && - (media.render ? ( - media.render() - ) : ( - - ))} + {hasMedia && media?.render && media.render()} + {hasMedia && media?.filePaths && ( + + )} + {hasMedia && renderMedia && ( +
+ {renderMedia({ playStep: 0 })} +
+ )}
{title && ( @@ -286,15 +298,20 @@ CoachmarkStackHome.propTypes = { * If a stepped animation is required, use `{filePaths}`. * * @see {@link MEDIA_PROP_TYPE}. + * @deprecated please use the `renderMedia` prop */ - media: PropTypes.oneOfType([ - PropTypes.shape({ - render: PropTypes.func, - }), - PropTypes.shape({ - filePaths: PropTypes.arrayOf(PropTypes.string), - }), - ]) as PropTypes.Validator, + media: deprecateProp( + PropTypes.oneOfType([ + PropTypes.shape({ + render: PropTypes.func, + }), + PropTypes.shape({ + filePaths: PropTypes.arrayOf(PropTypes.string), + }), + ]), + '' + ) as PropTypes.Validator, + /** * The labels used to link to the stackable Coachmarks. */ @@ -318,6 +335,10 @@ CoachmarkStackHome.propTypes = { * element is hidden or component is unmounted, the CoachmarkStackHome will disappear. */ portalTarget: PropTypes.string, + /** + * Optional prop to render any media like images or animated media. + */ + renderMedia: PropTypes.func, /** * The title of the Coachmark. diff --git a/packages/ibm-products/src/components/InlineTip/InlineTip.stories.jsx b/packages/ibm-products/src/components/InlineTip/InlineTip.stories.jsx index 038add24b1..8a42605c3d 100644 --- a/packages/ibm-products/src/components/InlineTip/InlineTip.stories.jsx +++ b/packages/ibm-products/src/components/InlineTip/InlineTip.stories.jsx @@ -19,6 +19,7 @@ const InlineTipAnimation = new URL( import.meta.url ).pathname; import DocsPage from './InlineTip.docs-page'; +import { SteppedAnimatedMedia } from '../SteppedAnimatedMedia'; export default { title: 'Experimental/Onboarding/Inline tip/InlineTip', @@ -36,13 +37,17 @@ export default { options: ['None', '', ''], control: { type: 'radio' }, }, - media: { + renderMedia: { options: ['None', 'Render a static image', 'Render an animation'], control: { type: 'radio' }, }, narrow: { control: { type: null }, }, + media: { + control: { type: null }, + description: 'Deprecated: Property replaced by "renderMedia"', + }, }, }; @@ -68,7 +73,7 @@ const defaultProps = { collapsible: false, action: 'None', expandButtonLabel: 'Read more', - media: 'None', + renderMedia: 'None', onClick: () => { action(`Clicked the tertiary button`)(); }, @@ -80,16 +85,18 @@ const defaultProps = { }; const Template = (args) => { - const { media, narrow, action: componentAction } = args; + const { renderMedia, narrow, action: componentAction } = args; const selectedMedia = (function () { - switch (media) { + switch (renderMedia) { case 'Render a static image': - return { render: () => }; + return () => ; + case 'Render an animation': - return { - filePaths: [InlineTipAnimation], - }; + return () => ( + + ); + default: return null; } @@ -129,7 +136,11 @@ const Template = (args) => { narrow ? 'storybook--inline-tip-narrow' : 'storybook--inline-tip-wide', ])} > - +
); }; diff --git a/packages/ibm-products/src/components/InlineTip/InlineTip.test.js b/packages/ibm-products/src/components/InlineTip/InlineTip.test.js index 5e68b785bf..102b43c692 100644 --- a/packages/ibm-products/src/components/InlineTip/InlineTip.test.js +++ b/packages/ibm-products/src/components/InlineTip/InlineTip.test.js @@ -139,7 +139,7 @@ describe(componentName, () => { expect(screen.getByText(readLessLabel)).toBeInTheDocument(); }); - it(`renders an image`, () => { + it(`renders an image with media prop`, () => { render( { ); expect(screen.getByRole('img')).toBeInTheDocument(); }); + it(`renders an image`, () => { + render( + img} + > + {children} + + ); + expect(screen.getByRole('img')).toBeInTheDocument(); + }); it(`renders in the narrow format`, () => { render( diff --git a/packages/ibm-products/src/components/InlineTip/InlineTip.tsx b/packages/ibm-products/src/components/InlineTip/InlineTip.tsx index 82ba2a2c06..fd85e9eedc 100644 --- a/packages/ibm-products/src/components/InlineTip/InlineTip.tsx +++ b/packages/ibm-products/src/components/InlineTip/InlineTip.tsx @@ -93,12 +93,18 @@ export interface InlineTipProps { * - If a stepped animation is required, use `{filePaths}`. * * Enabling `media` disables the `collapsible` feature. + * @deprecated please use the `renderMedia` prop */ media?: MediaType; + /** + * Optional prop to render any media like images or any animated media. + */ + renderMedia?: () => ReactNode; /** * Set to `true` to arrange the information in a format * that is easier to read in a limited space. */ + narrow?: boolean; /** * Function to call when the tertiary button is clicked. @@ -143,6 +149,7 @@ export let InlineTip = React.forwardRef( collapseButtonLabel = defaults.collapseButtonLabel, expandButtonLabel = defaults.expandButtonLabel, media, + renderMedia, narrow = defaults.narrow, onClick, onClose, @@ -153,6 +160,8 @@ export let InlineTip = React.forwardRef( }: PropsWithChildren, ref: ForwardedRef ) => { + const hasMedia = renderMedia || media; + const [isCollapsed, setIsCollapsed] = useState(collapsible); const labelId = useRef(uuidv4()).current; @@ -162,7 +171,7 @@ export let InlineTip = React.forwardRef( ); let childrenToRender = children; - if (!media && collapsible && isCollapsed) { + if (!hasMedia && collapsible && isCollapsed) { childrenToRender = (

{previewText}

); @@ -182,7 +191,7 @@ export let InlineTip = React.forwardRef( className, collapsible && `${blockClass}__collapsible`, isCollapsed && `${blockClass}__collapsible-collapsed`, - media && `${blockClass}__has-media`, + hasMedia && `${blockClass}__has-media`, [narrow ? `${blockClass}__narrow` : `${blockClass}__wide`], withLeftGutter && !narrow && `${blockClass}__with-left-gutter` )} @@ -203,7 +212,7 @@ export let InlineTip = React.forwardRef(
{/* Hide the idea icon if is narrow and showing an image */} - {((!media && narrow) || !narrow) && ( + {((!hasMedia && narrow) || !narrow) && (
@@ -224,7 +233,7 @@ export let InlineTip = React.forwardRef( {(collapsible || tertiaryButtonLabel) && (
{/* Disable the collapsible feature if an image is visible */} - {collapsible && !media && ( + {collapsible && !hasMedia && (
- {media && - (media.render ? ( -
{media.render()}
- ) : ( - - ))} + {hasMedia && media?.render && ( +
{media.render()}
+ )} + {hasMedia && media?.filePaths && ( + + )} + {hasMedia && renderMedia && ( +
{renderMedia()}
+ )} ); } @@ -309,6 +321,7 @@ InlineTip.propTypes = { * - If a stepped animation is required, use `{filePaths}`. * * Enabling `media` disables the `collapsible` feature. + * @deprecated please use the `renderMedia` prop */ /**@ts-ignore*/ media: PropTypes.oneOfType([ @@ -332,6 +345,10 @@ InlineTip.propTypes = { * Function to call when the InlineTip is closed via the "X" button. */ onClose: PropTypes.func, + /** + * Optional prop to render any media like images or animated media. + */ + renderMedia: PropTypes.func, /** * Defining the label will show a the tertiary button with the crossroads icon. * You will still need to define the `onClose` method to trigger a callback. diff --git a/packages/ibm-products/src/components/SteppedAnimatedMedia/SteppedAnimatedMedia.tsx b/packages/ibm-products/src/components/SteppedAnimatedMedia/SteppedAnimatedMedia.tsx index 57cadd0fe2..fec4de8517 100644 --- a/packages/ibm-products/src/components/SteppedAnimatedMedia/SteppedAnimatedMedia.tsx +++ b/packages/ibm-products/src/components/SteppedAnimatedMedia/SteppedAnimatedMedia.tsx @@ -68,6 +68,7 @@ export const SteppedAnimatedMedia = React.forwardRef( const localRef = ref ?? backupRef; const localRefValue = (localRef as MutableRefObject) .current; + const filePathStr = filePaths?.join(); //converting the array to string will avoid unwanted useEffect trigger. // load animation source useEffect(() => { const isJsonFile = (filePath) => filePath.includes('.json'); @@ -81,7 +82,8 @@ export const SteppedAnimatedMedia = React.forwardRef( } } loadArtifact(); - }, [filePaths]); + // eslint-disable-next-line + }, [filePathStr]); useEffect(() => { const prefersReducedMotion = window?.matchMedia