Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Coachmark): convert to .tsx #5097

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading