Skip to content

Commit

Permalink
feat(Coachmark): convert to .tsx (#5097)
Browse files Browse the repository at this point in the history
* feat(Coachmark): convert enums to TS

* feat(Coachmark): convert CoachmarkDragbar to TS

* feat(Coachmark): convert CoachmarkHeader to TS

* feat(Coachmark): convert CoachmarkOverlay to TS

* feat(Coachmark): convert CoachmarkTagline to TS

* feat(Coachmark): convert Coachmark to TS
  • Loading branch information
emyarod authored May 9, 2024
1 parent b8e266b commit 2c4d66f
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<HTMLElement | null>;

/**
* 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<HTMLElement, CoachmarkProps>(
(
{
align = defaults.align,
Expand Down Expand Up @@ -172,7 +245,7 @@ export let Coachmark = forwardRef(
<CoachmarkContext.Provider value={contextValue}>
<div
className={cx(blockClass, `${blockClass}__${theme}`, className)}
ref={_coachmarkRef}
ref={_coachmarkRef as MutableRefObject<HTMLDivElement | null>}
{
// Pass through any other property values as HTML attributes.
...rest
Expand All @@ -183,7 +256,7 @@ export let Coachmark = forwardRef(
{isOpen &&
createPortal(
<CoachmarkOverlay
ref={_overlayRef}
ref={_overlayRef as MutableRefObject<HTMLDivElement | null>}
fixedIsVisible={false}
kind={overlayKind}
onClose={handleClose}
Expand All @@ -195,7 +268,8 @@ export let Coachmark = forwardRef(
>
{children}
</CoachmarkOverlay>,
portalNode
// Default to `document.body` when `portalNode` is `null`
portalNode || document.body
)}
</div>
</CoachmarkContext.Provider>
Expand Down Expand Up @@ -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<HTMLElement | null>,
}),

/**
* By default, the Coachmark will be appended to the end of `document.body`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,46 @@ const defaults = {
theme: 'light',
};

interface CoachmarkDragbarProps {
/**
* Handler to manage keyboard interactions with the dragbar.
*/
a11yKeyboardHandler: (event: React.KeyboardEvent<HTMLButtonElement>) => 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,
Expand Down Expand Up @@ -96,6 +131,8 @@ export let CoachmarkDragbar = React.forwardRef(
className={`${overlayBlockClass}__handle`}
onMouseDown={handleDragStart}
onKeyDown={a11yKeyboardHandler}
// TODO: i18n
title="Drag Handle"
>
<Draggable size="16" />
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<HTMLDivElement, CoachmarkOverlayProps>(
(
{
children,
Expand All @@ -51,7 +95,7 @@ export let CoachmarkOverlay = forwardRef(
) => {
const { winHeight, winWidth } = useWindowDimensions();
const [a11yDragMode, setA11yDragMode] = useState(false);
const overlayRef = useRef();
const overlayRef = useRef<HTMLDivElement>(null);
const coachmark = useCoachmark();
const isBeacon = kind === COACHMARK_OVERLAY_KIND.TOOLTIP;
const isDraggable = kind === COACHMARK_OVERLAY_KIND.FLOATING;
Expand Down Expand Up @@ -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;
}
}
}

Expand All @@ -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(
Expand Down Expand Up @@ -170,9 +217,11 @@ export let CoachmarkOverlay = forwardRef(
<CoachmarkHeader onClose={onClose} />
)}
<div className={`${blockClass}__body`} ref={ref} id={contentId}>
{React.Children.map(children, (child) =>
React.cloneElement(child, { isVisible })
)}
{React.Children.map(children, (child) => {
return React.cloneElement(child as React.ReactElement<any>, {
isVisible,
});
})}
</div>
{isBeacon && <span className={`${blockClass}__caret`} />}
</div>
Expand Down
Loading

0 comments on commit 2c4d66f

Please sign in to comment.