From 5b642788ba2b87999cfa9ade1e7460064243c76f Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Tue, 31 Oct 2023 16:00:13 -0300 Subject: [PATCH 01/18] feat: expose tooltip ref (imperative mode) --- src/App.tsx | 38 +- src/components/Tooltip/Tooltip.tsx | 55 +- src/components/Tooltip/TooltipTypes.d.ts | 24 + .../TooltipController/TooltipController.tsx | 601 +++++++++--------- src/index.tsx | 2 + 5 files changed, 411 insertions(+), 309 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index fd85b9ad..29910fef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,8 @@ /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import { TooltipController as Tooltip } from 'components/TooltipController' -import { IPosition } from 'components/Tooltip/TooltipTypes.d' -import React, { useState } from 'react' +import { IPosition, TooltipImperativeProps } from 'components/Tooltip/TooltipTypes.d' +import React, { useEffect, useRef, useState } from 'react' import { inline, offset } from '@floating-ui/dom' import styles from './styles.module.css' @@ -11,6 +11,7 @@ function App() { const [isDarkOpen, setIsDarkOpen] = useState(false) const [position, setPosition] = useState({ x: 0, y: 0 }) const [toggle, setToggle] = useState(false) + const tooltipRef = useRef(null) const handlePositionClick: React.MouseEventHandler = (event) => { const x = event.clientX @@ -23,6 +24,19 @@ function App() { setAnchorId(target.id) } + useEffect(() => { + const handleQ = (event: KeyboardEvent) => { + if (event.key === 'q') { + // q + tooltipRef.current?.close() + } + } + window.addEventListener('keydown', handleQ) + return () => { + window.removeEventListener('keydown', handleQ) + } + }) + return (
diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 5413a652..2a436341 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useRef, useCallback } from 'react' +import React, { useEffect, useState, useRef, useCallback, useImperativeHandle } from 'react' import { autoUpdate } from '@floating-ui/dom' import classNames from 'classnames' import debounce from 'utils/debounce' @@ -8,10 +8,11 @@ import { getScrollParent } from 'utils/get-scroll-parent' import { computeTooltipPosition } from 'utils/compute-positions' import coreStyles from './core-styles.module.css' import styles from './styles.module.css' -import type { IPosition, ITooltip, PlacesType } from './TooltipTypes' +import type { IPosition, ITooltip, PlacesType, TooltipImperativeOpenOptions } from './TooltipTypes' const Tooltip = ({ // props + forwardRef, id, className, classNameArrow, @@ -58,6 +59,9 @@ const Tooltip = ({ const [inlineArrowStyles, setInlineArrowStyles] = useState({}) const [show, setShow] = useState(false) const [rendered, setRendered] = useState(false) + const [imperativeOptions, setImperativeOptions] = useState( + null, + ) const wasShowing = useRef(false) const lastFloatPosition = useRef(null) /** @@ -149,6 +153,7 @@ const Tooltip = ({ if (show) { afterShow?.() } else { + setImperativeOptions(null) afterHide?.() } }, [show]) @@ -274,6 +279,9 @@ const Tooltip = ({ } const handleClickOutsideAnchors = (event: MouseEvent) => { + if (!show) { + return + } const anchorById = document.querySelector(`[id='${anchorId}']`) const anchors = [anchorById, ...anchorsBySelect] if (anchors.some((anchor) => anchor?.contains(event.target as HTMLElement))) { @@ -293,9 +301,10 @@ const Tooltip = ({ const debouncedHandleShowTooltip = debounce(handleShowTooltip, 50, true) const debouncedHandleHideTooltip = debounce(handleHideTooltip, 50, true) const updateTooltipPosition = useCallback(() => { - if (position) { + const actualPosition = imperativeOptions?.position ?? position + if (actualPosition) { // if `position` is set, override regular and `float` positioning - handleTooltipPosition(position) + handleTooltipPosition(actualPosition) return } @@ -349,6 +358,7 @@ const Tooltip = ({ offset, positionStrategy, position, + imperativeOptions?.position, float, ]) @@ -484,7 +494,7 @@ const Tooltip = ({ ]) useEffect(() => { - let selector = anchorSelect ?? '' + let selector = imperativeOptions?.anchorSelect ?? anchorSelect ?? '' if (!selector && id) { selector = `[data-tooltip-id='${id}']` } @@ -584,7 +594,7 @@ const Tooltip = ({ return () => { documentObserver.disconnect() } - }, [id, anchorSelect, activeAnchor]) + }, [id, anchorSelect, imperativeOptions?.anchorSelect, activeAnchor]) useEffect(() => { updateTooltipPosition() @@ -628,7 +638,7 @@ const Tooltip = ({ }, []) useEffect(() => { - let selector = anchorSelect + let selector = imperativeOptions?.anchorSelect ?? anchorSelect if (!selector && id) { selector = `[data-tooltip-id='${id}']` } @@ -642,9 +652,34 @@ const Tooltip = ({ // warning was already issued in the controller setAnchorsBySelect([]) } - }, [id, anchorSelect]) + }, [id, anchorSelect, imperativeOptions?.anchorSelect]) + + const actualContent = imperativeOptions?.content ?? content + const canShow = Boolean(!hidden && actualContent && show && Object.keys(inlineStyles).length > 0) - const canShow = !hidden && content && show && Object.keys(inlineStyles).length > 0 + useImperativeHandle(forwardRef, () => ({ + open: (options) => { + if (options?.anchorSelect) { + try { + document.querySelector(options.anchorSelect) + } catch { + if (!process.env.NODE_ENV || process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.warn(`[react-tooltip] "${options.anchorSelect}" is not a valid CSS selector`) + } + return + } + } + setImperativeOptions(options ?? null) + handleShow(true) + }, + close: () => { + handleShow(false) + }, + activeAnchor, + place: actualPlacement, + isOpen: rendered && canShow, + })) return rendered ? ( - {content} + {actualContent} void + close: () => void + /** + * @readonly + */ + activeAnchor: HTMLElement | null + /** + * @readonly + */ + place: PlacesType + /** + * @readonly + */ + isOpen: boolean +} + export interface ITooltip { + forwardRef?: React.ForwardedRef className?: string classNameArrow?: string content?: ChildrenType diff --git a/src/components/TooltipController/TooltipController.tsx b/src/components/TooltipController/TooltipController.tsx index 729e77b8..5dd2de08 100644 --- a/src/components/TooltipController/TooltipController.tsx +++ b/src/components/TooltipController/TooltipController.tsx @@ -9,341 +9,348 @@ import type { DataAttribute, ITooltip, ChildrenType, + TooltipImperativeProps, } from 'components/Tooltip/TooltipTypes' import { useTooltip } from 'components/TooltipProvider' import { TooltipContent } from 'components/TooltipContent' import type { ITooltipController } from './TooltipControllerTypes' -const TooltipController = ({ - id, - anchorId, - anchorSelect, - content, - html, - render, - className, - classNameArrow, - variant = 'dark', - place = 'top', - offset = 10, - wrapper = 'div', - children = null, - events = ['hover'], - openOnClick = false, - positionStrategy = 'absolute', - middlewares, - delayShow = 0, - delayHide = 0, - float = false, - hidden = false, - noArrow = false, - clickable = false, - closeOnEsc = false, - closeOnScroll = false, - closeOnResize = false, - style, - position, - isOpen, - disableStyleInjection = false, - border, - opacity, - arrowColor, - setIsOpen, - afterShow, - afterHide, -}: ITooltipController) => { - const [tooltipContent, setTooltipContent] = useState(content) - const [tooltipHtml, setTooltipHtml] = useState(html) - const [tooltipPlace, setTooltipPlace] = useState(place) - const [tooltipVariant, setTooltipVariant] = useState(variant) - const [tooltipOffset, setTooltipOffset] = useState(offset) - const [tooltipDelayShow, setTooltipDelayShow] = useState(delayShow) - const [tooltipDelayHide, setTooltipDelayHide] = useState(delayHide) - const [tooltipFloat, setTooltipFloat] = useState(float) - const [tooltipHidden, setTooltipHidden] = useState(hidden) - const [tooltipWrapper, setTooltipWrapper] = useState(wrapper) - const [tooltipEvents, setTooltipEvents] = useState(events) - const [tooltipPositionStrategy, setTooltipPositionStrategy] = useState(positionStrategy) - const [activeAnchor, setActiveAnchor] = useState(null) - const styleInjectionRef = useRef(disableStyleInjection) - /** - * @todo Remove this in a future version (provider/wrapper method is deprecated) - */ - const { anchorRefs, activeAnchor: providerActiveAnchor } = useTooltip(id) +const TooltipController = React.forwardRef( + ( + { + id, + anchorId, + anchorSelect, + content, + html, + render, + className, + classNameArrow, + variant = 'dark', + place = 'top', + offset = 10, + wrapper = 'div', + children = null, + events = ['hover'], + openOnClick = false, + positionStrategy = 'absolute', + middlewares, + delayShow = 0, + delayHide = 0, + float = false, + hidden = false, + noArrow = false, + clickable = false, + closeOnEsc = false, + closeOnScroll = false, + closeOnResize = false, + style, + position, + isOpen, + disableStyleInjection = false, + border, + opacity, + arrowColor, + setIsOpen, + afterShow, + afterHide, + }: ITooltipController, + ref, + ) => { + const [tooltipContent, setTooltipContent] = useState(content) + const [tooltipHtml, setTooltipHtml] = useState(html) + const [tooltipPlace, setTooltipPlace] = useState(place) + const [tooltipVariant, setTooltipVariant] = useState(variant) + const [tooltipOffset, setTooltipOffset] = useState(offset) + const [tooltipDelayShow, setTooltipDelayShow] = useState(delayShow) + const [tooltipDelayHide, setTooltipDelayHide] = useState(delayHide) + const [tooltipFloat, setTooltipFloat] = useState(float) + const [tooltipHidden, setTooltipHidden] = useState(hidden) + const [tooltipWrapper, setTooltipWrapper] = useState(wrapper) + const [tooltipEvents, setTooltipEvents] = useState(events) + const [tooltipPositionStrategy, setTooltipPositionStrategy] = useState(positionStrategy) + const [activeAnchor, setActiveAnchor] = useState(null) + const styleInjectionRef = useRef(disableStyleInjection) + /** + * @todo Remove this in a future version (provider/wrapper method is deprecated) + */ + const { anchorRefs, activeAnchor: providerActiveAnchor } = useTooltip(id) - const getDataAttributesFromAnchorElement = (elementReference: HTMLElement) => { - const dataAttributes = elementReference?.getAttributeNames().reduce((acc, name) => { - if (name.startsWith('data-tooltip-')) { - const parsedAttribute = name.replace(/^data-tooltip-/, '') as DataAttribute - acc[parsedAttribute] = elementReference?.getAttribute(name) ?? null - } - return acc - }, {} as Record) + const getDataAttributesFromAnchorElement = (elementReference: HTMLElement) => { + const dataAttributes = elementReference?.getAttributeNames().reduce((acc, name) => { + if (name.startsWith('data-tooltip-')) { + const parsedAttribute = name.replace(/^data-tooltip-/, '') as DataAttribute + acc[parsedAttribute] = elementReference?.getAttribute(name) ?? null + } + return acc + }, {} as Record) - return dataAttributes - } + return dataAttributes + } - const applyAllDataAttributesFromAnchorElement = ( - dataAttributes: Record, - ) => { - const handleDataAttributes: Record void> = { - place: (value) => { - setTooltipPlace((value as PlacesType) ?? place) - }, - content: (value) => { - setTooltipContent(value ?? content) - }, - html: (value) => { - setTooltipHtml(value ?? html) - }, - variant: (value) => { - setTooltipVariant((value as VariantType) ?? variant) - }, - offset: (value) => { - setTooltipOffset(value === null ? offset : Number(value)) - }, - wrapper: (value) => { - setTooltipWrapper((value as WrapperType) ?? wrapper) - }, - events: (value) => { - const parsed = value?.split(' ') as EventsType[] - setTooltipEvents(parsed ?? events) - }, - 'position-strategy': (value) => { - setTooltipPositionStrategy((value as PositionStrategy) ?? positionStrategy) - }, - 'delay-show': (value) => { - setTooltipDelayShow(value === null ? delayShow : Number(value)) - }, - 'delay-hide': (value) => { - setTooltipDelayHide(value === null ? delayHide : Number(value)) - }, - float: (value) => { - setTooltipFloat(value === null ? float : value === 'true') - }, - hidden: (value) => { - setTooltipHidden(value === null ? hidden : value === 'true') - }, + const applyAllDataAttributesFromAnchorElement = ( + dataAttributes: Record, + ) => { + const handleDataAttributes: Record void> = { + place: (value) => { + setTooltipPlace((value as PlacesType) ?? place) + }, + content: (value) => { + setTooltipContent(value ?? content) + }, + html: (value) => { + setTooltipHtml(value ?? html) + }, + variant: (value) => { + setTooltipVariant((value as VariantType) ?? variant) + }, + offset: (value) => { + setTooltipOffset(value === null ? offset : Number(value)) + }, + wrapper: (value) => { + setTooltipWrapper((value as WrapperType) ?? wrapper) + }, + events: (value) => { + const parsed = value?.split(' ') as EventsType[] + setTooltipEvents(parsed ?? events) + }, + 'position-strategy': (value) => { + setTooltipPositionStrategy((value as PositionStrategy) ?? positionStrategy) + }, + 'delay-show': (value) => { + setTooltipDelayShow(value === null ? delayShow : Number(value)) + }, + 'delay-hide': (value) => { + setTooltipDelayHide(value === null ? delayHide : Number(value)) + }, + float: (value) => { + setTooltipFloat(value === null ? float : value === 'true') + }, + hidden: (value) => { + setTooltipHidden(value === null ? hidden : value === 'true') + }, + } + // reset unset data attributes to default values + // without this, data attributes from the last active anchor will still be used + Object.values(handleDataAttributes).forEach((handler) => handler(null)) + Object.entries(dataAttributes).forEach(([key, value]) => { + handleDataAttributes[key as DataAttribute]?.(value) + }) } - // reset unset data attributes to default values - // without this, data attributes from the last active anchor will still be used - Object.values(handleDataAttributes).forEach((handler) => handler(null)) - Object.entries(dataAttributes).forEach(([key, value]) => { - handleDataAttributes[key as DataAttribute]?.(value) - }) - } - useEffect(() => { - setTooltipContent(content) - }, [content]) + useEffect(() => { + setTooltipContent(content) + }, [content]) - useEffect(() => { - setTooltipHtml(html) - }, [html]) + useEffect(() => { + setTooltipHtml(html) + }, [html]) - useEffect(() => { - setTooltipPlace(place) - }, [place]) + useEffect(() => { + setTooltipPlace(place) + }, [place]) - useEffect(() => { - setTooltipVariant(variant) - }, [variant]) + useEffect(() => { + setTooltipVariant(variant) + }, [variant]) - useEffect(() => { - setTooltipOffset(offset) - }, [offset]) + useEffect(() => { + setTooltipOffset(offset) + }, [offset]) - useEffect(() => { - setTooltipDelayShow(delayShow) - }, [delayShow]) + useEffect(() => { + setTooltipDelayShow(delayShow) + }, [delayShow]) - useEffect(() => { - setTooltipDelayHide(delayHide) - }, [delayHide]) + useEffect(() => { + setTooltipDelayHide(delayHide) + }, [delayHide]) - useEffect(() => { - setTooltipFloat(float) - }, [float]) + useEffect(() => { + setTooltipFloat(float) + }, [float]) - useEffect(() => { - setTooltipHidden(hidden) - }, [hidden]) + useEffect(() => { + setTooltipHidden(hidden) + }, [hidden]) - useEffect(() => { - setTooltipPositionStrategy(positionStrategy) - }, [positionStrategy]) + useEffect(() => { + setTooltipPositionStrategy(positionStrategy) + }, [positionStrategy]) - useEffect(() => { - if (styleInjectionRef.current === disableStyleInjection) { - return - } - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line no-console - console.warn('[react-tooltip] Do not change `disableStyleInjection` dynamically.') - } - }, [disableStyleInjection]) + useEffect(() => { + if (styleInjectionRef.current === disableStyleInjection) { + return + } + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.warn('[react-tooltip] Do not change `disableStyleInjection` dynamically.') + } + }, [disableStyleInjection]) - useEffect(() => { - if (typeof window !== 'undefined') { - window.dispatchEvent( - new CustomEvent('react-tooltip-inject-styles', { - detail: { - disableCore: disableStyleInjection === 'core', - disableBase: disableStyleInjection, - }, - }), - ) - } - }, []) + useEffect(() => { + if (typeof window !== 'undefined') { + window.dispatchEvent( + new CustomEvent('react-tooltip-inject-styles', { + detail: { + disableCore: disableStyleInjection === 'core', + disableBase: disableStyleInjection, + }, + }), + ) + } + }, []) - useEffect(() => { - const elementRefs = new Set(anchorRefs) + useEffect(() => { + const elementRefs = new Set(anchorRefs) - let selector = anchorSelect - if (!selector && id) { - selector = `[data-tooltip-id='${id}']` - } - if (selector) { - try { - const anchorsBySelect = document.querySelectorAll(selector) - anchorsBySelect.forEach((anchor) => { - elementRefs.add({ current: anchor }) - }) - } catch { - if (!process.env.NODE_ENV || process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line no-console - console.warn(`[react-tooltip] "${selector}" is not a valid CSS selector`) + let selector = anchorSelect + if (!selector && id) { + selector = `[data-tooltip-id='${id}']` + } + if (selector) { + try { + const anchorsBySelect = document.querySelectorAll(selector) + anchorsBySelect.forEach((anchor) => { + elementRefs.add({ current: anchor }) + }) + } catch { + if (!process.env.NODE_ENV || process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.warn(`[react-tooltip] "${selector}" is not a valid CSS selector`) + } } } - } - const anchorById = document.querySelector(`[id='${anchorId}']`) - if (anchorById) { - elementRefs.add({ current: anchorById }) - } + const anchorById = document.querySelector(`[id='${anchorId}']`) + if (anchorById) { + elementRefs.add({ current: anchorById }) + } - if (!elementRefs.size) { - return () => null - } + if (!elementRefs.size) { + return () => null + } - const anchorElement = activeAnchor ?? anchorById ?? providerActiveAnchor.current + const anchorElement = activeAnchor ?? anchorById ?? providerActiveAnchor.current - const observerCallback: MutationCallback = (mutationList) => { - mutationList.forEach((mutation) => { - if ( - !anchorElement || - mutation.type !== 'attributes' || - !mutation.attributeName?.startsWith('data-tooltip-') - ) { - return - } - // make sure to get all set attributes, since all unset attributes are reset + const observerCallback: MutationCallback = (mutationList) => { + mutationList.forEach((mutation) => { + if ( + !anchorElement || + mutation.type !== 'attributes' || + !mutation.attributeName?.startsWith('data-tooltip-') + ) { + return + } + // make sure to get all set attributes, since all unset attributes are reset + const dataAttributes = getDataAttributesFromAnchorElement(anchorElement) + applyAllDataAttributesFromAnchorElement(dataAttributes) + }) + } + + // Create an observer instance linked to the callback function + const observer = new MutationObserver(observerCallback) + + // do not check for subtree and childrens, we only want to know attribute changes + // to stay watching `data-attributes-*` from anchor element + const observerConfig = { attributes: true, childList: false, subtree: false } + + if (anchorElement) { const dataAttributes = getDataAttributesFromAnchorElement(anchorElement) applyAllDataAttributesFromAnchorElement(dataAttributes) - }) - } + // Start observing the target node for configured mutations + observer.observe(anchorElement, observerConfig) + } - // Create an observer instance linked to the callback function - const observer = new MutationObserver(observerCallback) + return () => { + // Remove the observer when the tooltip is destroyed + observer.disconnect() + } + }, [anchorRefs, providerActiveAnchor, activeAnchor, anchorId, anchorSelect]) - // do not check for subtree and childrens, we only want to know attribute changes - // to stay watching `data-attributes-*` from anchor element - const observerConfig = { attributes: true, childList: false, subtree: false } + useEffect(() => { + if (process.env.NODE_ENV === 'production') { + return + } + if (style?.border) { + // eslint-disable-next-line no-console + console.warn('[react-tooltip] Do not set `style.border`. Use `border` prop instead.') + } + if (border && !CSS.supports('border', `${border}`)) { + // eslint-disable-next-line no-console + console.warn(`[react-tooltip] "${border}" is not a valid \`border\`.`) + } + if (style?.opacity) { + // eslint-disable-next-line no-console + console.warn('[react-tooltip] Do not set `style.opacity`. Use `opacity` prop instead.') + } + if (opacity && !CSS.supports('opacity', `${opacity}`)) { + // eslint-disable-next-line no-console + console.warn(`[react-tooltip] "${opacity}" is not a valid \`opacity\`.`) + } + }, []) - if (anchorElement) { - const dataAttributes = getDataAttributesFromAnchorElement(anchorElement) - applyAllDataAttributesFromAnchorElement(dataAttributes) - // Start observing the target node for configured mutations - observer.observe(anchorElement, observerConfig) + /** + * content priority: children < render or content < html + * children should be lower priority so that it can be used as the "default" content + */ + let renderedContent: ChildrenType = children + const contentWrapperRef = useRef(null) + if (render) { + const rendered = render({ content: tooltipContent ?? null, activeAnchor }) as React.ReactNode + renderedContent = rendered ? ( +
+ {rendered} +
+ ) : null + } else if (tooltipContent) { + renderedContent = tooltipContent } - - return () => { - // Remove the observer when the tooltip is destroyed - observer.disconnect() + if (tooltipHtml) { + renderedContent = } - }, [anchorRefs, providerActiveAnchor, activeAnchor, anchorId, anchorSelect]) - useEffect(() => { - if (process.env.NODE_ENV === 'production') { - return - } - if (style?.border) { - // eslint-disable-next-line no-console - console.warn('[react-tooltip] Do not set `style.border`. Use `border` prop instead.') + const props: ITooltip = { + forwardRef: ref, + id, + anchorId, + anchorSelect, + className, + classNameArrow, + content: renderedContent, + contentWrapperRef, + place: tooltipPlace, + variant: tooltipVariant, + offset: tooltipOffset, + wrapper: tooltipWrapper, + events: tooltipEvents, + openOnClick, + positionStrategy: tooltipPositionStrategy, + middlewares, + delayShow: tooltipDelayShow, + delayHide: tooltipDelayHide, + float: tooltipFloat, + hidden: tooltipHidden, + noArrow, + clickable, + closeOnEsc, + closeOnScroll, + closeOnResize, + style, + position, + isOpen, + border, + opacity, + arrowColor, + setIsOpen, + afterShow, + afterHide, + activeAnchor, + setActiveAnchor: (anchor: HTMLElement | null) => setActiveAnchor(anchor), } - if (border && !CSS.supports('border', `${border}`)) { - // eslint-disable-next-line no-console - console.warn(`[react-tooltip] "${border}" is not a valid \`border\`.`) - } - if (style?.opacity) { - // eslint-disable-next-line no-console - console.warn('[react-tooltip] Do not set `style.opacity`. Use `opacity` prop instead.') - } - if (opacity && !CSS.supports('opacity', `${opacity}`)) { - // eslint-disable-next-line no-console - console.warn(`[react-tooltip] "${opacity}" is not a valid \`opacity\`.`) - } - }, []) - - /** - * content priority: children < render or content < html - * children should be lower priority so that it can be used as the "default" content - */ - let renderedContent: ChildrenType = children - const contentWrapperRef = useRef(null) - if (render) { - const rendered = render({ content: tooltipContent ?? null, activeAnchor }) as React.ReactNode - renderedContent = rendered ? ( -
- {rendered} -
- ) : null - } else if (tooltipContent) { - renderedContent = tooltipContent - } - if (tooltipHtml) { - renderedContent = - } - - const props: ITooltip = { - id, - anchorId, - anchorSelect, - className, - classNameArrow, - content: renderedContent, - contentWrapperRef, - place: tooltipPlace, - variant: tooltipVariant, - offset: tooltipOffset, - wrapper: tooltipWrapper, - events: tooltipEvents, - openOnClick, - positionStrategy: tooltipPositionStrategy, - middlewares, - delayShow: tooltipDelayShow, - delayHide: tooltipDelayHide, - float: tooltipFloat, - hidden: tooltipHidden, - noArrow, - clickable, - closeOnEsc, - closeOnScroll, - closeOnResize, - style, - position, - isOpen, - border, - opacity, - arrowColor, - setIsOpen, - afterShow, - afterHide, - activeAnchor, - setActiveAnchor: (anchor: HTMLElement | null) => setActiveAnchor(anchor), - } - return -} + return + }, +) export default TooltipController diff --git a/src/index.tsx b/src/index.tsx index 6dc35544..1faa1755 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -12,6 +12,7 @@ import type { WrapperType, IPosition, Middleware, + TooltipImperativeProps, } from './components/Tooltip/TooltipTypes' import type { ITooltipController } from './components/TooltipController/TooltipControllerTypes' import type { ITooltipWrapper } from './components/TooltipProvider/TooltipProviderTypes' @@ -47,6 +48,7 @@ export type { ITooltipWrapper, IPosition, Middleware, + TooltipImperativeProps as TooltipRefProps, } export { removeStyle } from './utils/handle-style' From d07bc9be25eafb06eacedfe0cb23896870114a84 Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Wed, 1 Nov 2023 15:14:34 -0300 Subject: [PATCH 02/18] feat: imperative mode `place` option --- src/components/Tooltip/Tooltip.tsx | 5 +++-- src/components/Tooltip/TooltipTypes.d.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 2a436341..79b3c021 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -239,7 +239,7 @@ const Tooltip = ({ }, } as Element computeTooltipPosition({ - place, + place: imperativeOptions?.place ?? place, offset, elementReference: virtualElement, tooltipReference: tooltipRef.current, @@ -328,7 +328,7 @@ const Tooltip = ({ } computeTooltipPosition({ - place, + place: imperativeOptions?.place ?? place, offset, elementReference: activeAnchor, tooltipReference: tooltipRef.current, @@ -355,6 +355,7 @@ const Tooltip = ({ content, externalStyles, place, + imperativeOptions?.place, offset, positionStrategy, position, diff --git a/src/components/Tooltip/TooltipTypes.d.ts b/src/components/Tooltip/TooltipTypes.d.ts index 2cd7d59b..1413e20f 100644 --- a/src/components/Tooltip/TooltipTypes.d.ts +++ b/src/components/Tooltip/TooltipTypes.d.ts @@ -52,6 +52,7 @@ export interface IPosition { export interface TooltipImperativeOpenOptions { anchorSelect?: string position?: IPosition + place?: PlacesType content?: ChildrenType } From f53d1c9644c66f4cea24ba59363a60f6effcd877 Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Wed, 1 Nov 2023 15:29:07 -0300 Subject: [PATCH 03/18] docs: imperative mode --- docs/docs/examples/imperative-mode.mdx | 180 +++++++++++++++++++++++++ docs/docs/options.mdx | 1 + 2 files changed, 181 insertions(+) create mode 100644 docs/docs/examples/imperative-mode.mdx diff --git a/docs/docs/examples/imperative-mode.mdx b/docs/docs/examples/imperative-mode.mdx new file mode 100644 index 00000000..93d92eb2 --- /dev/null +++ b/docs/docs/examples/imperative-mode.mdx @@ -0,0 +1,180 @@ +--- +sidebar_position: 1 +--- + +# Imperative mode + +Using the ReactTooltip imperative mode to control the tooltip programatically. + +import { useRef } from 'react'; +import { Tooltip } from 'react-tooltip' + +export const TooltipAnchor = ({ children, id, ...rest }) => { + return ( + + {children} + + ) +} + +### Basic usage + +A ref object created with `React.useRef()` can passed to the `ref` tooltip prop. +It allows you to expose internal state variables (read-only), and also to control the tooltip programatically. + +The relevant interfaces are as follows: + +```ts +interface TooltipImperativeOpenOptions { + anchorSelect?: string + position?: IPosition + place?: PlacesType + /** + * In practice, `ChildrenType` -> `React.ReactNode` + */ + content?: ChildrenType +} + +interface TooltipImperativeProps { + open: (options?: TooltipImperativeOpenOptions) => void + close: () => void + /** + * @readonly + */ + activeAnchor: HTMLElement | null + /** + * @readonly + */ + place: PlacesType + /** + * @readonly + */ + isOpen: boolean +} +``` + +- `open()` opens the tooltip programatically. All of the function arguments are optional + - `anchorSelect` overrides the selector currently in use. Ideally, it should match only one element (e.g. `#some-element`) + - `position` overrides the tooltip position. Behaves the same way as the `position` tooltip prop + - `place` overrides the tooltip placement relative to the anchor. Behaves the same was the `place` tooltip prop + - `content` overrides the tooltip content +- `close()` closes the tooltip programatically + +:::note + +These are read-only. Updating their values has no effect on the tooltip. + +::: + +- `activeAnchor` is a reference to the current anchor element +- `place` is the current tooltip placement relative to the anchor element. Can differ from the `place` tooltip prop if the tooltip is close to the edges of its container +- `isOpen` indicates whether the tooltip is currently being shown or not + +:::info + +The imperative methods can be applied alongside regular tooltip usage. For example, you could use just `close()` to close a regular tooltip after an API request is finished. + +::: + +```jsx +import { useRef } from 'react'; +import { Tooltip, TooltipImperativeProps } from 'react-tooltip'; + +const tooltipRef1 = useRef(null) +const tooltipRef2 = useRef(null) + + + ◕‿‿◕ + + + + + +``` + +export const ImperativeModeExample = () => { + const tooltipRef1 = useRef(null) + const tooltipRef2 = useRef(null) + return ( + <> + + ◕‿‿◕ + +
+ + +
+ + + + ) +} + +
+ +
diff --git a/docs/docs/options.mdx b/docs/docs/options.mdx index d91b7d5c..0a902db7 100644 --- a/docs/docs/options.mdx +++ b/docs/docs/options.mdx @@ -89,6 +89,7 @@ import { Tooltip } from 'react-tooltip'; | name | type | required | default | values | description | | ----------------------- | -------------------------------------- | -------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ref` | Tooltip reference | no | | `React.useRef` | Reference object which exposes internal state, and some methods for manually controlling the tooltip. See [the examples](./examples/imperative-mode.mdx). | | `className` | `string` | no | | | Class name to customize tooltip element. You can also use the default class `react-tooltip` which is set internally | | `classNameArrow` | `string` | no | | | Class name to customize tooltip arrow element. You can also use the default class `react-tooltip-arrow` which is set internally | | `content` | `string` | no | | | Content to de displayed in tooltip (`html` prop is priorized over `content`) | From d50dde7ffe19f1a41a986baec6e9d3f98d2ad34b Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Wed, 1 Nov 2023 15:40:21 -0300 Subject: [PATCH 04/18] docs: imperative mode --- docs/docs/examples/imperative-mode.mdx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/docs/examples/imperative-mode.mdx b/docs/docs/examples/imperative-mode.mdx index 93d92eb2..e41502cd 100644 --- a/docs/docs/examples/imperative-mode.mdx +++ b/docs/docs/examples/imperative-mode.mdx @@ -70,13 +70,23 @@ interface TooltipImperativeProps { } ``` -- `open()` opens the tooltip programatically. All of the function arguments are optional +#### Methods + +:::info + +The imperative methods can be applied alongside regular tooltip usage. For example, you could use just `close()` to close a regular tooltip after an API request is finished. + +::: + +- `open()` opens the tooltip programatically. All of the arguments are optional - `anchorSelect` overrides the selector currently in use. Ideally, it should match only one element (e.g. `#some-element`) - `position` overrides the tooltip position. Behaves the same way as the `position` tooltip prop - `place` overrides the tooltip placement relative to the anchor. Behaves the same was the `place` tooltip prop - `content` overrides the tooltip content - `close()` closes the tooltip programatically +#### Internal state + :::note These are read-only. Updating their values has no effect on the tooltip. @@ -87,12 +97,6 @@ These are read-only. Updating their values has no effect on the tooltip. - `place` is the current tooltip placement relative to the anchor element. Can differ from the `place` tooltip prop if the tooltip is close to the edges of its container - `isOpen` indicates whether the tooltip is currently being shown or not -:::info - -The imperative methods can be applied alongside regular tooltip usage. For example, you could use just `close()` to close a regular tooltip after an API request is finished. - -::: - ```jsx import { useRef } from 'react'; import { Tooltip, TooltipImperativeProps } from 'react-tooltip'; From 4eb512803ca05f23dae7a776a3872eb514a6a068 Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Wed, 1 Nov 2023 15:41:47 -0300 Subject: [PATCH 05/18] docs: imperative mode --- docs/docs/examples/imperative-mode.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/examples/imperative-mode.mdx b/docs/docs/examples/imperative-mode.mdx index e41502cd..ee27ca2c 100644 --- a/docs/docs/examples/imperative-mode.mdx +++ b/docs/docs/examples/imperative-mode.mdx @@ -2,7 +2,7 @@ sidebar_position: 1 --- -# Imperative mode +# Imperative mode (ref) Using the ReactTooltip imperative mode to control the tooltip programatically. From c19a861390384edd2c1b051cc59e8f3473c841c6 Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Wed, 1 Nov 2023 15:43:34 -0300 Subject: [PATCH 06/18] docs: imperative mode --- docs/docs/examples/imperative-mode.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/examples/imperative-mode.mdx b/docs/docs/examples/imperative-mode.mdx index ee27ca2c..0f706276 100644 --- a/docs/docs/examples/imperative-mode.mdx +++ b/docs/docs/examples/imperative-mode.mdx @@ -80,8 +80,8 @@ The imperative methods can be applied alongside regular tooltip usage. Fo - `open()` opens the tooltip programatically. All of the arguments are optional - `anchorSelect` overrides the selector currently in use. Ideally, it should match only one element (e.g. `#some-element`) - - `position` overrides the tooltip position. Behaves the same way as the `position` tooltip prop - - `place` overrides the tooltip placement relative to the anchor. Behaves the same was the `place` tooltip prop + - `position` overrides the tooltip position. Behaves the same as the `position` tooltip prop + - `place` overrides the tooltip placement relative to the anchor. Behaves the same as the `place` tooltip prop - `content` overrides the tooltip content - `close()` closes the tooltip programatically From eaf4964e1d3e9848e4b9f4e60d6c7ae63bb3d9fd Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Wed, 1 Nov 2023 15:49:48 -0300 Subject: [PATCH 07/18] feat: imperative mode open/close delay --- src/components/Tooltip/Tooltip.tsx | 18 +++++++++++++----- src/components/Tooltip/TooltipTypes.d.ts | 7 ++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 79b3c021..1b9deda2 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -158,14 +158,14 @@ const Tooltip = ({ } }, [show]) - const handleShowTooltipDelayed = () => { + const handleShowTooltipDelayed = (delay = delayShow) => { if (tooltipShowDelayTimerRef.current) { clearTimeout(tooltipShowDelayTimerRef.current) } tooltipShowDelayTimerRef.current = setTimeout(() => { handleShow(true) - }, delayShow) + }, delay) } const handleHideTooltipDelayed = (delay = delayHide) => { @@ -672,10 +672,18 @@ const Tooltip = ({ } } setImperativeOptions(options ?? null) - handleShow(true) + if (options?.delay) { + handleShowTooltipDelayed(options.delay) + } else { + handleShow(true) + } }, - close: () => { - handleShow(false) + close: (options) => { + if (options?.delay) { + handleHideTooltipDelayed(options.delay) + } else { + handleShow(false) + } }, activeAnchor, place: actualPlacement, diff --git a/src/components/Tooltip/TooltipTypes.d.ts b/src/components/Tooltip/TooltipTypes.d.ts index 1413e20f..28d91687 100644 --- a/src/components/Tooltip/TooltipTypes.d.ts +++ b/src/components/Tooltip/TooltipTypes.d.ts @@ -54,11 +54,16 @@ export interface TooltipImperativeOpenOptions { position?: IPosition place?: PlacesType content?: ChildrenType + delay?: number +} + +export interface TooltipImperativeCloseOptions { + delay?: number } export interface TooltipImperativeProps { open: (options?: TooltipImperativeOpenOptions) => void - close: () => void + close: (options?: TooltipImperativeCloseOptions) => void /** * @readonly */ From b2160bc3f44bacc2fe1c32952e58d427688cb2c6 Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Wed, 1 Nov 2023 15:52:43 -0300 Subject: [PATCH 08/18] docs: imperative mode open/close delay --- docs/docs/examples/imperative-mode.mdx | 13 +++++++++++++ src/components/Tooltip/TooltipTypes.d.ts | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/docs/docs/examples/imperative-mode.mdx b/docs/docs/examples/imperative-mode.mdx index 0f706276..caf2e9ab 100644 --- a/docs/docs/examples/imperative-mode.mdx +++ b/docs/docs/examples/imperative-mode.mdx @@ -50,6 +50,17 @@ interface TooltipImperativeOpenOptions { * In practice, `ChildrenType` -> `React.ReactNode` */ content?: ChildrenType + /** + * Delay (in ms) before opening the tooltip. + */ + delay?: number +} + +interface TooltipImperativeCloseOptions { + /** + * Delay (in ms) before closing the tooltip. + */ + delay?: number } interface TooltipImperativeProps { @@ -83,7 +94,9 @@ The imperative methods can be applied alongside regular tooltip usage. Fo - `position` overrides the tooltip position. Behaves the same as the `position` tooltip prop - `place` overrides the tooltip placement relative to the anchor. Behaves the same as the `place` tooltip prop - `content` overrides the tooltip content + - `delay` indicates how long (in ms) before the tooltip actually opens - `close()` closes the tooltip programatically + - `delay` indicates how long (in ms) before the tooltip actually closes #### Internal state diff --git a/src/components/Tooltip/TooltipTypes.d.ts b/src/components/Tooltip/TooltipTypes.d.ts index 28d91687..aa2cd250 100644 --- a/src/components/Tooltip/TooltipTypes.d.ts +++ b/src/components/Tooltip/TooltipTypes.d.ts @@ -54,10 +54,16 @@ export interface TooltipImperativeOpenOptions { position?: IPosition place?: PlacesType content?: ChildrenType + /** + * @description Delay (in ms) before opening the tooltip. + */ delay?: number } export interface TooltipImperativeCloseOptions { + /** + * @description Delay (in ms) before closing the tooltip. + */ delay?: number } From 555c133b58fa0681e14b2a1050e1a31db0a56875 Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Wed, 1 Nov 2023 19:32:25 -0300 Subject: [PATCH 09/18] fix: improve click outside check performance --- src/components/Tooltip/Tooltip.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 1b9deda2..46fe80d5 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -282,12 +282,13 @@ const Tooltip = ({ if (!show) { return } - const anchorById = document.querySelector(`[id='${anchorId}']`) - const anchors = [anchorById, ...anchorsBySelect] - if (anchors.some((anchor) => anchor?.contains(event.target as HTMLElement))) { + const target = event.target as HTMLElement + if (tooltipRef.current?.contains(target)) { return } - if (tooltipRef.current?.contains(event.target as HTMLElement)) { + const anchorById = document.querySelector(`[id='${anchorId}']`) + const anchors = [anchorById, ...anchorsBySelect] + if (anchors.some((anchor) => anchor?.contains(target))) { return } handleShow(false) From d2a371b1031501f1345b18afa22ca1bf3d3ad186 Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Wed, 1 Nov 2023 19:39:01 -0300 Subject: [PATCH 10/18] Merge branch 'master' into feat/imperative-mode --- docs/docs/examples/events.mdx | 4 +- docs/docs/examples/styling.mdx | 20 ++ docs/docs/getting-started.mdx | 2 + docs/docs/options.mdx | 25 ++- docs/docs/upgrade-guide/changelog-v4-v5.md | 20 +- docs/package.json | 2 +- docs/yarn.lock | 8 +- package.json | 2 +- src/App.tsx | 4 +- src/components/Tooltip/Tooltip.tsx | 203 ++++++++++++------ src/components/Tooltip/TooltipTypes.d.ts | 24 +++ src/components/Tooltip/core-styles.module.css | 11 +- .../TooltipController/TooltipController.tsx | 6 + .../TooltipControllerTypes.d.ts | 23 +- src/tokens.css | 2 + 15 files changed, 255 insertions(+), 101 deletions(-) diff --git a/docs/docs/examples/events.mdx b/docs/docs/examples/events.mdx index 037c72a9..0d4d0a4b 100644 --- a/docs/docs/examples/events.mdx +++ b/docs/docs/examples/events.mdx @@ -8,7 +8,9 @@ Events available in ReactTooltip component. :::danger -This has been deprecated. Use the `openOnClick` tooltip prop instead. +This has been deprecated. Use `openOnClick`, or `openEvents`, `closeEvents`, and `globalCloseEvents` instead. + +See the [options page](../options.mdx#available-props) for more details. ::: diff --git a/docs/docs/examples/styling.mdx b/docs/docs/examples/styling.mdx index 724e479f..b4f1eec7 100644 --- a/docs/docs/examples/styling.mdx +++ b/docs/docs/examples/styling.mdx @@ -372,6 +372,26 @@ In summary, if you do it correctly you can use CSS specificity instead of `!impo ::: +### Customizing opening/closing animation + +By default, the tooltip has a fade-in/fade-out transition when opening/closing, with a delay of 150ms for both. +If you wish to change the delay for any of them, override the following CSS variables: + +:::caution + +Do not set `--rt-transition-closing-delay` to `0`. Doing so will result in the tooltip component being stuck (but not visible) on the DOM. This isn't itself a problem, but may lead to performance issues. + +Set to `1ms` or a similar value if you want to disable the fade-out transition when closing. + +::: + +```css +:root { + --rt-transition-show-delay: 0.15s; + --rt-transition-closing-delay: 0.15s; +} +``` + ### Disabling ReactTooltip CSS ReactTooltip works seamlessly by automatically injecting CSS into your application. To disable this functionality, use the tooltip prop `disableStyleInjection`. diff --git a/docs/docs/getting-started.mdx b/docs/docs/getting-started.mdx index 9beb573f..7bcb7a69 100644 --- a/docs/docs/getting-started.mdx +++ b/docs/docs/getting-started.mdx @@ -219,5 +219,7 @@ For advanced styling, check [the examples](./examples/styling.mdx). --rt-color-warning: #f0ad4e; --rt-color-info: #337ab7; --rt-opacity: 0.9; + --rt-transition-show-delay: 0.15s; + --rt-transition-closing-delay: 0.15s; } ``` diff --git a/docs/docs/options.mdx b/docs/docs/options.mdx index 0a902db7..9e72700d 100644 --- a/docs/docs/options.mdx +++ b/docs/docs/options.mdx @@ -92,8 +92,8 @@ import { Tooltip } from 'react-tooltip'; | `ref` | Tooltip reference | no | | `React.useRef` | Reference object which exposes internal state, and some methods for manually controlling the tooltip. See [the examples](./examples/imperative-mode.mdx). | | `className` | `string` | no | | | Class name to customize tooltip element. You can also use the default class `react-tooltip` which is set internally | | `classNameArrow` | `string` | no | | | Class name to customize tooltip arrow element. You can also use the default class `react-tooltip-arrow` which is set internally | -| `content` | `string` | no | | | Content to de displayed in tooltip (`html` prop is priorized over `content`) | -| ~~`html`~~ | ~~`string`~~ | ~~no~~ | | | ~~HTML content to de displayed in tooltip~~
**DEPRECATED**
Use `children` or `render` instead | +| `content` | `string` | no | | | Content to be displayed in tooltip (`html` prop is priorized over `content`) | +| ~~`html`~~ | ~~`string`~~ | ~~no~~ | | | ~~HTML content to be displayed in tooltip~~
**DEPRECATED**
Use `children` or `render` instead | | `render` | `function` | no | | | A function which receives a ref to the currently active anchor element and returns the content for the tooltip. Check the [examples](./examples/render.mdx) | | `place` | `string` | no | `top` | `top` `top-start` `top-end` `right` `right-start` `right-end` `bottom` `bottom-start` `bottom-end` `left` `left-start` `left-end` | Position relative to the anchor element where the tooltip will be rendered (if possible) | | `offset` | `number` | no | `10` | any `number` | Space between the tooltip element and anchor element (arrow not included in calculation) | @@ -104,7 +104,7 @@ import { Tooltip } from 'react-tooltip'; | `wrapper` | HTML tag | no | `div` | `div` `span` `p` ... | Element wrapper for the tooltip container, can be `div`, `span`, `p` or any valid HTML tag | | `children` | React node | no | `undefined` | valid React children | The tooltip children have lower priority compared to the `content` prop and the `data-tooltip-content` attribute. Useful for setting default content | | ~~`events`~~ | ~~`string[]`~~ | ~~no~~ | ~~`hover`~~ | ~~`hover` `click`~~ | ~~Events to watch for when handling the tooltip state~~
**DEPRECATED**
Use `openOnClick` tooltip prop instead | -| `openOnClick` | `boolean` | no | `false` | `true` `false` | Controls whether the tooltip should open when clicking (`true`) or hovering (`false`) the anchor element | +| `openOnClick` | `boolean` | no | `false` | `true` `false` | When enabled, the tooltip will open on click instead of on hover. Use `openEvents`, `closeEvents`, and `globalCloseEvents` for more customization | | `positionStrategy` | `string` | no | `absolute` | `absolute` `fixed` | The position strategy used for the tooltip. Set to `fixed` if you run into issues with `overflow: hidden` on the tooltip parent container | | `delayShow` | `number` | no | | any `number` | The delay (in ms) before showing the tooltip | | `delayHide` | `number` | no | | any `number` | The delay (in ms) before hiding the tooltip | @@ -112,17 +112,20 @@ import { Tooltip } from 'react-tooltip'; | `hidden` | `boolean` | no | `false` | `true` `false` | Tooltip will not be shown | | `noArrow` | `boolean` | no | `false` | `true` `false` | Tooltip arrow will not be shown | | `clickable` | `boolean` | no | `false` | `true` `false` | Allow interaction with elements inside the tooltip. Useful when using buttons and inputs | -| `closeOnEsc` | `boolean` | no | `false` | `true` `false` | Pressing escape key will close the tooltip | -| `closeOnScroll` | `boolean` | no | `false` | `true` `false` | Scrolling will close the tooltip (for this to work, scroll element must be either the root html tag, the tooltip parent, or the anchor parent) | -| `closeOnResize` | `boolean` | no | `false` | `true` `false` | Resizing the window will close the tooltip | +| ~~`closeOnEsc`~~ | ~~`boolean`~~ | ~~no~~ | ~~`false`~~ | ~~`true` `false`~~ | ~~Pressing escape key will close the tooltip~~
**DEPRECATED**
Use `globalCloseEvents` instead. | +| ~~`closeOnScroll`~~ | ~~`boolean`~~ | ~~no~~ | ~~`false`~~ | ~~`true` `false`~~ | ~~Scrolling will close the tooltip~~
**DEPRECATED**
Use `globalCloseEvents` instead. | +| ~~`closeOnResize`~~ | ~~`boolean`~~ | ~~no~~ | ~~`false`~~ | ~~`true` `false`~~ | ~~Resizing the window will close the tooltip~~
**DEPRECATED**
Use `globalCloseEvents` instead. | +| `openEvents` | `Record` | no | `mouseenter` `focus` | `mouseenter` `focus` `click` `dblclick` `mousedown` | Events to be listened on the anchor elements to open the tooltip | +| `closeEvents` | `Record` | no | `mouseleave` `blur` | `mouseleave` `blur` `click` `dblclick` `mouseup` | Events to be listened on the anchor elements to close the tooltip | +| `globalCloseEvents` | `Record` | no | | `escape` `scroll` `resize` `clickOutsideAnchor` | Global events to be listened to close the tooltip (`escape` closes on pressing `ESC`, `clickOutsideAnchor` is useful with click events on `openEvents`) | | `style` | `CSSProperties` | no | | a CSS style object | Add inline styles directly to the tooltip | | `position` | `{ x: number; y: number }` | no | | any `number` value for both `x` and `y` | Override the tooltip position on the DOM | -| `isOpen` | `boolean` | no | handled by internal state | `true` `false` | The tooltip can be controlled or uncontrolled, this attribute can be used to handle show and hide tooltip outside tooltip (can be used **without** `setIsOpen`) | +| `isOpen` | `boolean` | no | | `true` `false` | The tooltip can be controlled or uncontrolled, this attribute can be used to handle show and hide tooltip outside tooltip (can be used **without** `setIsOpen`) | | `setIsOpen` | `function` | no | | | The tooltip can be controlled or uncontrolled, this attribute can be used to handle show and hide tooltip outside tooltip | | `afterShow` | `function` | no | | | A function to be called after the tooltip is shown | | `afterHide` | `function` | no | | | A function to be called after the tooltip is hidden | | `middlewares` | `Middleware[]` | no | | array of valid `floating-ui` middlewares | Allows for advanced customization. Check the [`floating-ui` docs](https://floating-ui.com/docs/middleware) for more information | -| `border` | `CSSProperties['border']` | no | | a CSS border style | Change the style of the tooltip border (including the arrow) | -| `opacity` | `CSSProperties['opacity']` | no | `0.9` | a CSS opacity value | Change the opacity of the tooltip | -| `arrowColor` | `CSSProperties['backgroundColor']` | no | | a CSS background color | Change color of the tooltip arrow | -| `disableStyleInjection` | `boolean` | `'core'` | no | `false` | `true` `false` `'core'` | Whether to disable automatic style injection. Do not set dynamically. Check the [styling page](./examples/styling#disabling-reacttooltip-css) for more details | +| `border` | CSS border | no | | a CSS border style | Change the style of the tooltip border (including the arrow) | +| `opacity` | CSS opacity | no | `0.9` | a CSS opacity value | Change the opacity of the tooltip | +| `arrowColor` | CSS color | no | | a CSS background color | Change color of the tooltip arrow | +| `disableStyleInjection` | `boolean` or `'core'` | no | `false` | `true` `false` `'core'` | Whether to disable automatic style injection. Do not set dynamically. Check the [styling page](./examples/styling#disabling-reacttooltip-css) for more details | diff --git a/docs/docs/upgrade-guide/changelog-v4-v5.md b/docs/docs/upgrade-guide/changelog-v4-v5.md index a63a48f3..599bbd61 100644 --- a/docs/docs/upgrade-guide/changelog-v4-v5.md +++ b/docs/docs/upgrade-guide/changelog-v4-v5.md @@ -50,9 +50,15 @@ If you run into any problems with the tooltip not updating after changes are mad - [x] `float` - `boolean` - used to achieve V4's `effect="float"` - [x] `hidden` - `boolean` - when set, the tooltip will not show - [x] `render` - `function` - can be used to render dynamic content based on the active anchor element (check [the examples](../examples/render.mdx) for more details) -- [x] `closeOnEsc` - `boolean` - when set, the tooltip will close after pressing the escape key -- [x] `closeOnScroll` - `boolean` - when set, the tooltip will close when scrolling (similar to V4's `scrollHide`) -- [x] `closeOnResize` - `boolean` - when set, the tooltip will close when resizing the window (same as V4's `resizeHide`) +- [x] `closeOnEsc` - **DEPRECATED** - ~~`boolean` - when set, the tooltip will close after pressing the escape key~~ +- [x] `closeOnScroll` - **DEPRECATED** - ~~`boolean` - when set, the tooltip will close when scrolling (similar to V4's `scrollHide`)~~ +- [x] `closeOnResize` - **DEPRECATED** - ~~`boolean` - when set, the tooltip will close when resizing the window (same as V4's `resizeHide`)~~ + +:::note + +Use `globalCloseEvents` instead of `closeOnEsc`, `closeOnScroll`, and `closeOnResize`. See the [options page](../options.mdx#available-props) for more details. + +::: ## `V4` props available in `V5` @@ -78,10 +84,10 @@ If you run into any problems with the tooltip not updating after changes are mad - [x] `delayHide` - also available on anchor element as `data-delay-hide` - [ ] `delayUpdate` - can be implemented if requested - [x] `delayShow` - also available on anchor element as `data-delay-show` -- [ ] `event` -- [ ] `eventOff` +- [x] `event` - functionality changed and renamed to `openEvents` +- [x] `eventOff` - functionality changed and renamed to `closeEvents` - [ ] `isCapture` -- [ ] `globalEventOff` +- [x] `globalEventOff` - functionality changed and renamed to `globalCloseEvents` - [ ] `getContent` - pass dynamic values to `content` instead - [x] `afterShow` - [x] `afterHide` @@ -92,7 +98,7 @@ If you run into any problems with the tooltip not updating after changes are mad - [x] `wrapper` - also available on anchor element as `data-tooltip-wrapper` - [ ] `bodyMode` - [x] `clickable` -- [ ] `disableInternalStyle` +- [x] `disableInternalStyle` - renamed to `disableStyleInjection` ### Detailed informations diff --git a/docs/package.json b/docs/package.json index 957c8cd0..a00fdc6c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -23,7 +23,7 @@ "raw-loader": "^4.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-tooltip": "5.21.1" + "react-tooltip": "5.22.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "^2.4.1", diff --git a/docs/yarn.lock b/docs/yarn.lock index 3dc1311a..faae58dd 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -6126,10 +6126,10 @@ react-textarea-autosize@^8.3.2: use-composed-ref "^1.3.0" use-latest "^1.2.1" -react-tooltip@5.21.1: - version "5.21.1" - resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.21.1.tgz#bc49d9662a4b90ed1ed4417b7de535cb5bee1d01" - integrity sha512-wJqF/yzK1wuJuy5/zAkVErFA609fVv1ZukhGjw44PcMvg9wL0jomnpQyz3qH1H7TWjz/wqO/OMc3ipQNjZ8zYg== +react-tooltip@5.22.0: + version "5.22.0" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.22.0.tgz#3082065b1efbd207813f484b617b1b75325f15d5" + integrity sha512-xbJBRY1LyHYd7j00UeBOqZR9SH/1S47Pe+m8vM1a+ZXglkeSNnBt5YYoPttU/amjC/VZJAPQ8+2B9x8Fl8U1qA== dependencies: "@floating-ui/dom" "^1.0.0" classnames "^2.3.0" diff --git a/package.json b/package.json index 7f0ea2c8..7d9cf5ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-tooltip", - "version": "5.21.5", + "version": "5.22.0", "description": "react tooltip component", "scripts": { "dev-rollup": "node ./prebuild.js --env=development && node --max_old_space_size=2048 ./node_modules/rollup/dist/bin/rollup -c rollup.config.dev.js --watch", diff --git a/src/App.tsx b/src/App.tsx index 29910fef..4a0a4147 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -103,7 +103,9 @@ function App() { ref={tooltipRef} anchorSelect="section[id='section-anchor-select'] > p > button" place="bottom" - events={['click']} + openEvents={{ click: true }} + closeEvents={{ click: true }} + globalCloseEvents={{ clickOutsideAnchor: true }} > Tooltip content
diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 46fe80d5..019e995a 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -8,7 +8,15 @@ import { getScrollParent } from 'utils/get-scroll-parent' import { computeTooltipPosition } from 'utils/compute-positions' import coreStyles from './core-styles.module.css' import styles from './styles.module.css' -import type { IPosition, ITooltip, PlacesType, TooltipImperativeOpenOptions } from './TooltipTypes' +import type { + AnchorCloseEvents, + AnchorOpenEvents, + GlobalCloseEvents, + IPosition, + ITooltip, + PlacesType, + TooltipImperativeOpenOptions, +} from './TooltipTypes' const Tooltip = ({ // props @@ -35,6 +43,9 @@ const Tooltip = ({ closeOnEsc = false, closeOnScroll = false, closeOnResize = false, + openEvents, + closeEvents, + globalCloseEvents, style: externalStyles, position, afterShow, @@ -72,7 +83,49 @@ const Tooltip = ({ const [anchorsBySelect, setAnchorsBySelect] = useState([]) const mounted = useRef(false) + /** + * @todo Update when deprecated stuff gets removed. + */ const shouldOpenOnClick = openOnClick || events.includes('click') + const hasClickEvent = + shouldOpenOnClick || openEvents?.click || openEvents?.dblclick || openEvents?.mousedown + const actualOpenEvents: AnchorOpenEvents = openEvents + ? { ...openEvents } + : { + mouseenter: true, + focus: true, + click: false, + dblclick: false, + mousedown: false, + } + if (!openEvents && shouldOpenOnClick) { + Object.assign(actualOpenEvents, { + mouseenter: false, + focus: false, + click: true, + }) + } + const actualCloseEvents: AnchorCloseEvents = closeEvents + ? { ...closeEvents } + : { + mouseleave: true, + blur: true, + click: false, + } + if (!closeEvents && shouldOpenOnClick) { + Object.assign(actualCloseEvents, { + mouseleave: false, + blur: false, + }) + } + const actualGlobalCloseEvents: GlobalCloseEvents = globalCloseEvents + ? { ...globalCloseEvents } + : { + escape: closeOnEsc || false, + scroll: closeOnScroll || false, + resize: closeOnResize || false, + clickOutsideAnchor: hasClickEvent || false, + } /** * useLayoutEffect runs before useEffect, @@ -86,24 +139,6 @@ const Tooltip = ({ } }, []) - useEffect(() => { - if (!show) { - /** - * this fixes weird behavior when switching between two anchor elements very quickly - * remove the timeout and switch quickly between two adjancent anchor elements to see it - * - * in practice, this means the tooltip is not immediately removed from the DOM on hide - */ - const timeout = setTimeout(() => { - setRendered(false) - }, 150) - return () => { - clearTimeout(timeout) - } - } - return () => null - }, [show]) - const handleShow = (value: boolean) => { if (!mounted.current) { return @@ -271,13 +306,6 @@ const Tooltip = ({ lastFloatPosition.current = mousePosition } - const handleClickTooltipAnchor = (event?: Event) => { - handleShowTooltip(event) - if (delayHide) { - handleHideTooltipDelayed() - } - } - const handleClickOutsideAnchors = (event: MouseEvent) => { if (!show) { return @@ -383,13 +411,13 @@ const Tooltip = ({ const anchorScrollParent = getScrollParent(activeAnchor) const tooltipScrollParent = getScrollParent(tooltipRef.current) - if (closeOnScroll) { + if (actualGlobalCloseEvents.scroll) { window.addEventListener('scroll', handleScrollResize) anchorScrollParent?.addEventListener('scroll', handleScrollResize) tooltipScrollParent?.addEventListener('scroll', handleScrollResize) } let updateTooltipCleanup: null | (() => void) = null - if (closeOnResize) { + if (actualGlobalCloseEvents.resize) { window.addEventListener('resize', handleScrollResize) } else if (activeAnchor && tooltipRef.current) { updateTooltipCleanup = autoUpdate( @@ -410,29 +438,63 @@ const Tooltip = ({ } handleShow(false) } - - if (closeOnEsc) { + if (actualGlobalCloseEvents.escape) { window.addEventListener('keydown', handleEsc) } + if (actualGlobalCloseEvents.clickOutsideAnchor) { + window.addEventListener('click', handleClickOutsideAnchors) + } + const enabledEvents: { event: string; listener: (event?: Event) => void }[] = [] - if (shouldOpenOnClick) { - window.addEventListener('click', handleClickOutsideAnchors) - enabledEvents.push({ event: 'click', listener: handleClickTooltipAnchor }) - } else { - enabledEvents.push( - { event: 'mouseenter', listener: debouncedHandleShowTooltip }, - { event: 'mouseleave', listener: debouncedHandleHideTooltip }, - { event: 'focus', listener: debouncedHandleShowTooltip }, - { event: 'blur', listener: debouncedHandleHideTooltip }, - ) - if (float) { - enabledEvents.push({ - event: 'mousemove', - listener: handleMouseMove, - }) + const handleClickOpenTooltipAnchor = (event?: Event) => { + if (show) { + return + } + handleShowTooltip(event) + } + const handleClickCloseTooltipAnchor = () => { + if (!show) { + return } + handleHideTooltip() + } + + const regularEvents = ['mouseenter', 'mouseleave', 'focus', 'blur'] + const clickEvents = ['click', 'dblclick', 'mousedown', 'mouseup'] + + Object.entries(actualOpenEvents).forEach(([event, enabled]) => { + if (!enabled) { + return + } + if (regularEvents.includes(event)) { + enabledEvents.push({ event, listener: debouncedHandleShowTooltip }) + } else if (clickEvents.includes(event)) { + enabledEvents.push({ event, listener: handleClickOpenTooltipAnchor }) + } else { + // never happens + } + }) + + Object.entries(actualCloseEvents).forEach(([event, enabled]) => { + if (!enabled) { + return + } + if (regularEvents.includes(event)) { + enabledEvents.push({ event, listener: debouncedHandleHideTooltip }) + } else if (clickEvents.includes(event)) { + enabledEvents.push({ event, listener: handleClickCloseTooltipAnchor }) + } else { + // never happens + } + }) + + if (float) { + enabledEvents.push({ + event: 'mousemove', + listener: handleMouseMove, + }) } const handleMouseEnterTooltip = () => { @@ -443,7 +505,9 @@ const Tooltip = ({ handleHideTooltip() } - if (clickable && !shouldOpenOnClick) { + if (clickable && !hasClickEvent) { + // used to keep the tooltip open when hovering content. + // not needed if using click events. tooltipRef.current?.addEventListener('mouseenter', handleMouseEnterTooltip) tooltipRef.current?.addEventListener('mouseleave', handleMouseLeaveTooltip) } @@ -455,23 +519,23 @@ const Tooltip = ({ }) return () => { - if (closeOnScroll) { + if (actualGlobalCloseEvents.scroll) { window.removeEventListener('scroll', handleScrollResize) anchorScrollParent?.removeEventListener('scroll', handleScrollResize) tooltipScrollParent?.removeEventListener('scroll', handleScrollResize) } - if (closeOnResize) { + if (actualGlobalCloseEvents.resize) { window.removeEventListener('resize', handleScrollResize) } else { updateTooltipCleanup?.() } - if (shouldOpenOnClick) { + if (actualGlobalCloseEvents.clickOutsideAnchor) { window.removeEventListener('click', handleClickOutsideAnchors) } - if (closeOnEsc) { + if (actualGlobalCloseEvents.escape) { window.removeEventListener('keydown', handleEsc) } - if (clickable && !shouldOpenOnClick) { + if (clickable && !hasClickEvent) { tooltipRef.current?.removeEventListener('mouseenter', handleMouseEnterTooltip) tooltipRef.current?.removeEventListener('mouseleave', handleMouseLeaveTooltip) } @@ -491,8 +555,11 @@ const Tooltip = ({ rendered, anchorRefs, anchorsBySelect, - closeOnEsc, - events, + // the effect uses the `actual*Events` objects, but this should work + openEvents, + closeEvents, + globalCloseEvents, + shouldOpenOnClick, ]) useEffect(() => { @@ -580,7 +647,7 @@ const Tooltip = ({ }) if (newAnchors.length || removedAnchors.length) { setAnchorsBySelect((anchors) => [ - ...anchors.filter((anchor) => removedAnchors.includes(anchor)), + ...anchors.filter((anchor) => !removedAnchors.includes(anchor)), ...newAnchors, ]) } @@ -702,13 +769,21 @@ const Tooltip = ({ styles[variant], className, `react-tooltip__place-${actualPlacement}`, - { - 'react-tooltip__show': canShow, - [coreStyles['show']]: canShow, - [coreStyles['fixed']]: positionStrategy === 'fixed', - [coreStyles['clickable']]: clickable, - }, + coreStyles[canShow ? 'show' : 'closing'], + canShow ? 'react-tooltip__show' : 'react-tooltip__closing', + positionStrategy === 'fixed' && coreStyles['fixed'], + clickable && coreStyles['clickable'], )} + onTransitionEnd={(event: TransitionEvent) => { + /** + * @warning if `--rt-transition-closing-delay` is set to 0, + * the tooltip will be stuck (but not visible) on the DOM + */ + if (show || event.propertyName !== 'opacity') { + return + } + setRendered(false) + }} style={{ ...externalStyles, ...inlineStyles, @@ -723,13 +798,7 @@ const Tooltip = ({ coreStyles['arrow'], styles['arrow'], classNameArrow, - { - /** - * changed from dash `no-arrow` to camelcase because of: - * https://github.com/indooorsman/esbuild-css-modules-plugin/issues/42 - */ - [coreStyles['noArrow']]: noArrow, - }, + noArrow && coreStyles['noArrow'], )} style={{ ...inlineArrowStyles, diff --git a/src/components/Tooltip/TooltipTypes.d.ts b/src/components/Tooltip/TooltipTypes.d.ts index aa2cd250..121163e6 100644 --- a/src/components/Tooltip/TooltipTypes.d.ts +++ b/src/components/Tooltip/TooltipTypes.d.ts @@ -84,6 +84,27 @@ export interface TooltipImperativeProps { isOpen: boolean } +export type AnchorOpenEvents = { + mouseenter?: boolean + focus?: boolean + click?: boolean + dblclick?: boolean + mousedown?: boolean +} +export type AnchorCloseEvents = { + mouseleave?: boolean + blur?: boolean + click?: boolean + dblclick?: boolean + mouseup?: boolean +} +export type GlobalCloseEvents = { + escape?: boolean + scroll?: boolean + resize?: boolean + clickOutsideAnchor?: boolean +} + export interface ITooltip { forwardRef?: React.ForwardedRef className?: string @@ -117,6 +138,9 @@ export interface ITooltip { closeOnEsc?: boolean closeOnScroll?: boolean closeOnResize?: boolean + openEvents?: AnchorOpenEvents + closeEvents?: AnchorCloseEvents + globalCloseEvents?: GlobalCloseEvents style?: CSSProperties position?: IPosition isOpen?: boolean diff --git a/src/components/Tooltip/core-styles.module.css b/src/components/Tooltip/core-styles.module.css index bc317a17..cbb99823 100644 --- a/src/components/Tooltip/core-styles.module.css +++ b/src/components/Tooltip/core-styles.module.css @@ -1,12 +1,10 @@ .tooltip { - visibility: hidden; position: absolute; top: 0; left: 0; pointer-events: none; opacity: 0; - transition: opacity 0.3s ease-out; - will-change: opacity, visibility; + will-change: opacity; } .fixed { @@ -27,8 +25,13 @@ } .show { - visibility: visible; opacity: var(--rt-opacity); + transition: opacity var(--rt-transition-show-delay) ease-out; +} + +.closing { + opacity: 0; + transition: opacity var(--rt-transition-closing-delay) ease-in; } /** end - core styles **/ diff --git a/src/components/TooltipController/TooltipController.tsx b/src/components/TooltipController/TooltipController.tsx index 5dd2de08..03b5a0a0 100644 --- a/src/components/TooltipController/TooltipController.tsx +++ b/src/components/TooltipController/TooltipController.tsx @@ -44,6 +44,9 @@ const TooltipController = React.forwardRef Date: Wed, 1 Nov 2023 19:46:39 -0300 Subject: [PATCH 11/18] fix: `afterHide()` and imperative options reset --- src/components/Tooltip/Tooltip.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 019e995a..7092f95a 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -188,8 +188,9 @@ const Tooltip = ({ if (show) { afterShow?.() } else { - setImperativeOptions(null) - afterHide?.() + /** + * see `onTransitionEnd` on tooltip wrapper + */ } }, [show]) @@ -783,6 +784,8 @@ const Tooltip = ({ return } setRendered(false) + setImperativeOptions(null) + afterHide?.() }} style={{ ...externalStyles, From a8f5443d2c16b5c559d5d35c88e3bd1d5fd63e93 Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Wed, 1 Nov 2023 19:50:34 -0300 Subject: [PATCH 12/18] feat: `imperativeModeOnly` prop --- src/App.tsx | 4 +-- src/components/Tooltip/Tooltip.tsx | 26 +++++++++++++++++++ src/components/Tooltip/TooltipTypes.d.ts | 1 + .../TooltipController/TooltipController.tsx | 2 ++ .../TooltipControllerTypes.d.ts | 5 ++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4a0a4147..25264569 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -103,9 +103,7 @@ function App() { ref={tooltipRef} anchorSelect="section[id='section-anchor-select'] > p > button" place="bottom" - openEvents={{ click: true }} - closeEvents={{ click: true }} - globalCloseEvents={{ clickOutsideAnchor: true }} + imperativeModeOnly > Tooltip content
diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 7092f95a..60698844 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -46,6 +46,7 @@ const Tooltip = ({ openEvents, closeEvents, globalCloseEvents, + imperativeModeOnly, style: externalStyles, position, afterShow, @@ -111,6 +112,8 @@ const Tooltip = ({ mouseleave: true, blur: true, click: false, + dblclick: false, + mouseup: false, } if (!closeEvents && shouldOpenOnClick) { Object.assign(actualCloseEvents, { @@ -127,6 +130,29 @@ const Tooltip = ({ clickOutsideAnchor: hasClickEvent || false, } + if (imperativeModeOnly) { + Object.assign(actualOpenEvents, { + mouseenter: false, + focus: false, + click: false, + dblclick: false, + mousedown: false, + }) + Object.assign(actualCloseEvents, { + mouseleave: false, + blur: false, + click: false, + dblclick: false, + mouseup: false, + }) + Object.assign(actualGlobalCloseEvents, { + escape: false, + scroll: false, + resize: false, + clickOutsideAnchor: false, + }) + } + /** * useLayoutEffect runs before useEffect, * but should be used carefully because of caveats diff --git a/src/components/Tooltip/TooltipTypes.d.ts b/src/components/Tooltip/TooltipTypes.d.ts index 121163e6..83a26319 100644 --- a/src/components/Tooltip/TooltipTypes.d.ts +++ b/src/components/Tooltip/TooltipTypes.d.ts @@ -141,6 +141,7 @@ export interface ITooltip { openEvents?: AnchorOpenEvents closeEvents?: AnchorCloseEvents globalCloseEvents?: GlobalCloseEvents + imperativeModeOnly?: boolean style?: CSSProperties position?: IPosition isOpen?: boolean diff --git a/src/components/TooltipController/TooltipController.tsx b/src/components/TooltipController/TooltipController.tsx index 03b5a0a0..592817d3 100644 --- a/src/components/TooltipController/TooltipController.tsx +++ b/src/components/TooltipController/TooltipController.tsx @@ -47,6 +47,7 @@ const TooltipController = React.forwardRef Date: Wed, 1 Nov 2023 20:01:32 -0300 Subject: [PATCH 13/18] docs: `imperativeModeOnly` prop --- docs/docs/examples/imperative-mode.mdx | 10 ++++++++++ docs/docs/options.mdx | 1 + 2 files changed, 11 insertions(+) diff --git a/docs/docs/examples/imperative-mode.mdx b/docs/docs/examples/imperative-mode.mdx index caf2e9ab..103715d5 100644 --- a/docs/docs/examples/imperative-mode.mdx +++ b/docs/docs/examples/imperative-mode.mdx @@ -87,6 +87,8 @@ interface TooltipImperativeProps { The imperative methods can be applied alongside regular tooltip usage. For example, you could use just `close()` to close a regular tooltip after an API request is finished. +If you intend on using the tooltip exclusively with these methods, setting the `imperativeModeOnly` tooltip prop to disable default behavior is recommended. Otherwise, you might face undesired behavior. + ::: - `open()` opens the tooltip programatically. All of the arguments are optional @@ -150,6 +152,14 @@ const tooltipRef2 = useRef(null) ``` +:::caution + +Notice the tooltip still closes when unhovering the anchor element. This might be undesired if you're using the imperative methods exclusively. + +If that's the case, use the `imperativeModeOnly` tooltip prop to disable default tooltip behavior. + +::: + export const ImperativeModeExample = () => { const tooltipRef1 = useRef(null) const tooltipRef2 = useRef(null) diff --git a/docs/docs/options.mdx b/docs/docs/options.mdx index 9e72700d..de25b904 100644 --- a/docs/docs/options.mdx +++ b/docs/docs/options.mdx @@ -118,6 +118,7 @@ import { Tooltip } from 'react-tooltip'; | `openEvents` | `Record` | no | `mouseenter` `focus` | `mouseenter` `focus` `click` `dblclick` `mousedown` | Events to be listened on the anchor elements to open the tooltip | | `closeEvents` | `Record` | no | `mouseleave` `blur` | `mouseleave` `blur` `click` `dblclick` `mouseup` | Events to be listened on the anchor elements to close the tooltip | | `globalCloseEvents` | `Record` | no | | `escape` `scroll` `resize` `clickOutsideAnchor` | Global events to be listened to close the tooltip (`escape` closes on pressing `ESC`, `clickOutsideAnchor` is useful with click events on `openEvents`) | +| `imperativeModeOnly` | `boolean` | no | `false` | `true` `false` | When enabled, default tooltip behavior is disabled. Check [the examples](./examples/imperative-mode.mdx) for more details | | `style` | `CSSProperties` | no | | a CSS style object | Add inline styles directly to the tooltip | | `position` | `{ x: number; y: number }` | no | | any `number` value for both `x` and `y` | Override the tooltip position on the DOM | | `isOpen` | `boolean` | no | | `true` `false` | The tooltip can be controlled or uncontrolled, this attribute can be used to handle show and hide tooltip outside tooltip (can be used **without** `setIsOpen`) | From 075d3eb5e4a880ebb94add923baff9ddea7f81f2 Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Wed, 1 Nov 2023 20:07:58 -0300 Subject: [PATCH 14/18] docs: text improvements --- docs/docs/examples/imperative-mode.mdx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/docs/examples/imperative-mode.mdx b/docs/docs/examples/imperative-mode.mdx index 103715d5..1c359b0d 100644 --- a/docs/docs/examples/imperative-mode.mdx +++ b/docs/docs/examples/imperative-mode.mdx @@ -4,7 +4,7 @@ sidebar_position: 1 # Imperative mode (ref) -Using the ReactTooltip imperative mode to control the tooltip programatically. +Using the ReactTooltip imperative mode to control the tooltip programmatically. import { useRef } from 'react'; import { Tooltip } from 'react-tooltip' @@ -36,10 +36,10 @@ export const TooltipAnchor = ({ children, id, ...rest }) => { ### Basic usage -A ref object created with `React.useRef()` can passed to the `ref` tooltip prop. -It allows you to expose internal state variables (read-only), and also to control the tooltip programatically. +A ref object created with `React.useRef()` can be passed to the `ref` tooltip prop. +It allows you to expose internal state variables (read-only), and to also control the tooltip programmatically. -The relevant interfaces are as follows: +#### API ```ts interface TooltipImperativeOpenOptions { @@ -85,19 +85,19 @@ interface TooltipImperativeProps { :::info -The imperative methods can be applied alongside regular tooltip usage. For example, you could use just `close()` to close a regular tooltip after an API request is finished. +The imperative methods can be applied alongside regular tooltip usage. For example, you could use just `close()` to close a regular tooltip after an HTTP request is finished. -If you intend on using the tooltip exclusively with these methods, setting the `imperativeModeOnly` tooltip prop to disable default behavior is recommended. Otherwise, you might face undesired behavior. +If you intend to use the tooltip exclusively with these methods, setting the `imperativeModeOnly` prop to disable default behavior is recommended. Otherwise, you might face undesired behavior. ::: -- `open()` opens the tooltip programatically. All of the arguments are optional - - `anchorSelect` overrides the selector currently in use. Ideally, it should match only one element (e.g. `#some-element`) - - `position` overrides the tooltip position. Behaves the same as the `position` tooltip prop - - `place` overrides the tooltip placement relative to the anchor. Behaves the same as the `place` tooltip prop - - `content` overrides the tooltip content +- `open()` opens the tooltip programmatically. All arguments are optional + - `anchorSelect` overrides the current selector. Ideally, it should match only one element (e.g. `#my-element`) + - `position` overrides the `position` tooltip prop + - `place` overrides the `place` tooltip prop + - `content` overrides the tooltip content, whether it was set through `content`, `render`, or any other way - `delay` indicates how long (in ms) before the tooltip actually opens -- `close()` closes the tooltip programatically +- `close()` closes the tooltip programmatically - `delay` indicates how long (in ms) before the tooltip actually closes #### Internal state From 7bd996d575b232b5087d434f475d826540c703dc Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Fri, 3 Nov 2023 14:04:29 -0300 Subject: [PATCH 15/18] docs: fix imperative mode interface --- docs/docs/examples/imperative-mode.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/examples/imperative-mode.mdx b/docs/docs/examples/imperative-mode.mdx index 1c359b0d..a6bda6cb 100644 --- a/docs/docs/examples/imperative-mode.mdx +++ b/docs/docs/examples/imperative-mode.mdx @@ -65,7 +65,7 @@ interface TooltipImperativeCloseOptions { interface TooltipImperativeProps { open: (options?: TooltipImperativeOpenOptions) => void - close: () => void + close: (options?: TooltipImperativeCloseOptions) => void /** * @readonly */ From 1d0a8f9ad6811b2e269f9c529c16a277d2f89d21 Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Mon, 13 Nov 2023 14:49:07 -0300 Subject: [PATCH 16/18] fix: explicit boolean type --- src/components/Tooltip/Tooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 708b19b8..eda9e2d9 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -782,7 +782,7 @@ const Tooltip = ({ }, activeAnchor, place: actualPlacement, - isOpen: rendered && !hidden && actualContent && canShow, + isOpen: Boolean(rendered && !hidden && actualContent && canShow), })) return rendered && !hidden && actualContent ? ( From ae686155e65e98ad1ccc974ca2f556ba1c3d0f67 Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Mon, 13 Nov 2023 14:49:29 -0300 Subject: [PATCH 17/18] refactor: `TooltipRefProps` --- src/App.tsx | 4 ++-- src/components/Tooltip/TooltipTypes.d.ts | 4 ++-- src/components/TooltipController/TooltipController.tsx | 4 ++-- src/index.tsx | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4a0a4147..18a48af9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ /* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import { TooltipController as Tooltip } from 'components/TooltipController' -import { IPosition, TooltipImperativeProps } from 'components/Tooltip/TooltipTypes.d' +import { IPosition, TooltipRefProps } from 'components/Tooltip/TooltipTypes.d' import React, { useEffect, useRef, useState } from 'react' import { inline, offset } from '@floating-ui/dom' import styles from './styles.module.css' @@ -11,7 +11,7 @@ function App() { const [isDarkOpen, setIsDarkOpen] = useState(false) const [position, setPosition] = useState({ x: 0, y: 0 }) const [toggle, setToggle] = useState(false) - const tooltipRef = useRef(null) + const tooltipRef = useRef(null) const handlePositionClick: React.MouseEventHandler = (event) => { const x = event.clientX diff --git a/src/components/Tooltip/TooltipTypes.d.ts b/src/components/Tooltip/TooltipTypes.d.ts index 83a26319..71b0f97e 100644 --- a/src/components/Tooltip/TooltipTypes.d.ts +++ b/src/components/Tooltip/TooltipTypes.d.ts @@ -67,7 +67,7 @@ export interface TooltipImperativeCloseOptions { delay?: number } -export interface TooltipImperativeProps { +export interface TooltipRefProps { open: (options?: TooltipImperativeOpenOptions) => void close: (options?: TooltipImperativeCloseOptions) => void /** @@ -106,7 +106,7 @@ export type GlobalCloseEvents = { } export interface ITooltip { - forwardRef?: React.ForwardedRef + forwardRef?: React.ForwardedRef className?: string classNameArrow?: string content?: ChildrenType diff --git a/src/components/TooltipController/TooltipController.tsx b/src/components/TooltipController/TooltipController.tsx index 1b05c4f7..cdc65cff 100644 --- a/src/components/TooltipController/TooltipController.tsx +++ b/src/components/TooltipController/TooltipController.tsx @@ -9,14 +9,14 @@ import type { DataAttribute, ITooltip, ChildrenType, - TooltipImperativeProps, + TooltipRefProps, } from 'components/Tooltip/TooltipTypes' import { useTooltip } from 'components/TooltipProvider' import { TooltipContent } from 'components/TooltipContent' import cssSupports from 'utils/css-supports' import type { ITooltipController } from './TooltipControllerTypes' -const TooltipController = React.forwardRef( +const TooltipController = React.forwardRef( ( { id, diff --git a/src/index.tsx b/src/index.tsx index 1faa1755..c11ef9cf 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -12,7 +12,7 @@ import type { WrapperType, IPosition, Middleware, - TooltipImperativeProps, + TooltipRefProps, } from './components/Tooltip/TooltipTypes' import type { ITooltipController } from './components/TooltipController/TooltipControllerTypes' import type { ITooltipWrapper } from './components/TooltipProvider/TooltipProviderTypes' @@ -48,7 +48,7 @@ export type { ITooltipWrapper, IPosition, Middleware, - TooltipImperativeProps as TooltipRefProps, + TooltipRefProps, } export { removeStyle } from './utils/handle-style' From f0b6e4cd566d09c41640bc7f5e41be45e4df55be Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Mon, 13 Nov 2023 14:49:46 -0300 Subject: [PATCH 18/18] docs: `TooltipRefProps` --- docs/docs/examples/imperative-mode.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docs/examples/imperative-mode.mdx b/docs/docs/examples/imperative-mode.mdx index a6bda6cb..b8fe3553 100644 --- a/docs/docs/examples/imperative-mode.mdx +++ b/docs/docs/examples/imperative-mode.mdx @@ -63,7 +63,7 @@ interface TooltipImperativeCloseOptions { delay?: number } -interface TooltipImperativeProps { +interface TooltipRefProps { open: (options?: TooltipImperativeOpenOptions) => void close: (options?: TooltipImperativeCloseOptions) => void /** @@ -114,10 +114,10 @@ These are read-only. Updating their values has no effect on the tooltip. ```jsx import { useRef } from 'react'; -import { Tooltip, TooltipImperativeProps } from 'react-tooltip'; +import { Tooltip, TooltipRefProps } from 'react-tooltip'; -const tooltipRef1 = useRef(null) -const tooltipRef2 = useRef(null) +const tooltipRef1 = useRef(null) +const tooltipRef2 = useRef(null) ◕‿‿◕