From 0d32877464cca21e1f727fd18b26e721b33a233d Mon Sep 17 00:00:00 2001 From: Anamika T S Date: Thu, 25 Sep 2025 16:43:24 +0530 Subject: [PATCH 1/4] feat(coachmarkStacked): implemented as patterns --- .../Coachmark/next/Coachmark/Coachmark.tsx | 1 - .../next/Coachmark/CoachmarkContent.tsx | 12 +- .../CoachmarkTagline/CoachmarkTagline.test.js | 87 +++ .../CoachmarkTagline/CoachmarkTagline.tsx | 157 +++++ .../next/Coachmark/CoachmarkTagline/index.ts | 9 + .../src/global/js/utils/carousel/carousel.ts | 36 +- .../src/global/js/utils/carousel/types.ts | 1 + .../CoachmarkStacked/CoachmarkStacked.mdx | 38 ++ .../CoachmarkStacked.stories.jsx | 556 ++++++++++++++++++ .../CoachmarkStacked/_storybook-styles.scss | 185 ++++++ 10 files changed, 1066 insertions(+), 16 deletions(-) create mode 100644 packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkTagline/CoachmarkTagline.test.js create mode 100644 packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkTagline/CoachmarkTagline.tsx create mode 100644 packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkTagline/index.ts create mode 100644 packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.mdx create mode 100644 packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx create mode 100644 packages/ibm-products/src/patterns/CoachmarkStacked/_storybook-styles.scss diff --git a/packages/ibm-products/src/components/Coachmark/next/Coachmark/Coachmark.tsx b/packages/ibm-products/src/components/Coachmark/next/Coachmark/Coachmark.tsx index 2aac0a0f53c..97895297745 100644 --- a/packages/ibm-products/src/components/Coachmark/next/Coachmark/Coachmark.tsx +++ b/packages/ibm-products/src/components/Coachmark/next/Coachmark/Coachmark.tsx @@ -11,7 +11,6 @@ import React, { ReactNode, RefAttributes, RefObject, - createContext, forwardRef, useEffect, useRef, diff --git a/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkContent.tsx b/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkContent.tsx index d6baf1c3dd6..0d670ca9ebe 100644 --- a/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkContent.tsx +++ b/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkContent.tsx @@ -68,10 +68,10 @@ const CoachmarkContent = forwardRef( const targetId = open ? triggerRef?.current?.id : null; const handleRef = useRef(null); - const bubbleRef = ref && typeof ref !== 'function' ? ref : handleRef; + const bubbleRef = ref || handleRef; useEffect(() => { - if (open && bubbleRef.current) { + if (open && 'current' in bubbleRef && bubbleRef.current) { requestAnimationFrame(() => { const contentBody = bubbleRef.current?.querySelector( `.${contentBodyClass}` @@ -91,7 +91,8 @@ const CoachmarkContent = forwardRef( useEffect(() => { const handleOutsideClick = (event: MouseEvent) => { const targetElement = document.getElementById(targetId || ''); - const bubbleElement = bubbleRef.current; + const bubbleElement = + bubbleRef && 'current' in bubbleRef ? bubbleRef.current : null; if ( bubbleElement && @@ -122,7 +123,7 @@ const CoachmarkContent = forwardRef( }, [open, targetId, setOpen]); useEffect(() => { - if (open && bubbleRef.current) { + if (open && 'current' in bubbleRef && bubbleRef.current) { const dragContainer = bubbleRef.current.querySelector( `.${pkg.prefix}__bubble` ); @@ -131,7 +132,8 @@ const CoachmarkContent = forwardRef( } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open, bubbleRef.current]); + }, [open, bubbleRef]); + return ( open && (
diff --git a/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkTagline/CoachmarkTagline.test.js b/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkTagline/CoachmarkTagline.test.js new file mode 100644 index 00000000000..0267d34818d --- /dev/null +++ b/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkTagline/CoachmarkTagline.test.js @@ -0,0 +1,87 @@ +/** + * Copyright IBM Corp. 2024, 2025 + * + * 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 React 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'; +import { Coachmark } from '../Coachmark'; +import { CoachmarkTagline } from '.'; +import { Button } from '@carbon/react'; + +const blockClass = `${pkg.prefix}--coachmark-tagline`; +const componentName = CoachmarkTagline.displayName; + +// values to use +const childDataTestId = `CoachmarkTagline-${uuidv4()}`; +const className = `class-${uuidv4()}`; + +const renderCoachmarkWithTagline = ({ ...rest } = {}) => + render( + + + + + +

Hello World

+

this is a description test

+ +
+
+
+ ); + +describe(componentName, () => { + beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(() => {}); + }); + it('renders a component CoachmarkTagline', () => { + renderCoachmarkWithTagline(); + expect(screen.getByTestId(childDataTestId)).toHaveClass(blockClass); + }); + + it('has no accessibility violations', async () => { + const { container } = renderCoachmarkWithTagline(); + await expect(container).toBeAccessible(componentName); + await expect(container).toHaveNoAxeViolations(); + }); + + it('applies className to the containing node', () => { + renderCoachmarkWithTagline({ className }); + expect(screen.getByTestId(childDataTestId)).toHaveClass(className); + }); + + it('adds additional props to the containing node', () => { + const testingLabel = `testing-labels-${uuidv4()}`; + renderCoachmarkWithTagline({ + title: testingLabel, + }); + expect( + screen.getByRole('button', { name: testingLabel }) + ).toBeInTheDocument(); + }); + + it('forwards a ref to an appropriate node', () => { + const testRef = React.createRef(); + renderCoachmarkWithTagline({ ref: testRef }); + expect(testRef.current).toHaveClass(blockClass); + }); + + it('adds the Devtools attribute to the containing node', () => { + renderCoachmarkWithTagline(); + expect(screen.getByTestId(childDataTestId)).toHaveDevtoolsAttribute( + componentName + ); + }); +}); diff --git a/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkTagline/CoachmarkTagline.tsx b/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkTagline/CoachmarkTagline.tsx new file mode 100644 index 00000000000..e9d656c7899 --- /dev/null +++ b/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkTagline/CoachmarkTagline.tsx @@ -0,0 +1,157 @@ +/** + * Copyright IBM Corp. 2025 + * + * 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 portions of React that are needed. +import React, { ElementType } from 'react'; + +// Other standard imports. +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import { Close, Idea } from '@carbon/react/icons'; +import { Button, ButtonProps } from '@carbon/react'; +import { getDevtoolsProps } from '../../../../../global/js/utils/devtools'; +import { pkg } from '../../../../../settings'; + +// The block part of our conventional BEM class names (blockClass__E--M). +const blockClass = `${pkg.prefix}--coachmark-tagline`; +const componentName = 'CoachmarkTagline'; + +const defaults = { + onClose: () => {}, + closeIconDescription: 'Close', +}; + +export interface CoachmarkButtonProps extends ButtonProps { + onClick?(): void; + onDoubleClick?(): void; + tabIndex?: number; + ['aria-expanded']?: boolean; + id?: string; +} + +export interface CoachmarkTaglineProps { + /** + * Tooltip text and aria label for the Close button icon. + */ + closeIconDescription?: string; + /** + * Function to call when the close button is clicked. + */ + onClose?: () => void; + /** + * The title of the tagline. + */ + title: string; + /** + * button props + */ + buttonProps?: CoachmarkButtonProps; + /** + * Optional class name for this component. + */ + className?: string; + + isOpen?: boolean; +} + +/** + * DO NOT USE. This component is for the exclusive use + * of other Onboarding components. + */ +export let CoachmarkTagline = React.forwardRef< + HTMLDivElement, + CoachmarkTaglineProps +>( + ( + { + closeIconDescription = defaults.closeIconDescription, + onClose = defaults.onClose, + title, + buttonProps, + isOpen, + className, + ...rest + }, + ref + ) => { + return ( +
+ +
+
+
+ ); + } +); + +// Return a placeholder if not released and not enabled by feature flag +CoachmarkTagline = pkg.checkComponentEnabled(CoachmarkTagline, componentName); + +// The display name of the component, used by React. Note that displayName +// is used in preference to relying on function.name. +CoachmarkTagline.displayName = componentName; + +// The types and DocGen commentary for the component props, +// in alphabetical order (for consistency). +// See https://www.npmjs.com/package/prop-types#usage. +CoachmarkTagline.propTypes = { + /** + * button props + */ + buttonProps: PropTypes.shape({ + /**@ts-ignore*/ + ...Button.propTypes, + /**@ts-ignore*/ + id: PropTypes.string, + onClick: PropTypes.func, + onDoubleClick: PropTypes.func, + tabIndex: PropTypes.number, + ['aria-expanded']: PropTypes.bool, + }), + /** + * Optional class name for this component. + */ + className: PropTypes.string, + /** + * Tooltip text and aria label for the Close button icon. + */ + closeIconDescription: PropTypes.string, + /** + * Function to call when the close button is clicked. + */ + onClose: PropTypes.func, + /** + * The title of the tagline. + */ + title: PropTypes.string.isRequired, +}; diff --git a/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkTagline/index.ts b/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkTagline/index.ts new file mode 100644 index 00000000000..65cddc34e67 --- /dev/null +++ b/packages/ibm-products/src/components/Coachmark/next/Coachmark/CoachmarkTagline/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +export { CoachmarkTagline } from './CoachmarkTagline'; +export type { CoachmarkTaglineProps } from './CoachmarkTagline'; diff --git a/packages/ibm-products/src/global/js/utils/carousel/carousel.ts b/packages/ibm-products/src/global/js/utils/carousel/carousel.ts index 671ff77ea64..f349c8649c1 100644 --- a/packages/ibm-products/src/global/js/utils/carousel/carousel.ts +++ b/packages/ibm-products/src/global/js/utils/carousel/carousel.ts @@ -30,8 +30,12 @@ export const initCarousel = ( const minHeight = 4; // 10 rem - const { onViewChangeStart, onViewChangeEnd, excludeSwipeSupport } = - config || {}; + const { + onViewChangeStart, + onViewChangeEnd, + excludeSwipeSupport, + useMaxHeight, + } = config || {}; /** * Registers an HTMLElement at a specific index in the refs array. @@ -238,6 +242,8 @@ export const initCarousel = ( */ const performAnimation = (isInitial: boolean) => { let itemHeightSmallest = 0; + let itemHeightMaximum = 0; + Array.from(viewItems).forEach((viewItem: HTMLElement, index) => { const stackIndex = viewIndexStack.findIndex((idx) => idx === index); const stackIndexInstanceCount = previousViewIndexStack.filter( @@ -269,15 +275,25 @@ export const initCarousel = ( registerRef(index, viewItem); setTimeout(() => { - if ( - !itemHeightSmallest || - (viewItem.offsetHeight < itemHeightSmallest && - itemHeightSmallest > remToPx(minHeight)) - ) { - itemHeightSmallest = viewItem.offsetHeight; + if (useMaxHeight) { + const heights: number[] = Array.from(viewItems).map( + (viewItem) => viewItem.scrollHeight + ); + itemHeightMaximum = Math.max(...heights); + + viewItem.style.position = 'absolute'; + updateHeightForWrapper(itemHeightMaximum); + } else { + if ( + !itemHeightSmallest || + (viewItem.offsetHeight < itemHeightSmallest && + itemHeightSmallest > remToPx(minHeight)) + ) { + itemHeightSmallest = viewItem.offsetHeight; + } + viewItem.style.position = 'absolute'; + updateHeightForWrapper(itemHeightSmallest); } - viewItem.style.position = 'absolute'; - updateHeightForWrapper(itemHeightSmallest); }); const listener = (e: Event) => { diff --git a/packages/ibm-products/src/global/js/utils/carousel/types.ts b/packages/ibm-products/src/global/js/utils/carousel/types.ts index 8cf01f5e551..79e83412861 100644 --- a/packages/ibm-products/src/global/js/utils/carousel/types.ts +++ b/packages/ibm-products/src/global/js/utils/carousel/types.ts @@ -28,6 +28,7 @@ export type Config = { onViewChangeStart?: (args: CarouselResponse) => void; onViewChangeEnd?: (args: CarouselResponse) => void; excludeSwipeSupport?: boolean; + useMaxHeight?: boolean; }; interface InitCarousel { diff --git a/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.mdx b/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.mdx new file mode 100644 index 00000000000..2d1c780dd90 --- /dev/null +++ b/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.mdx @@ -0,0 +1,38 @@ +import { Story, Controls, Source, Canvas } from '@storybook/addon-docs/blocks'; +import { CodesandboxLink } from '../../global/js/utils/story-helper'; +import * as CoachmarkStackedStories from './CoachmarkStacked.stories'; +import { stackblitzPrefillConfig } from '../../../previewer/codePreviewer'; + +# CoachmarkStacked + +## Table of Contents + +- [Overview](#overview) +- [Example usage](#example-usage) + +## Overview + +The CoachmarkStacked pattern acts as a container element for onboarding and +should only be used within the scope of a Coachmark. + +To build this pattern, we recommend including the following components: + +- [Coachmark](https://carbon-for-ibm-products.netlify.app/?path=/docs/experimental-onboarding-coachmark-next--overview) +- [cds-button](https://web-components.carbondesignsystem.com/?path=/docs/components-button) +- Carousel utility + +## About Onboarding + +[Onboarding](https://pages.github.ibm.com/security/security-design/department/end-to-end-experiences/onboarding/overview/) +is a continuous learning methodology and framework that aims to orient, onboard, +explain, educate, and cultivate novice users into high-functioning power users. + +## Example usage + +{/* TODO: One example per designed use case. */} + + +
+ +
+
diff --git a/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx b/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx new file mode 100644 index 00000000000..b0c94b49bcc --- /dev/null +++ b/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx @@ -0,0 +1,556 @@ +/** + * Copyright IBM Corp. 2025 + * + * 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 React, { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from 'react'; +import { Button, Theme, Link as CarbonLink } from '@carbon/react'; +import styles from './_storybook-styles.scss?inline'; +import DocsPage from './CoachmarkStacked.mdx'; +import { Coachmark } from '../../components/Coachmark/next/Coachmark'; +import { + initCarousel, + InitCarousel, +} from '../../global/js/utils/carousel/carousel'; +import { CoachmarkTagline } from '../../components/Coachmark/next/Coachmark/CoachmarkTagline/CoachmarkTagline'; +import { Idea } from '@carbon/react/icons'; +import { pkg } from '../../settings'; +import cx from 'classnames'; + +export default { + title: 'Patterns/Coachmark Stacked', + component: () => {}, + tags: ['autodocs'], + parameters: { + styles, + docs: { + page: DocsPage, + }, + }, +}; + +//fetching theme +function useCarbonTheme() { + const [themeValue, setThemeValue] = useState(() => + document.documentElement.getAttribute('data-carbon-theme') + ); + + useEffect(() => { + const target = document.documentElement; + + // function to read the current theme + const readTheme = () => { + const newTheme = target.getAttribute('data-carbon-theme'); + setThemeValue((prev) => (prev !== newTheme ? newTheme : prev)); + }; + + const observer = new MutationObserver((mutationsList) => { + for (const mutation of mutationsList) { + if ( + mutation.type === 'attributes' && + mutation.attributeName === 'data-carbon-theme' + ) { + readTheme(); + } + } + }); + + observer.observe(target, { + attributes: true, + attributeFilter: ['data-carbon-theme'], + }); + + //fallback - check readTheme in every 200ms + const interval = setInterval(readTheme, 200); + + return () => { + observer.disconnect(); + clearInterval(interval); + }; + }, []); + + return themeValue; +} + +const CoachmarkStackedPattern = (args) => { + const carbonTheme = useCarbonTheme(); + const [isOpen, setIsOpen] = useState(true); + + const [currentViewIndex, setCurrentViewIndex] = useState(-1); + const [lastViewIndex, setLastViewIndex] = useState(-1); + //prettier-ignore + const [openId, setOpenId] = useState(0); + //prettier-ignore + const carouselContainerRef = useRef < HTMLDivElement > (null); + //prettier-ignore + const carouselInit = useRef < InitCarousel > (null); + //prettier-ignore + const [parentHeight, setParentHeight] = useState(0); + //prettier-ignore + const stackHomeContentRef = useRef(null); + //prettier-ignore + const stackedCoachmarkContentRefs = useRef([]); + + const blockClass = `coachmark-stacked-home`; + const stackedCoachmark = `stacked-coachmark`; + const elementBlockClass = `stacked_element_content`; + + const items = [ + { + id: 1, + label: 'Example 1', + }, + { + id: 2, + label: 'Example 2', + }, + { + id: 3, + label: 'Example 3', + }, + ]; + + const nestedItems = [ + { + id: 1, + title: 'Short Coachmark', + text: 'As small as it gets.', + type: 'simple', + button: ( + + ), + }, + { + id: 2, + type: 'carousel', + pages: [ + { + id: '1', + title: 'Mid-height Coachmark', + text: ( + <> + This should be about the same height as the base stack item. +
+
+ This is known as the enrichment phase. Enrichment supports you by + emulating how an analyst would evaluate a finding—for example, by + adding context, such as whether a certain piece of data is known + to be malicious, or is linked... + + ), + button: ( + Learn more + ), + }, + { + id: '2', + title: 'Hello World', + text: 'Link opens in new tab.', + button: ( + + {' '} + Learn more{' '} + + ), + }, + ], + }, + { + id: 3, + type: 'carousel', + pages: [ + { + id: '1', + title: 'Tall Coachmark', + text: ( + <> + These alerts contain data gathered from your connected security + systems. + + ), + }, + { + id: '2', + title: 'Alerts contain evidence, known as artifacts', + text: ( + <> + These help to determine whether the alert is good or bad. And as + alerts are added to a case, they become findings. + + ), + }, + { + id: '3', + title: 'Findings are enriched with more information and context', + text: ( + <> + This is known as the enrichment phase. Enrichment supports you by + emulating how an analyst would evaluate a finding—for example, by + adding context, such as whether a certain piece of data is known + to be malicious, or is linked to a known threat. +
+
+ Lets +
+
+ make +
+
+ this +
+
+ one +
+
+ really +
+
+ tall. + + ), + }, + { + id: '4', + title: 'Next, the correlation process takes place', + text: ( + <> + Based on the results of the enrichment process, findings that are + potentially related are grouped together, and then evaluated. + + ), + }, + { + id: '5', + title: + 'Between enrichment and correlation, the severity of a case is determined', + text: ( + <> + And once you know the severity, you can easily choose which case + to pick up next. + + ), + }, + ], + }, + ]; + + const handleClose = () => { + setIsOpen(false); + }; + + const handleCloseCarousel = (e) => { + setOpenId(null); + carouselInit.current.reset(); + e.stopPropagation(); + }; + + const handleTaglineClick = () => { + setIsOpen((isOpen) => !isOpen); + }; + + useEffect(() => { + if (carouselContainerRef && carouselContainerRef.current) { + carouselInit.current = initCarousel(carouselContainerRef.current, { + onViewChangeStart: onViewChangeStart, + onViewChangeEnd: onViewChangeEnd, + useMaxHeight: true, + }); + } + }, [carouselInit, openId]); + + const onViewChangeStart = () => {}; + const onViewChangeEnd = (options) => { + handleViewStackUpdate(options); + }; + + const handleViewStackUpdate = useCallback( + ({ currentIndex, lastIndex }) => { + setCurrentViewIndex(currentIndex); + setLastViewIndex(lastIndex); + }, + [openId] + ); + + const onNext = (e) => { + carouselInit.current.next(); + e.stopPropagation(); + }; + const onPrev = (e) => { + carouselInit.current.prev(); + e.stopPropagation(); + }; + + useLayoutEffect(() => { + if (!parentHeight) { + if (stackHomeContentRef.current) { + const stackHomeContent = + stackHomeContentRef.current.querySelector < + HTMLDivElement > + `div.${pkg.prefix}__bubble`; + if (stackHomeContent) { + const height = stackHomeContent.clientHeight; + + if (height > 0) { + setParentHeight(height); + } + } + } + return; + } + + if (stackHomeContentRef.current) { + const stackHomeContent = + stackHomeContentRef.current.querySelector < + HTMLDivElement > + `div.${pkg.prefix}__bubble`; + if (stackHomeContent) { + stackHomeContent.style.height = `${parentHeight}px`; + } + } + + if (!isOpen || openId <= 0) { + requestAnimationFrame(() => { + if (stackHomeContentRef.current) { + const stackHomeContent = + stackHomeContentRef.current.querySelector < + HTMLDivElement > + `div.${pkg.prefix}__bubble`; + + if (stackHomeContent) { + stackHomeContent.classList.remove(`${blockClass}--scaled-home`); + stackHomeContent.classList.add(`${blockClass}--unscaled-home`); + stackHomeContent.focus(); + } + } + return; + }, 0); + } + + if (openId > 0 && isOpen && stackedCoachmarkContentRefs.current) { + const container = stackedCoachmarkContentRefs.current[openId]; + console.log(container); + + const targetHome = Array.from( + container.querySelectorAll < + HTMLDivElement > + 'div.dev-prefix--c4p__bubble' + ).filter((bubble) => bubble.parentElement === container); + + if (targetHome.length > 0) { + setTimeout(() => { + targetHome.forEach((bubble) => { + requestAnimationFrame(() => { + const targetHomeHeight = bubble.clientHeight; + + const stackHomeContent = + stackHomeContentRef.current.querySelector < + HTMLDivElement > + `div.dev-prefix--c4p__bubble`; + if (stackHomeContent) { + stackHomeContent.style.height = `calc(${targetHomeHeight}px + 1px)`; + stackHomeContent.classList.add(`${blockClass}--scaled-home`); + stackHomeContent.classList.remove( + `${blockClass}--unscaled-home` + ); + stackedCoachmarkContentRefs.current[openId].focus(); + } + }); + }); + }, 0); + } + } + }, [openId, isOpen, parentHeight]); + + return ( + + + + + + +
+ +
+
+

Example title

+

+ This is an example of a description +

+
+
    + {items.map((item) => ( + +
  • + +
  • +
    + ))} +
+ +

+
+
+
+ {items.map((item) => ( + { + setOpenId(null); + e.stopPropagation(); + }} + align="top" + > + { + if (el) { + stackedCoachmarkContentRefs.current[item.id] = el; + } + }} + className={cx(`${elementBlockClass}`)} + > + + + {nestedItems + .filter((nested) => nested.id === item.id) + .map((nested) => { + if (nested.type === 'simple') { + return ( +
+

+ {nested.title} +

+

+ {nested.text} +

+
+ {nested.button} +
+
+ ); + } + if (nested.type === 'carousel') { + return ( + <> +
+ {nested.pages.map((page) => ( +
+

{page.title}

+

{page.text}

+

{page.button}

+
+ ))} +
+ +
+
+ {nested.pages.map((page, index) => { + if ( + carouselInit.current?.getActiveItem?.() + ?.index === index + ) + return ( + + {`${carouselInit.current?.getActiveItem?.()?.index + 1} / ${nested.pages.length}`} + + ); + })} +
+
+ {currentViewIndex !== 0 && ( + + )} + + {lastViewIndex !== currentViewIndex ? ( + + ) : ( + + )} +
+
+ + ); + } + return null; + })} +
+
+
+ ))} +
+ ); +}; + +export const CoachmarkStack = CoachmarkStackedPattern.bind({}); +CoachmarkStack.args = {}; diff --git a/packages/ibm-products/src/patterns/CoachmarkStacked/_storybook-styles.scss b/packages/ibm-products/src/patterns/CoachmarkStacked/_storybook-styles.scss new file mode 100644 index 00000000000..111eede4225 --- /dev/null +++ b/packages/ibm-products/src/patterns/CoachmarkStacked/_storybook-styles.scss @@ -0,0 +1,185 @@ +// +// Copyright IBM Corp. 2025 +// +// This source code is licensed under the Apache-2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +/* stylelint-disable declaration-no-important */ + +@use 'ALIAS_STORY_STYLE_CONFIG' as c4p-settings; +@use '@carbon/styles/scss/spacing' as *; +@use '@carbon/styles/scss/theme' as *; +@use '@carbon/styles/scss/themes' as *; +@use '@carbon/styles/scss/type'; +@use '../../global/js/utils/carousel/index' as carousel; +@use '@carbon/styles/scss/motion' as *; +@use '@carbon/layout/scss/convert'; +@use '@carbon/styles/scss/colors' as *; + +@use '@carbon/styles/scss/components/button/tokens' as *; + +@include carousel.carousel; + +$block-class: #{c4p-settings.$pkg-prefix}--coachmark__next; +$stack-home-class: 'coachmark-stacked-home'; +$stacked-coachmark: 'stacked-coachmark'; + +$elements-block-class: 'coachmark-stack-element'; + +// BUBBLE - content styling +.#{$block-class}--coachmark-content { + position: fixed; + inline-size: 18rem; + margin-inline-start: c4p-settings.$spacing-04; + visibility: visible; + + &.--is-visible { + visibility: visible; + } + + .#{$block-class}--content-body { + display: flex; + flex-direction: column; + } + + .#{$block-class}--content-body > h2 { + @include type.type-style('productive-heading-02'); + + margin: 0 0 $spacing-03; + } + + .#{$block-class}--content-body > p { + @include type.type-style('body-long-01'); + } + + .#{$block-class}--content-body > button { + display: flex; + align-self: flex-end; + margin-block-start: $spacing-05; + } +} + +.carousel__view > h2 { + @include type.type-style('productive-heading-02'); + + margin: 0 0 $spacing-03; +} + +.carousel__view > p { + @include type.type-style('body-long-01'); +} + +.carouselControls { + position: relative; + + .nextButton { + position: absolute; + inset-inline-start: $spacing-07; + } + + .prevButton { + position: absolute; + inset-inline-start: $spacing-07; + } +} + +.cds--content-switcher { + inline-size: 20rem; + margin-block: $spacing-09; + margin-inline: $spacing-07; +} + +.carouselControlWrapper__footer { + span { + justify-self: flex-start; + margin-inline-end: auto; + } +} + +.carouselControlWrapper__footer { + display: flex; + align-items: center; + justify-content: flex-end; + padding-block-start: c4p-settings.$spacing-05; +} + +.carouselControlWrapper--controls-progress { + justify-self: flex-start; + margin-inline-end: auto; + @include type.type-style('helper-text-01'); +} + +// SPECIFIC TO THE HOME ELEMENT +.#{$stack-home-class}, +.#{$stacked-coachmark} { + &__nav-links { + margin-block-start: $spacing-04; + margin-inline-start: calc(-1 * $spacing-05); + } + + &__icon-idea { + margin-block-end: $spacing-03; + } + + li { + display: block; + max-inline-size: 100%; + white-space: nowrap; + } + + &__title { + @include type.type-style('productive-heading-02'); + + margin: 0 0 $spacing-03; + } + + &__body { + @include type.type-style('body-long-01'); + } + + &__button { + margin-block-end: 0; + } +} + +.#{$stacked-coachmark} { + &__button { + display: flex; + align-items: center; + justify-content: flex-end; + padding: c4p-settings.$spacing-05 $spacing-05 0 $spacing-05; + } +} + +.#{c4p-settings.$pkg-prefix}__bubble__arrow { + display: none; +} + +$elements-block-class: 'stacked_element_content'; + +.#{$elements-block-class} { + inset: logical unset unset 0 0 !important; + inset-block-start: auto; + inset-inline-start: auto; +} +.#{c4p-settings.$pkg-prefix}--coachmark__next--coachmark-content { + inset-block-end: 0; + inset-block-start: unset !important; + inset-inline-end: c4p-settings.$spacing-05; + inset-inline-start: unset !important; + opacity: 1; + transform: none; +} + +.#{$stack-home-class}--scaled-home { + inset-block-end: c4p-settings.$spacing-05; + opacity: 0.8; + transform: scale(0.9); + transform-origin: top center; + transition: transform $duration-moderate-02 motion(exit, productive); +} + +.#{$stack-home-class}--unscaled-home { + transform: none; +} From f85a039aa2a1c818d2569a61af110bf02fb09cbc Mon Sep 17 00:00:00 2001 From: Anamika T S Date: Thu, 25 Sep 2025 16:50:48 +0530 Subject: [PATCH 2/4] feat(coachmarkStacked): implemented as patterns --- .../CoachmarkStacked.stories.jsx | 31 +++++++------------ .../CoachmarkStacked/_storybook-styles.scss | 10 +++--- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx b/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx index b0c94b49bcc..9e22505dda6 100644 --- a/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx +++ b/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx @@ -299,10 +299,8 @@ const CoachmarkStackedPattern = (args) => { useLayoutEffect(() => { if (!parentHeight) { if (stackHomeContentRef.current) { - const stackHomeContent = - stackHomeContentRef.current.querySelector < - HTMLDivElement > - `div.${pkg.prefix}__bubble`; + //prettier-ignore + const stackHomeContent = stackHomeContentRef.current.querySelector(`div.${pkg.prefix}__bubble`); if (stackHomeContent) { const height = stackHomeContent.clientHeight; @@ -315,10 +313,8 @@ const CoachmarkStackedPattern = (args) => { } if (stackHomeContentRef.current) { - const stackHomeContent = - stackHomeContentRef.current.querySelector < - HTMLDivElement > - `div.${pkg.prefix}__bubble`; + //prettier-ignore + const stackHomeContent = stackHomeContentRef.current.querySelector(`div.${pkg.prefix}__bubble`); if (stackHomeContent) { stackHomeContent.style.height = `${parentHeight}px`; } @@ -327,10 +323,8 @@ const CoachmarkStackedPattern = (args) => { if (!isOpen || openId <= 0) { requestAnimationFrame(() => { if (stackHomeContentRef.current) { - const stackHomeContent = - stackHomeContentRef.current.querySelector < - HTMLDivElement > - `div.${pkg.prefix}__bubble`; + //prettier-ignore + const stackHomeContent = stackHomeContentRef.current.querySelector(`div.${pkg.prefix}__bubble`); if (stackHomeContent) { stackHomeContent.classList.remove(`${blockClass}--scaled-home`); @@ -346,11 +340,10 @@ const CoachmarkStackedPattern = (args) => { const container = stackedCoachmarkContentRefs.current[openId]; console.log(container); + //prettier-ignore const targetHome = Array.from( - container.querySelectorAll < - HTMLDivElement > - 'div.dev-prefix--c4p__bubble' - ).filter((bubble) => bubble.parentElement === container); + container.querySelectorAll(`div.${pkg.prefix}__bubble`) + ).filter(bubble => bubble.parentElement === container); if (targetHome.length > 0) { setTimeout(() => { @@ -358,10 +351,8 @@ const CoachmarkStackedPattern = (args) => { requestAnimationFrame(() => { const targetHomeHeight = bubble.clientHeight; - const stackHomeContent = - stackHomeContentRef.current.querySelector < - HTMLDivElement > - `div.dev-prefix--c4p__bubble`; + //prettier-ignore + const stackHomeContent = stackHomeContentRef.current.querySelector(`div.dev-prefix--c4p__bubble`); if (stackHomeContent) { stackHomeContent.style.height = `calc(${targetHomeHeight}px + 1px)`; stackHomeContent.classList.add(`${blockClass}--scaled-home`); diff --git a/packages/ibm-products/src/patterns/CoachmarkStacked/_storybook-styles.scss b/packages/ibm-products/src/patterns/CoachmarkStacked/_storybook-styles.scss index 111eede4225..cbc54306e83 100644 --- a/packages/ibm-products/src/patterns/CoachmarkStacked/_storybook-styles.scss +++ b/packages/ibm-products/src/patterns/CoachmarkStacked/_storybook-styles.scss @@ -31,7 +31,7 @@ $elements-block-class: 'coachmark-stack-element'; .#{$block-class}--coachmark-content { position: fixed; inline-size: 18rem; - margin-inline-start: c4p-settings.$spacing-04; + margin-inline-start: $spacing-04; visibility: visible; &.--is-visible { @@ -101,7 +101,7 @@ $elements-block-class: 'coachmark-stack-element'; display: flex; align-items: center; justify-content: flex-end; - padding-block-start: c4p-settings.$spacing-05; + padding-block-start: $spacing-05; } .carouselControlWrapper--controls-progress { @@ -148,7 +148,7 @@ $elements-block-class: 'coachmark-stack-element'; display: flex; align-items: center; justify-content: flex-end; - padding: c4p-settings.$spacing-05 $spacing-05 0 $spacing-05; + padding: $spacing-05 $spacing-05 0 $spacing-05; } } @@ -166,14 +166,14 @@ $elements-block-class: 'stacked_element_content'; .#{c4p-settings.$pkg-prefix}--coachmark__next--coachmark-content { inset-block-end: 0; inset-block-start: unset !important; - inset-inline-end: c4p-settings.$spacing-05; + inset-inline-end: $spacing-05; inset-inline-start: unset !important; opacity: 1; transform: none; } .#{$stack-home-class}--scaled-home { - inset-block-end: c4p-settings.$spacing-05; + inset-block-end: $spacing-05; opacity: 0.8; transform: scale(0.9); transform-origin: top center; From c87c9e79a0b756a0afaacdda15a86d04de6b8a1a Mon Sep 17 00:00:00 2001 From: Anamika T S Date: Thu, 25 Sep 2025 19:12:55 +0530 Subject: [PATCH 3/4] feat(coachmarkStacked): add pkg.prefix --- .../src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx b/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx index 9e22505dda6..3af92bf83fd 100644 --- a/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx +++ b/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.stories.jsx @@ -338,7 +338,6 @@ const CoachmarkStackedPattern = (args) => { if (openId > 0 && isOpen && stackedCoachmarkContentRefs.current) { const container = stackedCoachmarkContentRefs.current[openId]; - console.log(container); //prettier-ignore const targetHome = Array.from( @@ -352,7 +351,7 @@ const CoachmarkStackedPattern = (args) => { const targetHomeHeight = bubble.clientHeight; //prettier-ignore - const stackHomeContent = stackHomeContentRef.current.querySelector(`div.dev-prefix--c4p__bubble`); + const stackHomeContent = stackHomeContentRef.current.querySelector(`div.${pkg.prefix}__bubble`); if (stackHomeContent) { stackHomeContent.style.height = `calc(${targetHomeHeight}px + 1px)`; stackHomeContent.classList.add(`${blockClass}--scaled-home`); From 10000f2871f480fb1e44291575b819f86ba6874b Mon Sep 17 00:00:00 2001 From: Anamika T S Date: Thu, 9 Oct 2025 13:26:43 +0530 Subject: [PATCH 4/4] feat: add documentation for carousel in mdx --- .../src/patterns/CoachmarkStacked/CoachmarkStacked.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.mdx b/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.mdx index 2d1c780dd90..d131a214171 100644 --- a/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.mdx +++ b/packages/ibm-products/src/patterns/CoachmarkStacked/CoachmarkStacked.mdx @@ -19,7 +19,7 @@ To build this pattern, we recommend including the following components: - [Coachmark](https://carbon-for-ibm-products.netlify.app/?path=/docs/experimental-onboarding-coachmark-next--overview) - [cds-button](https://web-components.carbondesignsystem.com/?path=/docs/components-button) -- Carousel utility +- [Carousel utility](https://github.com/carbon-design-system/carbon/blob/main/packages/utilities/src/carousel/README.md) ## About Onboarding