diff --git a/packages/web-react/src/components/Tooltip/TooltipModern.tsx b/packages/web-react/src/components/Tooltip/TooltipModern.tsx index e9e6b23d81..c8f3a11276 100644 --- a/packages/web-react/src/components/Tooltip/TooltipModern.tsx +++ b/packages/web-react/src/components/Tooltip/TooltipModern.tsx @@ -10,9 +10,10 @@ const TooltipModern = (props: TooltipModernProps) => { const { children, enableFlipping: flipProp = true, + enableFlippingCrossAxis: flipCrossAxis = true, + enableHover = false, enableShifting: shiftProp = true, enableSizing: sizeProp = false, - enableFlippingCrossAxis: flipCrossAxis = true, flipFallbackAxisSideDirection = 'none', flipFallbackPlacements = ['bottom', 'top'], id, @@ -52,6 +53,7 @@ const TooltipModern = (props: TooltipModernProps) => { const { getFloatingProps, getReferenceProps, maxWidth, middlewareData, placement, refs, x, y } = useFloating({ arrowRef, cornerOffset: tooltipCornerOffset, + enableHover, flipCrossAxis, flipFallbackAxisSideDirection, flipFallbackPlacements, diff --git a/packages/web-react/src/components/Tooltip/useFloating.ts b/packages/web-react/src/components/Tooltip/useFloating.ts index 11319cb2db..0d9406136c 100644 --- a/packages/web-react/src/components/Tooltip/useFloating.ts +++ b/packages/web-react/src/components/Tooltip/useFloating.ts @@ -9,7 +9,9 @@ import { shift, size, useClick, + useDismiss, useFloating as useFloatingUI, + useHover, useInteractions, useRole, } from '@floating-ui/react'; @@ -18,6 +20,7 @@ import { useState } from 'react'; type UseTooltipUIProps = { arrowRef: React.MutableRefObject; cornerOffset?: number; + enableHover?: boolean; flipCrossAxis: boolean; flipFallbackAxisSideDirection: 'none' | 'start' | 'end'; flipFallbackPlacements?: Placement | Placement[]; @@ -40,6 +43,7 @@ export const useFloating = (props: UseTooltipUIProps) => { const { arrowRef, cornerOffset = 0, + enableHover, flipCrossAxis, flipFallbackAxisSideDirection = 'none', flipFallbackPlacements, @@ -54,12 +58,27 @@ export const useFloating = (props: UseTooltipUIProps) => { } = props; const [maxWidth, setMaxWidth] = useState(undefined); + const [isClicked, setIsClicked] = useState(false); const mainAxisOffset = cornerOffset + tooltipArrowWidth; // Floating UI library settings const { x, y, refs, context, placement, middlewareData } = useFloatingUI({ open: isOpen, - onOpenChange: onToggle, + onOpenChange: (open, event, reason) => { + if (enableHover) { + // if tooltip is opened by click, do not close until clicked again or outside press or escape key + if (reason === 'click') setIsClicked((prev) => !prev); + if (isOpen && isClicked && reason === 'hover') return; + if (isOpen && isClicked && (reason === 'click' || reason === 'outside-press' || reason === 'escape-key')) { + setIsClicked(false); + onToggle(false); + + return; + } + } + + onToggle(open); + }, placement: tooltipPlacement, whileElementsMounted: autoUpdate, middleware: [ @@ -91,9 +110,11 @@ export const useFloating = (props: UseTooltipUIProps) => { }); // Floating UI library interaction hooks - const click = useClick(context); + const click = useClick(context, { enabled: true }); + const hover = useHover(context, { enabled: enableHover }); + const dismiss = useDismiss(context); const role = useRole(context, { role: 'tooltip' }); - const { getReferenceProps, getFloatingProps } = useInteractions([click, role]); + const { getReferenceProps, getFloatingProps } = useInteractions([click, hover, dismiss, role]); return { context, diff --git a/packages/web-react/src/components/TooltipModern/demo/TooltipHover.tsx b/packages/web-react/src/components/TooltipModern/demo/TooltipHover.tsx new file mode 100644 index 0000000000..83e59b2a89 --- /dev/null +++ b/packages/web-react/src/components/TooltipModern/demo/TooltipHover.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { TooltipModern, TooltipPopover, TooltipTrigger } from '..'; +import { Button } from '../../Button'; + +const TooltipHover = () => { + const [open, setOpen] = React.useState(false); + + return ( + + I have a tooltip 😎 + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Velit, labore eveniet deleniti rerum quod voluptatem + autem aperiam officiis qui officia! Suscipit aspernatur minima dolorum rerum harum ipsa neque culpa facilis? + + + ); +}; + +export default TooltipHover; diff --git a/packages/web-react/src/components/TooltipModern/demo/index.tsx b/packages/web-react/src/components/TooltipModern/demo/index.tsx index 5a8bbd4cf8..a3fdd0dac3 100644 --- a/packages/web-react/src/components/TooltipModern/demo/index.tsx +++ b/packages/web-react/src/components/TooltipModern/demo/index.tsx @@ -13,10 +13,14 @@ import TooltipDismissibleViaJS from './TooltipDismissibleViaJS'; import TooltipOnHover from './TooltipOnHover'; import TooltipPlacements from './TooltipPlacements'; import TooltipWithFloatingUI from './TooltipWithFloatingUI'; +import TooltipHover from './TooltipHover'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + diff --git a/packages/web-react/src/types/tooltip.ts b/packages/web-react/src/types/tooltip.ts index a4a6234e05..ca84b7ed4c 100644 --- a/packages/web-react/src/types/tooltip.ts +++ b/packages/web-react/src/types/tooltip.ts @@ -46,6 +46,7 @@ export interface SpiritTooltipModernProps extends TooltipModernProps, ChildrenPr enableFlippingCrossAxis?: boolean; enableShifting?: boolean; enableSizing?: boolean; + enableHover?: boolean; flipFallbackAxisSideDirection?: 'none' | 'start' | 'end'; flipFallbackPlacements?: Placement | Placement[]; } diff --git a/packages/web/src/js/Tooltip.ts b/packages/web/src/js/Tooltip.ts index 8eaaa9a358..e6bb1254e3 100644 --- a/packages/web/src/js/Tooltip.ts +++ b/packages/web/src/js/Tooltip.ts @@ -1,8 +1,7 @@ import * as FloatingUI from '@floating-ui/dom'; import BaseComponent from './BaseComponent'; -import EventHandler from './dom/EventHandler'; -import SelectorEngine from './dom/SelectorEngine'; -import { enableDismissTrigger, enableToggleTrigger, SpiritConfig } from './utils'; +import { EventHandler, SelectorEngine } from './dom'; +import { SpiritConfig, clickOutsideElement, enableDismissTrigger, enableToggleAutoloader } from './utils'; const NAME = 'tooltip'; const DATA_KEY = 'tooltip'; @@ -12,6 +11,12 @@ const EVENT_HIDE = `hide${EVENT_KEY}`; const EVENT_HIDDEN = `hidden${EVENT_KEY}`; const EVENT_SHOW = `show${EVENT_KEY}`; const EVENT_SHOWN = `shown${EVENT_KEY}`; +const EVENT_CLICK = 'click'; +const EVENT_MOUSEENTER = 'mouseenter'; +const EVENT_MOUSELEAVE = 'mouseleave'; + +const TRIGGER_HOVER = 'hover'; +const TRIGGER_CLICK = 'click'; const SELECTOR_ARROW = '[data-spirit-element="arrow"]'; const CLASS_NAME_VISIBLE = 'is-visible'; @@ -19,9 +24,10 @@ const CLASS_NAME_HIDDEN = 'is-hidden'; type Config = { enableFlipping: boolean; + enableFlippingCrossAxis: boolean; + enableHover: boolean; enableShifting: boolean; enableSizing: boolean; - enableFlippingCrossAxis: boolean; flipFallbackAxisSideDirection: 'none' | 'start' | 'end'; flipFallbackPlacements: string; placement: FloatingUI.Placement; @@ -33,22 +39,28 @@ export const transformStringToArray = (str: string) => class Tooltip extends BaseComponent { arrow?: HTMLElement; - arrowWidth?: number; arrowCornerOffset?: number; + arrowWidth?: number; tip: HTMLElement; tooltipComputedStyle?: CSSStyleDeclaration; tooltipMaxWidth?: number; tooltipOffset?: number; trigger?: HTMLElement; + isToggled: boolean; + isHovered: boolean; + activeTrigger: object; constructor(element: SpiritElement, config?: SpiritConfig) { if (typeof FloatingUI === 'undefined') { throw new TypeError('Floating UI dependency is missing. Please, install it (https://floating-ui.com/)'); } - + console.log('construct', element); super(element, config); this.tip = this.getTipElement(); + this.isToggled = false; + this.isHovered = false; + this.activeTrigger = {}; if (this.isPlacementControlled()) { this.trigger = this.getTipTooltipWrapper(); @@ -69,6 +81,10 @@ class Tooltip extends BaseComponent { ); } } + + console.log(this.trigger); + + this.addEventListeners(); } static get NAME() { @@ -76,11 +92,17 @@ class Tooltip extends BaseComponent { } toggle() { + this.activeTrigger[TRIGGER_CLICK] = 'click' in this.activeTrigger ? !this.activeTrigger[TRIGGER_CLICK] : true; + this.activeTrigger[TRIGGER_HOVER] = 'hover' in this.activeTrigger ? !this.activeTrigger[TRIGGER_HOVER] : true; + console.log('toggle', this.activeTrigger); + if (this.isShown()) { - this.hide(); - } else { - this.show(); + this.leave(); + + return; } + + this.enter(); } isPlacementControlled() { @@ -133,9 +155,16 @@ class Tooltip extends BaseComponent { } EventHandler.trigger(this.element, Tooltip.eventName(EVENT_SHOWN)); + + if (this.isHovered === false) { + this.leave(); + } + + this.isHovered = false; } hide() { + console.log('hide', this.activeTrigger); if (!this.isShown()) { return; } @@ -157,6 +186,14 @@ class Tooltip extends BaseComponent { } } + this.activeTrigger[TRIGGER_CLICK] = false; + this.activeTrigger[TRIGGER_HOVER] = false; + this.isHovered = false; + + if (this.isWithActiveTrigger()) { + return; + } + this.element.removeAttribute('aria-describedby'); EventHandler.trigger(this.element, Tooltip.eventName(EVENT_HIDDEN)); } @@ -322,9 +359,81 @@ class Tooltip extends BaseComponent { }); } } + + autoCloseHandler = (event: Event) => { + console.log('autoclose'); + const shouldClose = this.trigger && clickOutsideElement(this.trigger, event); + console.log(shouldClose); + if (event.target && shouldClose) { + this.activeTrigger[TRIGGER_CLICK] = false; + this.activeTrigger[TRIGGER_HOVER] = false; + + this.leave(); + } + }; + + isWithActiveTrigger() { + return Object.values(this.activeTrigger).includes(true); + } + + enter() { + console.log('enter'); + if (this.isShown() || this.isHovered) { + this.isHovered = true; + + return; + } + + this.isHovered = true; + + this.show(); + } + + leave() { + if (this.isWithActiveTrigger()) { + return; + } + console.log('leave', this.activeTrigger, this.isWithActiveTrigger()); + + this.isHovered = false; + + this.hide(); + } + + addEventListeners() { + const button = this.trigger?.querySelector('button') as HTMLButtonElement; + const { enableHover } = this.config as Config; + + EventHandler.on(document, EVENT_CLICK, (event: Event) => this.autoCloseHandler(event)); + + EventHandler.on(button, EVENT_CLICK, (event) => { + const context = Tooltip.getOrCreateInstance(this.element as HTMLElement); + console.log('click instance', context); + // context.activeTrigger[TRIGGER_CLICK] = true; + context.toggle(); + }); + + this.addMouseEventListeners(); + } + + addMouseEventListeners() { + const button = this.trigger?.querySelector('button') as HTMLButtonElement; + EventHandler.on(button, EVENT_MOUSEENTER, (event) => { + const context = Tooltip.getOrCreateInstance(this.element as HTMLElement); + console.log('hover enter', context); + context.activeTrigger[TRIGGER_HOVER] = true; + context.enter(); + }); + EventHandler.on(button, EVENT_MOUSELEAVE, (event) => { + const context = Tooltip.getOrCreateInstance(this.element as HTMLElement); + console.log('hover leave', context, context.activeTrigger); + context.activeTrigger[TRIGGER_HOVER] = false; + context.leave(); + }); + } } -enableToggleTrigger(Tooltip, 'toggle'); +enableToggleAutoloader(Tooltip, 'hide', 'target'); enableDismissTrigger(Tooltip, 'hide'); export default Tooltip; diff --git a/packages/web/src/js/utils/ComponentFunctions.ts b/packages/web/src/js/utils/ComponentFunctions.ts index 1310efd9f5..de281341ac 100644 --- a/packages/web/src/js/utils/ComponentFunctions.ts +++ b/packages/web/src/js/utils/ComponentFunctions.ts @@ -26,8 +26,20 @@ const onClickHandler = ( }); }; -const onLoadHandler = (element: HTMLElement, component: typeof BaseComponent) => { - component.getOrCreateInstance(element); +const onLoadHandler = ( + element: HTMLElement, + component: typeof BaseComponent, + method: string, + event: Event, + aim: Aim = 'trigger', +) => { + if (aim === 'target') { + const target = getTriggerOrTarget(getElement(element), aim); + const instance = component.getOrCreateInstance(target); + instance[method](target, event); + } else { + component.getOrCreateInstance(element); + } }; const enableDataTrigger = ( @@ -54,8 +66,8 @@ const enableDismissTrigger = (component: typeof BaseComponent, method = 'dismiss enableDataTrigger(ATTRIBUTE_DATA_DISMISS, component, onClickHandler, method, aim); }; -const enableToggleAutoloader = (component: typeof BaseComponent, method = 'toggle') => { - enableDataTrigger(ATTRIBUTE_DATA_TOGGLE, component, onLoadHandler, method); +const enableToggleAutoloader = (component: typeof BaseComponent, method = 'toggle', aim: Aim = 'trigger') => { + enableDataTrigger(ATTRIBUTE_DATA_TOGGLE, component, onLoadHandler, method, aim); }; const clickOutsideElement = (target: Element, event: Event) => !event.composedPath().includes(target); diff --git a/packages/web/src/scss/components/Tooltip/index.html b/packages/web/src/scss/components/Tooltip/index.html index 3b8f4c7802..e956b2b038 100644 --- a/packages/web/src/scss/components/Tooltip/index.html +++ b/packages/web/src/scss/components/Tooltip/index.html @@ -2,295 +2,31 @@
-

Placements

- -
- -
-
-
- - - - - -
-
- - - - - -
-
- - - - - -
-
- - - - - -
-
- -
-
- Click
- the dots! -
-
- bottom - -
-
- -
-
-
- - - -
-
- -
- -

Static Tooltip (No Interaction)

+

Tooltip on Hover with Floating UI

-
-
- Tooltips
- all day long… -
-
- Hello there! - -
-
- Hello there! - -
-
- Hello there! There is slightly more text in this tooltip. - -
-
- Hello there! - -
-
- -
- -
- -
- -

Tooltip on Hover (Pure CSS)

- -
- -
-
- -
- Hello there! - -
-
-
- -
- Hello there! - -
-
-
- -
- Hello there! - -
-
-
- -
- Hello there! - -
-
-
- -
- -
- -
- -

Tooltip with JS plugin

- -
- -

Without Floating UI

- - - - -
- - -
- -

With Floating UI and placement fallbacks

- - - - - -
@@ -299,296 +35,4 @@

With Floating UI and placement fallbacks

-
- -

Tooltip on Click (JavaScript)

- -
- -

Without Floating UI

- - - -
-
- I have an externally-triggered tooltip -
- -
- -

With Floating UI and placement fallbacks

- - - -
-
- I have an externally-triggered tooltip -
- -
- -
- -
- -
- -

Dismissible Tooltip

- -
- -
- -
- Close me - - -
-
- -
- -
- -
- -

Dismissible Tooltip via JS API and Floating UI

- -
-

- Saves data to local storage. -

- - - -
- - -
- -
- -
- -
- - - - - -

Advanced Floating Functionality

- -

- Try scrolling the frame or resizing the window to see how the Tooltip behaves. The Floating UI - library is trying to keep the Tooltip in the viewport and it is also flipping, shifting and - resizing the Tooltip when it is not possible to keep it in the viewport. -

- -
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
- -
- -
-
-
-
- -
- -
- -
-
-
-
- -
-
- -
- - - - -
- -
-
- -
-
- {{/ layout/plain }} diff --git a/yarn.lock b/yarn.lock index 93b1faae89..9ddd3f06f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3337,12 +3337,12 @@ resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.3.0.tgz#113bc85fa102cf890ae801668f43ee265c547a09" integrity sha512-vX1WVAdPjZg9DkDkC+zEx/tKtnST6/qcNpwcjeBgco3XRNHz5PUA+ivi/yr6G3o0kMR60uKBJcfOdfzOFI7PMQ== -"@floating-ui/core@^1.5.3": - version "1.5.3" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.3.tgz#b6aa0827708d70971c8679a16cf680a515b8a52a" - integrity sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q== +"@floating-ui/core@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1" + integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g== dependencies: - "@floating-ui/utils" "^0.2.0" + "@floating-ui/utils" "^0.2.1" "@floating-ui/dom@^1.3.0": version "1.3.0" @@ -3351,13 +3351,13 @@ dependencies: "@floating-ui/core" "^1.3.0" -"@floating-ui/dom@^1.5.4": - version "1.5.4" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.4.tgz#28df1e1cb373884224a463235c218dcbd81a16bb" - integrity sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ== +"@floating-ui/dom@^1.5.3", "@floating-ui/dom@^1.6.1": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.1.tgz#d552e8444f77f2d88534372369b3771dc3a2fa5d" + integrity sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ== dependencies: - "@floating-ui/core" "^1.5.3" - "@floating-ui/utils" "^0.2.0" + "@floating-ui/core" "^1.6.0" + "@floating-ui/utils" "^0.2.1" "@floating-ui/react-dom@^2.0.0": version "2.0.1" @@ -3366,23 +3366,23 @@ dependencies: "@floating-ui/dom" "^1.3.0" -"@floating-ui/react-dom@^2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.5.tgz#851522899c34e3e2be1e29f3294f150834936e28" - integrity sha512-UsBK30Bg+s6+nsgblXtZmwHhgS2vmbuQK22qgt2pTQM6M3X6H1+cQcLXqgRY3ihVLcZJE6IvqDQozhsnIVqK/Q== +"@floating-ui/react-dom@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d" + integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw== dependencies: - "@floating-ui/dom" "^1.5.4" + "@floating-ui/dom" "^1.6.1" -"@floating-ui/react@0.26.5": - version "0.26.5" - resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.5.tgz#6a8658c88ff017b7530594b94433d614bff66e06" - integrity sha512-LJeSQa+yOwV0Tdpc/C3Vr92QMrwRqRMTk4yOwsRJKc57x3Lcw317GE0EV+ECM7+Z89yEAPBe7nzbDEWfkWCrBA== +"@floating-ui/react@^0.26.5": + version "0.26.9" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.9.tgz#bbccbefa0e60c8b7f4c0387ba0fc0607bb65f2cc" + integrity sha512-p86wynZJVEkEq2BBjY/8p2g3biQ6TlgT4o/3KgFKyTWoJLU1GZ8wpctwRqtkEl2tseYA+kw7dBAIDFcednfI5w== dependencies: - "@floating-ui/react-dom" "^2.0.5" - "@floating-ui/utils" "^0.2.0" + "@floating-ui/react-dom" "^2.0.8" + "@floating-ui/utils" "^0.2.1" tabbable "^6.0.1" -"@floating-ui/utils@^0.2.0": +"@floating-ui/utils@^0.2.1": version "0.2.1" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==