Skip to content

Commit

Permalink
feat(tooltip): add closeMode to hide instead of unmount when closed
Browse files Browse the repository at this point in the history
  • Loading branch information
gcornut committed Sep 26, 2024
1 parent 5619271 commit 653e87d
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 17 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `Slideshow`: Changed active pagination item width for better a11y.

### Changed

- `Tooltip`: Add `closeMode` to hide the tooltip instead of unmounting it

## [3.9.1][] - 2024-09-17

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions packages/lumx-core/src/scss/components/tooltip/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
border-radius: var(--lumx-border-radius);
will-change: transform;

&--is-hidden {
visibility: hidden;
}

&__arrow {
position: absolute;
width: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { mdiChevronDown, mdiChevronUp } from '@lumx/icons';

import { Emphasis, Icon, Size, IconButton, IconButtonProps } from '@lumx/react';

import { Comp, GenericProps, isComponent } from '@lumx/react/utils/type';
import { Comp, GenericProps, HasCloseMode, isComponent } from '@lumx/react/utils/type';
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
import { renderLink } from '@lumx/react/utils/renderLink';
import { renderButtonOrLink } from '@lumx/react/utils/renderButtonOrLink';
Expand All @@ -16,7 +16,7 @@ import { useId } from '@lumx/react/hooks/useId';
/**
* Defines the props of the component.
*/
export interface SideNavigationItemProps extends GenericProps {
export interface SideNavigationItemProps extends GenericProps, HasCloseMode {
/** SideNavigationItem elements. */
children?: ReactNode;
/** Emphasis variant. */
Expand All @@ -36,11 +36,6 @@ export interface SideNavigationItemProps extends GenericProps {
/** Props to pass to the toggle button (minus those already set by the SideNavigationItem props). */
toggleButtonProps: Pick<IconButtonProps, 'label'> &
Omit<IconButtonProps, 'label' | 'onClick' | 'icon' | 'emphasis' | 'color' | 'size'>;
/**
* Choose how the children are hidden when closed
* ('hide' keeps the children in DOM but hide them, 'unmount' remove the children from the DOM).
*/
closeMode?: 'hide' | 'unmount';
/** On action button click callback. */
onActionClick?(evt: React.MouseEvent): void;
/** On click callback. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export const ForceOpen = {
},
};

/** Hide on close instead of unmounting */
export const CloseModeHide = {
args: {
...OnAButton.args,
closeMode: 'hide',
},
};

/** Display a multiline tooltip */
export const MultilineTooltip = {
args: {
Expand Down
13 changes: 13 additions & 0 deletions packages/lumx-react/src/components/tooltip/Tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,19 @@ describe(`<${Tooltip.displayName}>`, () => {
// Children ref is stable
expect(ref.current === element).toBe(true);
});

it.only('should render in closeMode=hide', async () => {
const { tooltip, anchorWrapper } = await setup({
label: 'Tooltip label',
children: <Button>Anchor</Button>,
closeMode: 'hide',
});
expect(tooltip).toBeInTheDocument();
expect(anchorWrapper).toBeInTheDocument();
expect(anchorWrapper).toHaveAttribute('aria-describedby', tooltip?.id);
const button = screen.queryByRole('button', { name: 'Anchor' });
expect(button?.parentElement).toBe(anchorWrapper);
});
});

describe('activation', () => {
Expand Down
31 changes: 21 additions & 10 deletions packages/lumx-react/src/components/tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { usePopper } from 'react-popper';
import classNames from 'classnames';

import { DOCUMENT } from '@lumx/react/constants';
import { Comp, GenericProps } from '@lumx/react/utils/type';
import { Comp, GenericProps, HasCloseMode } from '@lumx/react/utils/type';
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
import { mergeRefs } from '@lumx/react/utils/mergeRefs';
import { Placement } from '@lumx/react/components/popover';
Expand All @@ -22,7 +22,7 @@ export type TooltipPlacement = Extract<Placement, 'top' | 'right' | 'bottom' | '
/**
* Defines the props of the component.
*/
export interface TooltipProps extends GenericProps {
export interface TooltipProps extends GenericProps, HasCloseMode {
/** Anchor (element on which we activate the tooltip). */
children: ReactNode;
/** Delay (in ms) before closing the tooltip. */
Expand Down Expand Up @@ -50,6 +50,7 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME);
*/
const DEFAULT_PROPS: Partial<TooltipProps> = {
placement: Placement.BOTTOM,
closeMode: 'unmount',
};

/**
Expand All @@ -65,7 +66,7 @@ const ARROW_SIZE = 8;
* @return React element.
*/
export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, ref) => {
const { label, children, className, delay, placement, forceOpen, ...forwardedProps } = props;
const { label, children, className, delay, placement, forceOpen, closeMode, ...forwardedProps } = props;
// Disable in SSR.
if (!DOCUMENT) {
return <>{children}</>;
Expand All @@ -88,28 +89,38 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
const position = attributes?.popper?.['data-popper-placement'] ?? placement;
const { isOpen: isActivated, onPopperMount } = useTooltipOpen(delay, anchorElement);
const isOpen = (isActivated || forceOpen) && !!label;
const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isOpen, id, label);
const isMounted = isOpen || closeMode === 'hide';
const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isMounted, id, label);

const labelLines = label ? label.split('\n') : [];

return (
<>
<TooltipContextProvider>{wrappedChildren}</TooltipContextProvider>
{isOpen &&
{isMounted &&
createPortal(
<div
ref={mergeRefs(ref, setPopperElement, onPopperMount)}
{...forwardedProps}
id={id}
role="tooltip"
aria-label={label}
className={classNames(className, handleBasicClasses({ prefix: CLASSNAME, position }))}
aria-label={label || ''}
className={classNames(
className,
handleBasicClasses({
prefix: CLASSNAME,
position,
hidden: !isOpen && closeMode === 'hide',
}),
)}
style={styles.popper}
{...attributes.popper}
>
<div className={`${CLASSNAME}__arrow`} />
<div className={`${CLASSNAME}__inner`}>
{label.indexOf('\n') !== -1
? label.split('\n').map((sentence: string) => <p key={sentence}>{sentence}</p>)
: label}
{labelLines.map((line) => (
<p key={line}>{line}</p>
))}
</div>
</div>,
document.body,
Expand Down
9 changes: 9 additions & 0 deletions packages/lumx-react/src/utils/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ export interface HasClassName {
className?: string;
}


export interface HasCloseMode {
/**
* Choose how the children are hidden when closed
* ('hide' keeps the children in DOM but hide them, 'unmount' remove the children from the DOM).
*/
closeMode?: 'hide' | 'unmount';
}

/**
* Define a generic props types.
*/
Expand Down

0 comments on commit 653e87d

Please sign in to comment.