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(CoachmarkStack): convert to .tsx #5234

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 @@ -114,7 +114,7 @@ export let CoachmarkOverlayElements = React.forwardRef<
},
ref
) => {
const buttonFocusRef = useRef<ButtonProps>();
const buttonFocusRef = useRef<ButtonProps<any>>();
const scrollRef = useRef<CarouselProps>();
const [scrollPosition, setScrollPosition] = useState(0);
const [currentProgStep, _setCurrentProgStep] = useState(0);
Expand All @@ -123,7 +123,7 @@ export let CoachmarkOverlayElements = React.forwardRef<
const setCurrentProgStep = (value) => {
if (currentProgStep > 0 && value === 0 && buttonFocusRef.current) {
setTimeout(() => {
buttonFocusRef.current.focus();
buttonFocusRef.current?.focus();
}, 1000);
}
_setCurrentProgStep(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import React, {
useRef,
useState,
useCallback,
ReactNode,
} from 'react';
import { createPortal } from 'react-dom';

Expand All @@ -37,10 +38,73 @@ import { CoachmarkContext } from '../Coachmark/utils/context';
import { COACHMARK_OVERLAY_KIND } from '../Coachmark/utils/enums';
import { useIsomorphicEffect } from '../../global/js/hooks';

type Media =
| {
render?: () => ReactNode;
}
| {
filePaths?: string[];
};
interface CoachmarkStackProps {
/**
* CoachmarkStack should use a single CoachmarkOverlayElements component as a child.
*/
children: ReactNode;
/**
* Provide an optional class to be applied to the containing node.
*/
className?: string;
/**
* The label for the button that will close the Stack
*/
closeButtonLabel?: string;
// Pass through to CoachmarkStackHome
/**
* The description of the Coachmark.
*/
description: ReactNode;
/**
* 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}`.
*
* @see {@link MEDIA_PROP_TYPE}.
*/
media?: Media;
/**
* The labels used to link to the stackable Coachmarks.
*/
navLinkLabels: string[];
/**
* Function to call when the CoachmarkStack closes.
*/
onClose?: () => void;
/**
* Where in the DOM to render the stack.
* The default is `document.body`.
*/
portalTarget?: string;
/**
* The tagline title which will be fixed to the bottom right of the window and will serve as the display trigger.
*/
tagline: string;
/**
* Determines the theme of the component.
*/
theme?: 'light' | 'dark';
/**
* The title of the Coachmark.
*/
title: string;
}

const defaults = {
onClose: () => {},
// Pass through to CoachmarkStackHome
theme: 'light',
portalTarget: 'body',
};

// NOTE
Expand All @@ -56,7 +120,10 @@ const defaults = {
* user to gain understanding of the product's main value and discover new use cases.
* This variant allows the stacking of multiple coachmark overlays to be displayed by interacting with the tagline.
*/
export let CoachmarkStack = React.forwardRef(
export let CoachmarkStack = React.forwardRef<
HTMLDivElement,
CoachmarkStackProps
>(
(
{
children,
Expand All @@ -75,7 +142,7 @@ export let CoachmarkStack = React.forwardRef(
},
ref
) => {
const portalNode = useRef();
const portalNode = useRef<HTMLBodyElement | null>(null);

useIsomorphicEffect(() => {
portalNode.current = portalTarget
Expand All @@ -84,18 +151,18 @@ export let CoachmarkStack = React.forwardRef(
: document?.querySelector('body');
}, [portalTarget]);

const stackHomeRef = useRef();
const stackedCoachmarkRefs = useRef([]);
const stackHomeRef = useRef<HTMLDivElement | null>(null);
const stackedCoachmarkRefs = useRef<HTMLDivElement[]>([]);
const [isOpen, setIsOpen] = useState(false);
// selectedItemNumber -1 = parent close button was clicked, remove entire stack
// selectedItemNumber 0 = (default) the parent is visible, all children are hidden
// selectedItemNumber 1+ = a child is visible and stacked atop the parent
const [selectedItemNumber, setSelectedItemNumber] = useState(0);
// // The parent height and width values to return to after unstacked
const [parentHeight, setParentHeight] = useState(null);
const [parentHeight, setParentHeight] = useState<number>();
// parent height = child height when stacked behind a child that is shorter
const childArray = Children.toArray(children);
const mountedRef = useRef();
const mountedRef = useRef<boolean>();
// same value as CSS animation speed
const delayMs = 240;

Expand Down Expand Up @@ -176,17 +243,23 @@ export let CoachmarkStack = React.forwardRef(
if (!parentHeight) {
return;
}
stackHomeRef.current.style.height = `${parentHeight}px`;
if (stackHomeRef.current) {
stackHomeRef.current.style.height = `${parentHeight}px`;
}
if (!isOpen || targetSelectedItem < 0) {
stackHomeRef.current.focus();
if (stackHomeRef.current) {
stackHomeRef.current.focus();
}
return;
}

const targetHomeHeight =
stackedCoachmarkRefs.current[targetSelectedItem].clientHeight;

stackHomeRef.current.style.height = `${targetHomeHeight}px`;
stackedCoachmarkRefs.current[targetSelectedItem].focus();
if (stackHomeRef.current) {
stackHomeRef.current.style.height = `${targetHomeHeight}px`;
stackedCoachmarkRefs.current[targetSelectedItem].focus();
}
}, [selectedItemNumber, isOpen, parentHeight]);

const wrappedChildren = Children.map(childArray, (child, idx) => {
Expand All @@ -196,7 +269,9 @@ export let CoachmarkStack = React.forwardRef(
return (
<CoachmarkOverlay
key={idx}
ref={(ref) => (stackedCoachmarkRefs.current[idx] = ref)}
ref={(ref) =>
(stackedCoachmarkRefs.current[idx] = ref as HTMLDivElement)
}
kind={COACHMARK_OVERLAY_KIND.STACKED}
onClose={() => handleClose(false)}
theme={theme}
Expand Down Expand Up @@ -312,12 +387,12 @@ CoachmarkStack.propTypes = {
PropTypes.shape({
filePaths: PropTypes.arrayOf(PropTypes.string),
}),
]),
]) as PropTypes.Validator<Media>,

/**
* The labels used to link to the stackable Coachmarks.
*/
navLinkLabels: PropTypes.arrayOf(PropTypes.string).isRequired,
navLinkLabels: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,

/**
* Function to call when the CoachmarkStack closes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
* LICENSE file in the root directory of this source tree.
*/

import React, { forwardRef, useRef, useEffect, useState } from 'react';
import React, {
forwardRef,
useRef,
useEffect,
useState,
ReactNode,
} from 'react';
import pconsole from '../../global/js/utils/pconsole';
import PropTypes from 'prop-types';
import cx from 'classnames';
Expand All @@ -17,6 +23,74 @@ import { createPortal } from 'react-dom';
import { CoachmarkHeader } from '../Coachmark/CoachmarkHeader';
import { SteppedAnimatedMedia } from '../SteppedAnimatedMedia';
import { useIsomorphicEffect } from '../../global/js/hooks';
import { ButtonProps } from '@carbon/react';

type Media =
| {
render?: () => ReactNode;
filePaths?: never;
}
| {
render?: never;
filePaths?: string[];
};

interface CoachmarkStackHomeProps {
/**
* Optional class name for this component.
*/
className?: string;
/**
* The label for the button that will close the stack
*/
closeButtonLabel?: string;
/**
* The description of the Coachmark.
*/
description: React.ReactNode;
/**
* If the stack home is open.
*/
isOpen: boolean;
/**
* 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}`.
*
* @see {@link MEDIA_PROP_TYPE}.
*/
media?: Media;

/**
* The labels used to link to the stackable Coachmarks.
*/
navLinkLabels: string[];
/**
* For internal use only by CoachmarkStack and CoachmarkStackHome.
*/
onClickNavItem: (index: number) => void;
/**
* Function to call when the stack closes.
*/
onClose: () => void;
/**
* By default, the CoachmarkStackHome will be appended to the end of `document.body`.
* The CoachmarkStackHome will remain persistent as the user navigates the app until
* the user closes the CoachmarkStackHome.
*
* Alternatively, the app developer can tightly couple the CoachmarkStackHome to a DOM
* element or other component by specifying a CSS selector. The CoachmarkStackHome will
* remain visible as long as that element remains visible or mounted. When the
* element is hidden or component is unmounted, the CoachmarkStackHome will disappear.
*/
portalTarget?: string;
/**
* The title of the Coachmark.
*/
title: string;
}

// Carbon and package components we use.
/* TODO: @import(s) of carbon components and other package components. */
Expand All @@ -30,7 +104,10 @@ const componentName = 'CoachmarkStackHome';
* DO NOT USE. This component is for the exclusive use
* of other Onboarding components.
*/
export let CoachmarkStackHome = forwardRef(
export let CoachmarkStackHome = forwardRef<
HTMLDivElement,
CoachmarkStackHomeProps
>(
(
{
className,
Expand All @@ -47,7 +124,7 @@ export let CoachmarkStackHome = forwardRef(
},
ref
) => {
const buttonFocusRef = useRef();
const buttonFocusRef = useRef<ButtonProps<any> | null>(null);
const [linkFocusIndex, setLinkFocusIndex] = useState(0);
useEffect(() => {
setTimeout(() => {
Expand All @@ -57,7 +134,7 @@ export let CoachmarkStackHome = forwardRef(
}, 100);
}, [isOpen]);

const portalNode = useRef();
const portalNode = useRef<Element | null>(null);

useIsomorphicEffect(() => {
portalNode.current = portalTarget
Expand All @@ -72,6 +149,28 @@ export let CoachmarkStackHome = forwardRef(
);
}

function renderNavLink(
index,
label,
ref: React.RefObject<ButtonProps<any>> | null = null
) {
return (
<li key={index}>
<Button
kind="ghost"
size="sm"
onClick={() => {
setLinkFocusIndex(index);
onClickNavItem(index + 1);
}}
ref={ref}
>
{label}
</Button>
</li>
);
}

return portalNode?.current
? createPortal(
<div
Expand Down Expand Up @@ -124,9 +223,8 @@ export let CoachmarkStackHome = forwardRef(
{navLinkLabels.map((label, index) => {
if (index === linkFocusIndex) {
return renderNavLink(index, label, buttonFocusRef);
} else {
return renderNavLink(index, label);
}
return renderNavLink(index, label);
})}
</ul>
{closeButtonLabel && (
Expand All @@ -148,24 +246,6 @@ export let CoachmarkStackHome = forwardRef(
portalNode?.current
)
: null;

function renderNavLink(index, label, ref = null) {
return (
<li key={index}>
<Button
kind="ghost"
size="sm"
onClick={() => {
setLinkFocusIndex(index);
onClickNavItem(index + 1);
}}
ref={ref}
>
{label}
</Button>
</li>
);
}
}
);

Expand Down Expand Up @@ -215,11 +295,11 @@ CoachmarkStackHome.propTypes = {
PropTypes.shape({
filePaths: PropTypes.arrayOf(PropTypes.string),
}),
]),
]) as PropTypes.Validator<Media>,
/**
* The labels used to link to the stackable Coachmarks.
*/
navLinkLabels: PropTypes.arrayOf(PropTypes.string).isRequired,
navLinkLabels: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
/**
* For internal use only by CoachmarkStack and CoachmarkStackHome.
*/
Expand Down
Loading