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

fix(tooltip): fixed closing when mouse is hovering the tooltip text #1034

Merged
merged 2 commits into from
Dec 15, 2023
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
13 changes: 8 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Tooltip: fixed tooltip closing when mouse is hovering the tooltip text.
- Tooltip: fixed close on Escape key pressed (not only when anchor is focused).

## [3.6.0][] - 2023-12-05

### Changed
Expand All @@ -24,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Table row: selected states color update + documentation update
- Table row: selected states color update + documentation update
- `@lumx/core`: remove default line-height on custom title typography.

## [3.5.4][] - 2023-10-17
Expand Down Expand Up @@ -1840,9 +1845,7 @@ _Failed released_
[unreleased]: https://github.com/lumapps/design-system/compare/v3.5.3...HEAD
[3.5.3]: https://github.com/lumapps/design-system/compare/v3.5.2...v3.5.3
[3.5.2]: https://github.com/lumapps/design-system/tree/v3.5.2


[Unreleased]: https://github.com/lumapps/design-system/compare/v3.6.0...HEAD
[unreleased]: https://github.com/lumapps/design-system/compare/v3.6.0...HEAD
[3.6.0]: https://github.com/lumapps/design-system/compare/v3.5.5...v3.6.0
[3.5.5]: https://github.com/lumapps/design-system/compare/v3.5.4...v3.5.5
[3.5.4]: https://github.com/lumapps/design-system/tree/v3.5.4
[3.5.4]: https://github.com/lumapps/design-system/tree/v3.5.4
2 changes: 1 addition & 1 deletion packages/lumx-core/src/js/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const SLIDESHOW_TRANSITION_DURATION = 5000;
*/
export const TOOLTIP_HOVER_DELAY = {
open: 500,
close: 0,
close: 500,
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ export const TooltipWithDropdown = (props: any) => {
const [isOpen, setOpen] = useState(false);
return (
<>
<Tooltip label={!isOpen && 'Tooltip'} {...props}>
<br />
<Tooltip label={!isOpen && 'Tooltip'} {...props} placement="top">
<Button ref={setButton} onClick={() => setOpen((o) => !o)}>
Anchor
</Button>
</Tooltip>
<Dropdown anchorRef={{ current: button }} isOpen={isOpen}>
<Dropdown anchorRef={{ current: button }} isOpen={isOpen} onClose={() => setOpen(false)}>
Dropdown
</Dropdown>
</>
Expand Down
32 changes: 32 additions & 0 deletions packages/lumx-react/src/components/tooltip/Tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,38 @@ describe(`<${Tooltip.displayName}>`, () => {
});
});

it('should activate on hover anchor and then tooltip', async () => {
let { tooltip } = await setup({
label: 'Tooltip label',
children: <Button>Anchor</Button>,
forceOpen: false,
});

expect(tooltip).not.toBeInTheDocument();

// Hover anchor button
const button = getByClassName(document.body, Button.className as string);
await userEvent.hover(button);

// Tooltip opened
tooltip = await findByClassName(document.body, CLASSNAME);
expect(tooltip).toBeInTheDocument();
expect(button).toHaveAttribute('aria-describedby', tooltip?.id);

// Hover tooltip
await userEvent.hover(tooltip);
expect(tooltip).toBeInTheDocument();
expect(button).toHaveAttribute('aria-describedby', tooltip?.id);

// Un-hover tooltip
userEvent.unhover(tooltip);
await waitFor(() => {
expect(button).not.toHaveFocus();
// Tooltip closed
expect(tooltip).not.toBeInTheDocument();
});
});

it('should activate on anchor focus', async () => {
let { tooltip } = await setup({
label: 'Tooltip label',
Expand Down
7 changes: 4 additions & 3 deletions packages/lumx-react/src/components/tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,17 @@ export const Tooltip: Comp<TooltipProps, HTMLDivElement> = forwardRef((props, re
});

const position = attributes?.popper?.['data-popper-placement'] ?? placement;
const isOpen = useTooltipOpen(delay, anchorElement) || forceOpen;
const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isOpen as boolean, id);
const { isOpen: isActivated, onPopperMount } = useTooltipOpen(delay, anchorElement);
const isOpen = isActivated || forceOpen;
const wrappedChildren = useInjectTooltipRef(children, setAnchorElement, isOpen, id);

return (
<>
{wrappedChildren}
{isOpen &&
createPortal(
<div
ref={mergeRefs(ref, setPopperElement)}
ref={mergeRefs(ref, setPopperElement, onPopperMount)}
{...forwardedProps}
id={id}
role="tooltip"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { mergeRefs } from '@lumx/react/utils/mergeRefs';
export const useInjectTooltipRef = (
children: ReactNode,
setAnchorElement: (e: HTMLDivElement) => void,
isOpen: boolean,
isOpen: boolean | undefined,
id: string,
): ReactNode => {
return useMemo(() => {
Expand Down
94 changes: 56 additions & 38 deletions packages/lumx-react/src/components/tooltip/useTooltipOpen.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { onEscapePressed } from '@lumx/react/utils/event';
import { useEffect, useState } from 'react';
import { MutableRefObject, useEffect, useRef, useState } from 'react';
import { browserDoesNotSupportHover } from '@lumx/react/utils/browserDoesNotSupportHover';
import { TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/constants';
import { useCallbackOnEscape } from '@lumx/react/hooks/useCallbackOnEscape';

/**
* Hook controlling tooltip visibility using mouse hover the anchor and delay.
Expand All @@ -10,9 +10,15 @@ import { TOOLTIP_HOVER_DELAY, TOOLTIP_LONG_PRESS_DELAY } from '@lumx/react/const
* @param anchorElement Tooltip anchor element.
* @return whether or not to show the tooltip.
*/
export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLElement | null): boolean {
export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLElement | null) {
const [isOpen, setIsOpen] = useState(false);

const onPopperMount = useRef<any>(null) as MutableRefObject<(elem: HTMLElement | null) => void>;

// Global close on escape
const [closeCallback, setCloseCallback] = useState<undefined | (() => void)>(undefined);
useCallbackOnEscape(closeCallback);

useEffect(() => {
if (!anchorElement) {
return undefined;
Expand Down Expand Up @@ -45,53 +51,65 @@ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLEle
};

// Close or cancel opening of tooltip
const close = (overrideDelay = closeDelay) => {
const getClose = (overrideDelay = closeDelay) => {
if (!shouldOpen && !timer) return;
shouldOpen = false;
deferUpdate(overrideDelay);
};
const closeImmediately = () => close(0);

/**
* Handle touchend event
* If `touchend` comes before the open delay => cancel tooltip (close immediate).
* Else if `touchend` comes after the open delay => tooltip takes priority, the anchor's default touch end event is prevented.
*/
const touchEnd = (evt: Event) => {
if (!openStartTime) return;
if (Date.now() - openStartTime >= openDelay) {
// Tooltip take priority, event prevented.
evt.stopPropagation();
evt.preventDefault();
anchorElement.focus();
// Close with delay.
close();
} else {
// Close immediately.
closeImmediately();
}
};
const close = () => getClose(closeDelay);
const closeImmediately = () => getClose(0);
setCloseCallback(() => closeImmediately);

// Adapt event to browsers with or without `hover` support.
const events: Array<[Node, Event['type'], any]> = hoverNotSupported
? [
[anchorElement, hasTouch ? 'touchstart' : 'mousedown', open],
[anchorElement, hasTouch ? 'touchend' : 'mouseup', touchEnd],
]
: [
[anchorElement, 'mouseenter', open],
[anchorElement, 'mouseleave', close],
[anchorElement, 'mouseup', closeImmediately],
];
const events: Array<[Node, Event['type'], any]> = [];
if (hoverNotSupported) {
/**
* Handle touchend event
* If end comes before the open delay => cancel tooltip (close immediate).
* Else if end comes after the open delay => tooltip takes priority, the anchor's default touch end event is prevented.
*/
const longPressEnd = (evt: Event) => {
if (!openStartTime) return;
if (Date.now() - openStartTime >= openDelay) {
// Tooltip take priority, event prevented.
evt.stopPropagation();
evt.preventDefault();
anchorElement.focus();
// Close with delay.
close();
} else {
// Close immediately.
closeImmediately();
}
};

events.push(
[anchorElement, hasTouch ? 'touchstart' : 'mousedown', open],
[anchorElement, hasTouch ? 'touchend' : 'mouseup', longPressEnd],
);
} else {
events.push(
[anchorElement, 'mouseenter', open],
[anchorElement, 'mouseleave', close],
[anchorElement, 'mouseup', closeImmediately],
);

onPopperMount.current = (popperElement: HTMLElement | null) => {
if (!popperElement) return;
// Popper element hover
popperElement.addEventListener('mouseenter', open);
popperElement.addEventListener('mouseleave', close);
// Add to event list to remove on unmount
events.push([popperElement, 'mouseenter', open], [popperElement, 'mouseleave', close]);
};
}

// Events always applied no matter the browser:.
events.push(
// Open on focus.
[anchorElement, 'focusin', open],
// Close on lost focus.
[anchorElement, 'focusout', closeImmediately],
// Close on ESC keydown
[anchorElement, 'keydown', onEscapePressed(closeImmediately)],
);

// Attach events
Expand All @@ -109,5 +127,5 @@ export function useTooltipOpen(delay: number | undefined, anchorElement: HTMLEle
};
}, [anchorElement, delay]);

return isOpen;
return { isOpen, onPopperMount: onPopperMount.current };
}
Loading