diff --git a/packages/ibm-products/src/components/Coachmark/Coachmark.js b/packages/ibm-products/src/components/Coachmark/Coachmark.tsx similarity index 77% rename from packages/ibm-products/src/components/Coachmark/Coachmark.js rename to packages/ibm-products/src/components/Coachmark/Coachmark.tsx index f4d47b75be..371f763d84 100644 --- a/packages/ibm-products/src/components/Coachmark/Coachmark.js +++ b/packages/ibm-products/src/components/Coachmark/Coachmark.tsx @@ -7,13 +7,15 @@ import React, { forwardRef, + MutableRefObject, + ReactNode, useEffect, useRef, useState, useCallback, } from 'react'; import cx from 'classnames'; -import PropTypes, { Component } from 'prop-types'; +import PropTypes from 'prop-types'; import { createPortal } from 'react-dom'; import { CoachmarkOverlay } from './CoachmarkOverlay'; import { CoachmarkContext } from './utils/context'; @@ -34,13 +36,84 @@ const defaults = { theme: 'light', }; +interface CoachmarkProps { + /** + * Where to render the Coachmark relative to its target. + * Applies only to Floating and Tooltip Coachmarks. + * @see COACHMARK_ALIGNMENT + */ + align?: + | 'bottom' + | 'bottom-left' + | 'bottom-right' + | 'left' + | 'left-top' + | 'left-bottom' + | 'right' + | 'right-top' + | 'right-bottom' + | 'top' + | 'top-left' + | 'top-right'; + + /** + * Coachmark should use a single CoachmarkOverlayElements component as a child. + * @see CoachmarkOverlayElements + */ + children: ReactNode; + /** + * Optional class name for this component. + */ + className?: string; + + /** + * Function to call when the Coachmark closes. + */ + onClose?: () => void; + /** + * Optional class name for the Coachmark Overlay component. + */ + overlayClassName?: string; + + /** + * What kind or style of Coachmark to render. + */ + overlayKind?: 'tooltip' | 'floating' | 'stacked'; + + overlayRef?: MutableRefObject; + + /** + * By default, the Coachmark will be appended to the end of `document.body`. + * The Coachmark will remain persistent as the user navigates the app until + * the user closes the Coachmark. + * + * Alternatively, the app developer can tightly couple the Coachmark to a DOM + * element or other component by specifying a CSS selector. The Coachmark will + * remain visible as long as that element remains visible or mounted. When the + * element is hidden or component is unmounted, the Coachmark will disappear. + */ + portalTarget?: string; + /** + * Fine tune the position of the target in pixels. Applies only to Beacons. + */ + positionTune?: { x: number; y: number } | object; + /** + * The optional button or beacon that the user will click to show the Coachmark. + */ + target: React.ReactNode; + /** + * Determines the theme of the component. + */ + theme?: 'light' | 'dark'; +} + /** * Coachmarks are used to call out specific functionality or concepts * within the UI that may not be intuitive but are important for the * user to gain understanding of the product's main value and discover new use cases. */ -export let Coachmark = forwardRef( +export let Coachmark = forwardRef( ( { align = defaults.align, @@ -172,7 +245,7 @@ export let Coachmark = forwardRef(
} { // Pass through any other property values as HTML attributes. ...rest @@ -183,7 +256,7 @@ export let Coachmark = forwardRef( {isOpen && createPortal( } fixedIsVisible={false} kind={overlayKind} onClose={handleClose} @@ -195,7 +268,8 @@ export let Coachmark = forwardRef( > {children} , - portalNode + // Default to `document.body` when `portalNode` is `null` + portalNode || document.body )}
@@ -258,12 +332,11 @@ Coachmark.propTypes = { */ overlayKind: PropTypes.oneOf(['tooltip', 'floating', 'stacked']), - overlayRef: PropTypes.oneOfType([ - // Either a function - PropTypes.func, - // Or the instance of a DOM native element (see the note about SSR) - PropTypes.shape({ current: PropTypes.instanceOf(Component) }), - ]), + overlayRef: PropTypes.shape({ + current: PropTypes.instanceOf( + HTMLElement + ) as PropTypes.Validator, + }), /** * By default, the Coachmark will be appended to the end of `document.body`. diff --git a/packages/ibm-products/src/components/Coachmark/CoachmarkDragbar.js b/packages/ibm-products/src/components/Coachmark/CoachmarkDragbar.tsx similarity index 82% rename from packages/ibm-products/src/components/Coachmark/CoachmarkDragbar.js rename to packages/ibm-products/src/components/Coachmark/CoachmarkDragbar.tsx index 5b9c9f9938..e2cd077587 100644 --- a/packages/ibm-products/src/components/Coachmark/CoachmarkDragbar.js +++ b/packages/ibm-products/src/components/Coachmark/CoachmarkDragbar.tsx @@ -32,11 +32,46 @@ const defaults = { theme: 'light', }; +interface CoachmarkDragbarProps { + /** + * Handler to manage keyboard interactions with the dragbar. + */ + a11yKeyboardHandler: (event: React.KeyboardEvent) => void; + /** + * Tooltip text and aria label for the Close button icon. + */ + closeIconDescription?: string; + /** + * Function to call when the close button is clicked. + */ + onClose?: () => void; + /** + * Function to call when the user clicks and drags the Coachmark. + * For internal use only by the parent CoachmarkOverlay. + */ + onDrag?: (movementX: number, movementY: number) => void; + /** + * Show/hide the "X" close button. + */ + showCloseButton?: boolean; + /** + * Determines the theme of the component. + */ + theme?: 'light' | 'dark'; + /** + * Additional props passed to the component. + */ + [key: string]: any; +} + /** * DO NOT USE. This component is for the exclusive use * of other Novice to Pro components. */ -export let CoachmarkDragbar = React.forwardRef( +export let CoachmarkDragbar = React.forwardRef< + HTMLElement, + CoachmarkDragbarProps +>( ( { a11yKeyboardHandler, @@ -96,6 +131,8 @@ export let CoachmarkDragbar = React.forwardRef( className={`${overlayBlockClass}__handle`} onMouseDown={handleDragStart} onKeyDown={a11yKeyboardHandler} + // TODO: i18n + title="Drag Handle" > diff --git a/packages/ibm-products/src/components/Coachmark/CoachmarkHeader.js b/packages/ibm-products/src/components/Coachmark/CoachmarkHeader.tsx similarity index 85% rename from packages/ibm-products/src/components/Coachmark/CoachmarkHeader.js rename to packages/ibm-products/src/components/Coachmark/CoachmarkHeader.tsx index 357ef35163..c94fcfc562 100644 --- a/packages/ibm-products/src/components/Coachmark/CoachmarkHeader.js +++ b/packages/ibm-products/src/components/Coachmark/CoachmarkHeader.tsx @@ -29,11 +29,33 @@ const defaults = { theme: 'light', }; +interface CoachmarkHeaderProps { + /** + * Tooltip text and aria label for the Close button icon. + */ + closeIconDescription?: string; + /** + * Function to call when the close button is clicked. + */ + onClose?: () => void; + /** + * Show/hide the "X" close button. + */ + showCloseButton?: boolean; + /** + * Determines the theme of the component. + */ + theme?: 'light' | 'dark'; +} + /** * DO NOT USE. This component is for the exclusive use * of other Novice to Pro components. */ -export let CoachmarkHeader = React.forwardRef( +export let CoachmarkHeader = React.forwardRef< + HTMLElement, + CoachmarkHeaderProps +>( ( { closeIconDescription = defaults.closeIconDescription, diff --git a/packages/ibm-products/src/components/Coachmark/CoachmarkOverlay.js b/packages/ibm-products/src/components/Coachmark/CoachmarkOverlay.tsx similarity index 77% rename from packages/ibm-products/src/components/Coachmark/CoachmarkOverlay.js rename to packages/ibm-products/src/components/Coachmark/CoachmarkOverlay.tsx index 5f34d64725..445c2d8c8e 100644 --- a/packages/ibm-products/src/components/Coachmark/CoachmarkOverlay.js +++ b/packages/ibm-products/src/components/Coachmark/CoachmarkOverlay.tsx @@ -6,7 +6,13 @@ */ // Import portions of React that are needed. -import React, { forwardRef, useRef, useState, useEffect } from 'react'; +import React, { + forwardRef, + useRef, + useState, + useEffect, + ReactNode, +} from 'react'; import uuidv4 from '../../global/js/utils/uuidv4'; // Other standard imports. import PropTypes from 'prop-types'; @@ -32,11 +38,49 @@ const defaults = { theme: 'light', }; +interface CoachmarkOverlayProps { + /** + * The CoachmarkOverlayElements child components. + * Validation is handled in the parent Coachmark component. + */ + children: ReactNode; + /** + * Optional class name for this component. + */ + className?: string; + /** + * The visibility of CoachmarkOverlay is + * managed in the parent Coachmark component. + */ + fixedIsVisible: boolean; + /** + * What kind or style of Coachmark to render. + */ + kind?: COACHMARK_OVERLAY_KIND; + /** + * Function to call when the Coachmark closes. + */ + onClose: () => void; + /** + * Determines the theme of the component. + */ + theme?: 'light' | 'dark'; + /** + * Additional props passed to the component. + */ + [key: string]: any; +} + +type StyledTune = { + left?: number; + top?: number; +}; + /** * DO NOT USE. This component is for the exclusive use * of other Novice to Pro components. */ -export let CoachmarkOverlay = forwardRef( +export let CoachmarkOverlay = forwardRef( ( { children, @@ -51,7 +95,7 @@ export let CoachmarkOverlay = forwardRef( ) => { const { winHeight, winWidth } = useWindowDimensions(); const [a11yDragMode, setA11yDragMode] = useState(false); - const overlayRef = useRef(); + const overlayRef = useRef(null); const coachmark = useCoachmark(); const isBeacon = kind === COACHMARK_OVERLAY_KIND.TOOLTIP; const isDraggable = kind === COACHMARK_OVERLAY_KIND.FLOATING; @@ -82,25 +126,25 @@ export let CoachmarkOverlay = forwardRef( } }; - let styledTune = {}; + const styledTune: StyledTune = {}; if (isBeacon || isDraggable) { if (coachmark.targetRect) { - styledTune = { - left: coachmark.targetRect.x + window.scrollX, - top: coachmark.targetRect.y + window.scrollY, - }; + styledTune.left = coachmark.targetRect.x + window.scrollX; + styledTune.top = coachmark.targetRect.y + window.scrollY; } - - if (isBeacon) { - // Compensate for radius of beacon - styledTune.left += 16; - styledTune.top += 16; - } else if (isDraggable) { - // Compensate for width and height of target element - const offsetTune = getOffsetTune(coachmark, kind); - styledTune.left += offsetTune.left; - styledTune.top += offsetTune.top; + if (styledTune.left && styledTune.top) { + if (isBeacon) { + // Compensate for radius of beacon + styledTune.left += 16; + styledTune.top += 16; + } + if (isDraggable) { + // Compensate for width and height of target element + const offsetTune = getOffsetTune(coachmark, kind); + styledTune.left += offsetTune.left; + styledTune.top += offsetTune.top; + } } } @@ -125,6 +169,9 @@ export let CoachmarkOverlay = forwardRef( function handleDrag(movementX, movementY) { const overlay = overlayRef.current; + if (!overlay) { + return; + } const { x, y } = overlay.getBoundingClientRect(); const { targetX, targetY } = handleDragBounds( @@ -170,9 +217,11 @@ export let CoachmarkOverlay = forwardRef( )}
- {React.Children.map(children, (child) => - React.cloneElement(child, { isVisible }) - )} + {React.Children.map(children, (child) => { + return React.cloneElement(child as React.ReactElement, { + isVisible, + }); + })}
{isBeacon && } diff --git a/packages/ibm-products/src/components/Coachmark/CoachmarkTagline.js b/packages/ibm-products/src/components/Coachmark/CoachmarkTagline.tsx similarity index 86% rename from packages/ibm-products/src/components/Coachmark/CoachmarkTagline.js rename to packages/ibm-products/src/components/Coachmark/CoachmarkTagline.tsx index c4dec4617a..50f9eddb75 100644 --- a/packages/ibm-products/src/components/Coachmark/CoachmarkTagline.js +++ b/packages/ibm-products/src/components/Coachmark/CoachmarkTagline.tsx @@ -27,11 +27,33 @@ const defaults = { theme: 'light', }; +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; + /** + * Determines the theme of the component. + */ + theme?: 'light' | 'dark'; + /** + * The title of the tagline. + */ + title: string; +} + /** * DO NOT USE. This component is for the exclusive use * of other Novice to Pro components. */ -export let CoachmarkTagline = React.forwardRef( +export let CoachmarkTagline = React.forwardRef< + HTMLDivElement, + CoachmarkTaglineProps +>( ( { closeIconDescription = defaults.closeIconDescription, diff --git a/packages/ibm-products/src/components/Coachmark/utils/enums.ts b/packages/ibm-products/src/components/Coachmark/utils/enums.ts index fd9e1ffdf3..6047b3c034 100644 --- a/packages/ibm-products/src/components/Coachmark/utils/enums.ts +++ b/packages/ibm-products/src/components/Coachmark/utils/enums.ts @@ -14,27 +14,27 @@ export enum BEACON_KIND { * @param FIXED is fixed to the bottom-right of the viewport. * @param STACKED is fixed to the bottom-right of the viewport, includes links to show more, stackable Coachmarks if included. */ -export const COACHMARK_OVERLAY_KIND = { - TOOLTIP: 'tooltip', - FLOATING: 'floating', - FIXED: 'fixed', - STACKED: 'stacked', -}; +export enum COACHMARK_OVERLAY_KIND { + TOOLTIP = 'tooltip', + FLOATING = 'floating', + FIXED = 'fixed', + STACKED = 'stacked', +} /** * Where to render the Coachmark relative to its target. * Applies only to Floating and Tooltip Coachmarks. */ -export const COACHMARK_ALIGNMENT = { - BOTTOM: 'bottom', - BOTTOM_LEFT: 'bottom-left', - BOTTOM_RIGHT: 'bottom-right', - LEFT: 'left', - LEFT_TOP: 'left-top', - LEFT_BOTTOM: 'left-bottom', - RIGHT: 'right', - RIGHT_TOP: 'right-top', - RIGHT_BOTTOM: 'right-bottom', - TOP: 'top', - TOP_LEFT: 'top-left', - TOP_RIGHT: 'top-right', -}; +export enum COACHMARK_ALIGNMENT { + BOTTOM = 'bottom', + BOTTOM_LEFT = 'bottom-left', + BOTTOM_RIGHT = 'bottom-right', + LEFT = 'left', + LEFT_TOP = 'left-top', + LEFT_BOTTOM = 'left-bottom', + RIGHT = 'right', + RIGHT_TOP = 'right-top', + RIGHT_BOTTOM = 'right-bottom', + TOP = 'top', + TOP_LEFT = 'top-left', + TOP_RIGHT = 'top-right', +}