diff --git a/README.md b/README.md index 038529014..9b808be15 100755 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ import ReactDOMServer from 'react-dom/server'; [gabrieljablonski](https://github.com/gabrieljablonski) Maintainer. -[aronhelser](https://github.com/aronhelser) Passive maintainer - accepting PRs and doing minor testing, but not fixing issues or doing active development. +[aronhelser](https://github.com/aronhelser) (inactive). [alexgurr](https://github.com/alexgurr) (inactive). diff --git a/docs/docs/options.mdx b/docs/docs/options.mdx index 5dd427470..8947fd82b 100644 --- a/docs/docs/options.mdx +++ b/docs/docs/options.mdx @@ -49,27 +49,29 @@ import 'react-tooltip/dist/react-tooltip.css' #### Available props -| name | type | required | default | values | description | -| ---------------- | -------------------- | -------- | ------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| className | string | false | | | class name to customize tooltip element | -| classNameArrow | string | false | | | class name to customize tooltip arrow element | -| content | string | false | | | content to de displayed in tooltip (`content` prop is priorized over `html`) | -| html | string | false | | | html content to de displayed in tooltip | -| place | string | false | `top` | `top` `right` `bottom` `left` | place related to anchor element where the tooltip will be rendered if possible | -| offset | number | false | `10` | any `number` | space between the tooltip element and anchor element (arrow not included in calc) | -| id | string | false | React `useId` | any `string` | option to set a specific id into the tooltip element, the default value is handled by React `useId` introduced on React 18 | -| anchorId | string | false | `undefined` | any `string` | id reference from the element that the tooltip will be positioned around | -| variant | string | false | `dark` | `dark` `light` `success` `warning` `error` `info` | change the colors of tooltip with pre-defined values | -| wrapper | valid element | false | `div` | `ElementType` `div` `span` | element wrapper of tooltip container, can be `div`, `span` or any valid Element | -| children | valid React children | false | `undefined` | valid React children | content can be pass through props, data-attributes or as children, children will be priorized over other options | -| events | array | false | `hover` | `hover` `click` | pre-defined events to handle show or hide tooltip | -| positionStrategy | string | false | `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 | false | | any `number` | tooltip show will be delayed in miliseconds by the amount of value | -| delayHide | number | false | | any `number` | tooltip hide will be delayed in miliseconds by the amount of value | -| noArrow | boolean | false | `false` | `true` `false` | tooltip arrow will not be shown | -| style | CSSProperties | false | | any React inline style | add styles directly to the component by `style` attribute | -| isOpen | boolen | false | 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`) | -| setIsOpen | function | false | | | the tooltip can be controlled or uncontrolled, this attribute can be used to handle show and hide tooltip outside tooltip | +| name | type | required | default | values | description | +| ---------------- | -------------------------- | -------- | ------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| className | string | false | | | class name to customize tooltip element | +| classNameArrow | string | false | | | class name to customize tooltip arrow element | +| content | string | false | | | content to de displayed in tooltip (`content` prop is priorized over `html`) | +| html | string | false | | | html content to de displayed in tooltip | +| place | string | false | `top` | `top` `right` `bottom` `left` | place related to anchor element where the tooltip will be rendered if possible | +| offset | number | false | `10` | any `number` | space between the tooltip element and anchor element (arrow not included in calc) | +| id | string | false | React `useId` | any `string` | option to set a specific id into the tooltip element, the default value is handled by React `useId` introduced on React 18 | +| anchorId | string | false | `undefined` | any `string` | id reference from the element that the tooltip will be positioned around | +| variant | string | false | `dark` | `dark` `light` `success` `warning` `error` `info` | change the colors of tooltip with pre-defined values | +| wrapper | valid element | false | `div` | `ElementType` `div` `span` | element wrapper of tooltip container, can be `div`, `span` or any valid Element | +| children | valid React children | false | `undefined` | valid React children | content can be pass through props, data-attributes or as children, children will be priorized over other options | +| events | array | false | `hover` | `hover` `click` | pre-defined events to handle show or hide tooltip | +| positionStrategy | string | false | `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 | false | | any `number` | tooltip show will be delayed in miliseconds by the amount of value | +| delayHide | number | false | | any `number` | tooltip hide will be delayed in miliseconds by the amount of value | +| float | boolean | false | `false` | `true` `false` | tooltip will follow the mouse position when it moves inside the anchor element (same as V4's `effect="float"`) | +| noArrow | boolean | false | `false` | `true` `false` | tooltip arrow will not be shown | +| style | CSSProperties | false | | any React inline style | add styles directly to the component by `style` attribute | +| position | `{ x: number; y: number }` | false | | any `number` value for both `x` and `y` | override the tooltip position on the viewport | +| isOpen | boolen | false | 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`) | +| setIsOpen | function | false | | | the tooltip can be controlled or uncontrolled, this attribute can be used to handle show and hide tooltip outside tooltip | ### Data attributes @@ -89,18 +91,19 @@ import 'react-tooltip/dist/react-tooltip.css'; #### Available attributes -| name | type | required | default | values | description | -| ------------------------------ | ------ | -------- | ---------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| data-tooltip-content | string | false | | | content to de displayed in tooltip (`content` prop is priorized over `html`) | -| data-tooltip-html | string | false | | | html content to de displayed in tooltip | -| data-tooltip-place | string | false | `top` | `top` `right` `bottom` `left` | place related to anchor element where the tooltip will be rendered if possible | -| data-tooltip-offset | number | false | `10` | any `number` | space between the tooltip element and anchor element (arrow not included in calc) | -| data-tooltip-variant | string | false | `dark` | `dark` `light` `success` `warning` `error` `info` | change the colors of tooltip with pre-defined values | -| data-tooltip-wrapper | string | false | `div` | `div` `span` | element wrapper of tooltip container, can be `div`, `span` or any valid Element | -| data-tooltip-events | string | false | `hover` | `hover click` `hover` `click` | pre-defined events to handle show or hide tooltip | -| data-tooltip-position-strategy | string | false | `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 | -| data-tooltip-show | number | false | | any `number` | tooltip show will be delayed in miliseconds by the amount of value | -| data-tooltip-hide | number | false | | any `number` | tooltip hide will be delayed in miliseconds by the amount of value | +| name | type | required | default | values | description | +| ------------------------------ | ------- | -------- | ---------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| data-tooltip-content | string | false | | | content to de displayed in tooltip (`content` prop is priorized over `html`) | +| data-tooltip-html | string | false | | | html content to de displayed in tooltip | +| data-tooltip-place | string | false | `top` | `top` `right` `bottom` `left` | place related to anchor element where the tooltip will be rendered if possible | +| data-tooltip-offset | number | false | `10` | any `number` | space between the tooltip element and anchor element (arrow not included in calc) | +| data-tooltip-variant | string | false | `dark` | `dark` `light` `success` `warning` `error` `info` | change the colors of tooltip with pre-defined values | +| data-tooltip-wrapper | string | false | `div` | `div` `span` | element wrapper of tooltip container, can be `div`, `span` or any valid Element | +| data-tooltip-events | string | false | `hover` | `hover click` `hover` `click` | pre-defined events to handle show or hide tooltip | +| data-tooltip-position-strategy | string | false | `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 | +| data-tooltip-show | number | false | | any `number` | tooltip show will be delayed in miliseconds by the amount of value | +| data-tooltip-hide | number | false | | any `number` | tooltip hide will be delayed in miliseconds by the amount of value | +| data-tooltip-float | boolean | false | `false` | `true` `false` | tooltip will follow the mouse position when it moves inside the anchor element (same as V4's `effect="float"`) | #### Observations diff --git a/docs/docs/upgrade-guide/changelog-v4-v5.md b/docs/docs/upgrade-guide/changelog-v4-v5.md index b52c7357f..ba82b4fdc 100644 --- a/docs/docs/upgrade-guide/changelog-v4-v5.md +++ b/docs/docs/upgrade-guide/changelog-v4-v5.md @@ -8,74 +8,76 @@ If you are using V4 and want to use V5, please read this doc. ## From V4 to V5 -V4 was a great react tooltip component but was built a few years ago, he was built with react class components and it's hard to maintain and to the community give contributions, so, with this in mind, we build a new version of react tooltip using [float-ui](https://floating-ui.com/) behind the scenes. This gives a great improvement in performance and in a new and easier code to let the community contribute to the project. +V4 was a great react tooltip component but was built a few years ago, he was built with react class components and it's hard to maintain and to the community give contributions, so, with this in mind, we build a new version of react tooltip using [floating-ui](https://floating-ui.com/) behind the scenes. This gives a great improvement in performance and in a new and easier code to let the community contribute to the project. ## Improvements - Dropped package dependency `uuid` - - Using React `useId` - [Docs](https://reactjs.org/docs/hooks-reference.html#useid) -- - - Unfortunately `useId` was introduced only into React v18, so, that will be the minimum necessary version of React to V5 +- - - Unfortunately `useId` was introduced only into React v18, so that is the minimum required React version to use V5 - Dropped package dependency `prop-types` - V5 is written in TypeScript - V5 has minified and unminified files available to be used as you want -## Break Changes +## Breaking Changes -- All data attributes now has `tooltip` into his name +- All data attributes are now prefixed with `data-tooltip-` - Default Padding changed from `padding: 8px 21px;` to `padding: 8px 16px;` - Exported module now is `Tooltip` instead of `ReactTooltip` - - If you already have a `Tooltip` component in your application and want to explicitly declare this is `ReactTooltip`, just `import { Tooltip as ReactTooltip } from "react-tooltip"` -- CSS import is now optional, so, you can modify and/or add any styling to your floating tooltip element +- CSS import is now optional, so you can modify and/or add any styling to your floating tooltip element - `data-tip` attribute now is `data-tooltip-content` -- `getContent` prop was removed. Instead, you can directly pass dynamic content to the `content` tooltip prop, or to `data-tooltip-content` -- default behavior of tooltip now is `solid` instead of `float` +- `getContent` prop was removed. Instead, you can directly pass dynamic content to the `content` tooltip prop, or to `data-tooltip-content` in the anchor element +- Default behavior of tooltip now is equivalent to V4's `solid` effect, instead of `float`. The new `float` prop can be set to achieve V4's `effect="float"`. See [Options](../options.mdx) for more details. ## New Props -- [x] classNameArrow -- [x] events - `data-tooltip-events` -`['hover', 'click']` - default: `['hover']` (always an array when using as prop, even with only one option, when using as data attribute: `data-tooltip-events="hover click"`) -- [x] isOpen - `boolean` (to control tooltip state) - if not used, internal state of tooltip will handle the show state -- [x] setIsOpen - `function` (to control tooltip state) - if not used, internal state of tooltip will handle the show state +- [x] `classNameArrow` +- [x] `events` (or `data-tooltip-events` on anchor element) - `['hover', 'click']` - default: `['hover']` (always an array when using as prop, even with only one option, when using as data attribute: `data-tooltip-events="hover click"`) +- [x] `isOpen` - `boolean` (to control tooltip state) - if not used, tooltip state will be handled internally +- [x] `setIsOpen` - `function` (to control tooltip state) - if not used, tooltip state will be handled internally +- [x] `position` - `{ x: number; y: number }` - similar to V4's `overridePosition` +- [x] `float` - `boolean` - used to achieve V4's `effect="float"` ## `V4` props available in `V5` -- [x] children -- [x] place - `data-tooltip-place` -- [x] type - **Deprecated** | in V5 -> `variant` - `data-tooltip-variant` -- [ ] effect - not implemented yet, if many users need this feature, we will work on this one. -- [x] offset - `data-tooltip-offset` -- [ ] padding - **Deprecated** | in V5 -> can be easy updated by className prop -- [ ] multiline - **Deprecated** | in V5 -> this is already supported as default by `content` and `html` props -- [ ] border - **Deprecated** | in V5 -> can be easy updated by `className` prop -- [ ] borderClass - **Deprecated** | in V5 -> can be easy updated by `className` prop -- [ ] textColor - **Deprecated** | in V5 -> can be easy updated by `className` prop -- [ ] backgroundColor - **Deprecated** | in V5 -> can be easy updated by `className` prop -- [ ] borderColor - **Deprecated** | in V5 -> can be easy updated by `className` prop -- [ ] arrowColor - **Deprecated** | in V5 -> can be easy updated by `className` prop -- [ ] arrowRadius - **Deprecated** | in V5 -> can be easy updated by `className` prop -- [ ] tooltipRadius - **Deprecated** | in V5 -> can be easy updated by `className` prop -- [ ] insecure - **Deprecated** | in V5 -> CSS will be a separate file and can be imported or not -- [x] className -- [x] id -- [x] html -- [x] delayHide - `data-tooltip-delay-hide` -- [ ] delayUpdate - **Deprecated** | if requested, can be implemented later -- [x] delayShow - `data-tooltip-delay-show` -- [ ] event - not implemented yet, if many users need this feature, we will work on this one. -- [ ] eventOff - **Deprecated** -- [ ] isCapture - **Deprecated** -- [ ] globalEventOff - **Deprecated** -- [ ] getContent - use dynamic `content` instead -- [ ] afterShow - not implemented yet, if many users need this feature, we will work on this one. -- [ ] afterHide - not implemented yet, if many users need this feature, we will work on this one. -- [ ] overridePosition - **Deprecated** -- [ ] disable - **Deprecated** | in V5 -> state can be controlled or uncontrolled -- [ ] scrollHide - not implemented yet, if many users need this feature, we will validate if we wrok on this one. -- [ ] resizeHide - not implemented yet, if many users need this feature, we will validate if we wrok on this one. -- [x] wrapper - `data-tooltip-wrapper` -- [ ] bodyMode - **Deprecated** -- [ ] clickable - **Deprecated** | Supported by default in V5 -- [ ] disableInternalStyle - **Deprecated** | in V5 -> CSS will be a separate file and can be imported or not +- [x] `children` +- [x] `place` - also available on anchor element as `data-tooltip-place` +- [ ] `type` - use `variant`. also available on anchor element as `data-tooltip-variant` +- [ ] `effect` - use `float` prop +- [x] `offset` - also available on anchor element as `data-tooltip-offset` +- [ ] `padding` - use `className` and custom CSS +- [ ] `multiline` - supported by default in `content` and `html` props +- [ ] `border` - use `className` and custom CSS +- [ ] `borderClass` - use `className` and custom CSS +- [ ] `textColor` - use `className` and custom CSS +- [ ] `backgroundColor` - use `className` and custom CSS +- [ ] `borderColor` - use `className` and custom CSS +- [ ] `arrowColor` - use `className` and custom CSS +- [ ] `arrowRadius` - use `className` and custom CSS +- [ ] `tooltipRadius` - use `className` and custom CSS +- [ ] `insecure` - CSS will be a separate file and can be imported or not +- [x] `className` +- [x] `id` +- [x] `html` +- [x] `delayHide` - also available on anchor element as `data-delay-hide` +- [ ] `delayUpdate` - if requested, can be implemented later +- [x] `delayShow` - also available on anchor element as `data-delay-show` +- [ ] `event` +- [ ] `eventOff` +- [ ] `isCapture` +- [ ] `globalEventOff` +- [ ] `getContent` - use dynamic `content` +- [ ] `afterShow` - if requested, can be implemented later +- [ ] `afterHide` - if requested, can be implemented later +- [ ] `overridePosition` - use `position` +- [ ] `disable` - state can be controlled or uncontrolled +- [ ] `scrollHide` - if requested, can be implemented later +- [ ] `resizeHide` - if requested, can be implemented later +- [x] `wrapper` - also available on anchor element as `data-tooltip-wrapper` +- [ ] `bodyMode` +- [ ] `clickable` - use controlled state to keep tooltip open +- [ ] `disableInternalStyle` - CSS will be a separate file and can be imported or not ### Detailed informations diff --git a/docs/package.json b/docs/package.json index ff0239210..1de8fe10e 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.2.0" + "react-tooltip": "5.3.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "2.2.0", diff --git a/docs/yarn.lock b/docs/yarn.lock index 15f8f05e5..def29a93a 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -6112,10 +6112,10 @@ react-textarea-autosize@^8.3.2: use-composed-ref "^1.3.0" use-latest "^1.2.1" -react-tooltip@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.2.0.tgz#e10e7de2385e8fe6bf3438739c574558b455de3b" - integrity sha512-EH6XIg2MDbMTEElSAZQVXMVeFoOhTgQuea2or0iwyzsr9v8rJf3ImMhOtq7Xe/BPlougxC+PmOibazodLdaRoA== +react-tooltip@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.3.0.tgz#cba9d40396bf7c15c5d748425f76ec5c3e69b213" + integrity sha512-dE702vnPYYUPDWeFHCMzyCbJ3Ca3c160p1EQvumacTl19a0RjVJ4KHBT4XCJ1FVPLKjI6xJR0+RuSAzWyUkGow== dependencies: "@floating-ui/dom" "^1.0.4" classnames "^2.3.2" diff --git a/package.json b/package.json index e826fbe74..139bdaca0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-tooltip", - "version": "5.2.0", + "version": "5.3.0", "description": "react tooltip component", "scripts": { "dev": "node ./cli.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 810c4451c..0c382aa32 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,14 @@ +/* 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 { TooltipProvider, TooltipWrapper } from 'components/TooltipProvider' +import { IPosition } from 'components/Tooltip/TooltipTypes.d' import { useState } from 'react' import styles from './styles.module.css' function WithProviderMinimal() { return ( -
+

@@ -21,7 +24,7 @@ function WithProviderMinimal() { function WithProviderMultiple() { return ( -

+

@@ -39,6 +42,14 @@ function WithProviderMultiple() { function App() { const [anchorId, setAnchorId] = useState('button') const [isDarkOpen, setIsDarkOpen] = useState(false) + const [position, setPosition] = useState({ x: 0, y: 0 }) + const [toggle, setToggle] = useState(false) + + const handlePositionClick: React.MouseEventHandler = (event) => { + const x = event.clientX + const y = event.clientY + setPosition({ x, y }) + } return (

@@ -112,6 +123,45 @@ function App() { +
+
+
{ + setToggle((t) => !t) + }} + > + Hover me! +
+ +
+
+
{ + handlePositionClick(event) + }} + > + Click me! +
+ +
+
) } diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 995bf3e9d..995d8d2ef 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -5,7 +5,7 @@ import { TooltipContent } from 'components/TooltipContent' import { useTooltip } from 'components/TooltipProvider' import { computeTooltipPosition } from '../../utils/compute-positions' import styles from './styles.module.css' -import type { ITooltip } from './TooltipTypes' +import type { IPosition, ITooltip } from './TooltipTypes' const Tooltip = ({ // props @@ -22,8 +22,10 @@ const Tooltip = ({ children = null, delayShow = 0, delayHide = 0, + float = false, noArrow, style: externalStyles, + position, // props handled by controller isHtmlContent = false, content, @@ -38,6 +40,7 @@ const Tooltip = ({ const [inlineArrowStyles, setInlineArrowStyles] = useState({}) const [show, setShow] = useState(false) const [calculatingPosition, setCalculatingPosition] = useState(false) + const lastFloatPosition = useRef(null) const { anchorRefs, setActiveAnchor: setProviderActiveAnchor } = useTooltip()(id) const [activeAnchor, setActiveAnchor] = useState>({ current: null }) @@ -100,14 +103,71 @@ const Tooltip = ({ } } + const handleTooltipPosition = ({ x, y }: IPosition) => { + const virtualElement = { + getBoundingClientRect() { + return { + x, + y, + width: 0, + height: 0, + top: y, + left: x, + right: x, + bottom: y, + } + }, + } as Element + setCalculatingPosition(true) + computeTooltipPosition({ + place, + offset, + elementReference: virtualElement, + tooltipReference: tooltipRef.current, + tooltipArrowReference: tooltipArrowRef.current, + strategy: positionStrategy, + }).then((computedStylesData) => { + setCalculatingPosition(false) + if (Object.keys(computedStylesData.tooltipStyles).length) { + setInlineStyles(computedStylesData.tooltipStyles) + } + if (Object.keys(computedStylesData.tooltipArrowStyles).length) { + setInlineArrowStyles(computedStylesData.tooltipArrowStyles) + } + }) + } + + const handleMouseMove = (event?: Event) => { + if (!event) { + return + } + const mouseEvent = event as MouseEvent + const mousePosition = { + x: mouseEvent.clientX, + y: mouseEvent.clientY, + } + handleTooltipPosition(mousePosition) + lastFloatPosition.current = mousePosition + } + const handleClickTooltipAnchor = () => { if (setIsOpen) { setIsOpen(!isOpen) - } else if (isOpen === undefined) { - setShow((currentValue) => !currentValue) + } else if (!setIsOpen && isOpen === undefined) { + setShow(true) + if (delayHide) { + handleHideTooltipDelayed() + } } } + const handleClickOutsideAnchor = (event: MouseEvent) => { + if (event.target === activeAnchor.current) { + return + } + setShow(false) + } + // debounce handler to prevent call twice when // mouse enter and focus events being triggered toggether const debouncedHandleShowTooltip = debounce(handleShowTooltip, 50) @@ -125,13 +185,13 @@ const Tooltip = ({ } if (!elementRefs.size) { - // eslint-disable-next-line @typescript-eslint/no-empty-function - return () => {} + return () => null } const enabledEvents: { event: string; listener: (event?: Event) => void }[] = [] if (events.find((event: string) => event === 'click')) { + window.addEventListener('click', handleClickOutsideAnchor) enabledEvents.push({ event: 'click', listener: handleClickTooltipAnchor }) } @@ -142,6 +202,12 @@ const Tooltip = ({ { event: 'focus', listener: debouncedHandleShowTooltip }, { event: 'blur', listener: debouncedHandleHideTooltip }, ) + if (float) { + enabledEvents.push({ + event: 'mousemove', + listener: handleMouseMove, + }) + } } enabledEvents.forEach(({ event, listener }) => { @@ -151,15 +217,37 @@ const Tooltip = ({ }) return () => { + window.removeEventListener('click', handleClickOutsideAnchor) enabledEvents.forEach(({ event, listener }) => { elementRefs.forEach((ref) => { ref.current?.removeEventListener(event, listener) }) }) } - }, [anchorRefs, anchorId, events, delayHide, delayShow]) + }, [anchorRefs, activeAnchor, anchorId, events, delayHide, delayShow]) useEffect(() => { + if (position) { + // if `position` is set, override regular and `float` positioning + handleTooltipPosition(position) + return () => null + } + + if (float) { + if (lastFloatPosition.current) { + /* + Without this, changes to `content`, `place`, `offset`, ..., will only + trigger a position calculation after a `mousemove` event. + + To see why this matters, comment this line, run `yarn dev` and click the + "Hover me!" anchor. + */ + handleTooltipPosition(lastFloatPosition.current) + } + // if `float` is set, override regular positioning + return () => null + } + let elementReference = activeAnchor.current if (anchorId) { // `anchorId` element takes precedence @@ -190,7 +278,7 @@ const Tooltip = ({ return () => { mounted = false } - }, [show, isOpen, anchorId, activeAnchor, content, place, offset, positionStrategy]) + }, [show, isOpen, anchorId, activeAnchor, content, place, offset, positionStrategy, position]) useEffect(() => { return () => { diff --git a/src/components/Tooltip/TooltipTypes.d.ts b/src/components/Tooltip/TooltipTypes.d.ts index f31c7822b..924896ce1 100644 --- a/src/components/Tooltip/TooltipTypes.d.ts +++ b/src/components/Tooltip/TooltipTypes.d.ts @@ -1,4 +1,4 @@ -import type { ElementType, ReactNode, Element, CSSProperties } from 'react' +import type { ElementType, ReactNode, CSSProperties } from 'react' export type PlacesType = 'top' | 'right' | 'bottom' | 'left' @@ -23,6 +23,12 @@ export type DataAttribute = | 'position-strategy' | 'delay-show' | 'delay-hide' + | 'float' + +export interface IPosition { + x: number + y: number +} export interface ITooltip { className?: string @@ -41,8 +47,10 @@ export interface ITooltip { positionStrategy?: PositionStrategy delayShow?: number delayHide?: number + float?: boolean noArrow?: boolean style?: CSSProperties + position?: IPosition isOpen?: boolean setIsOpen?: (value: boolean) => void } diff --git a/src/components/TooltipController/TooltipController.tsx b/src/components/TooltipController/TooltipController.tsx index 8ad0fc678..645499dac 100644 --- a/src/components/TooltipController/TooltipController.tsx +++ b/src/components/TooltipController/TooltipController.tsx @@ -28,8 +28,10 @@ const TooltipController = ({ positionStrategy = 'absolute', delayShow = 0, delayHide = 0, + float = false, noArrow, style, + position, isOpen, setIsOpen, }: ITooltipController) => { @@ -39,6 +41,7 @@ const TooltipController = ({ const [tooltipOffset, setTooltipOffset] = useState(offset) const [tooltipDelayShow, setTooltipDelayShow] = useState(delayShow) const [tooltipDelayHide, setTooltipDelayHide] = useState(delayHide) + const [tooltipFloat, setTooltipFloat] = useState(float) const [tooltipWrapper, setTooltipWrapper] = useState(wrapper) const [tooltipEvents, setTooltipEvents] = useState(events) const [tooltipPositionStrategy, setTooltipPositionStrategy] = useState(positionStrategy) @@ -94,6 +97,9 @@ const TooltipController = ({ 'delay-hide': (value) => { setTooltipDelayHide(value === null ? delayHide : Number(value)) }, + float: (value) => { + setTooltipFloat(value === null ? float : Boolean(value)) + }, } // reset unset data attributes to default values // without this, data attributes from the last active anchor will still be used @@ -121,8 +127,7 @@ const TooltipController = ({ } if (!elementRefs.size) { - // eslint-disable-next-line @typescript-eslint/no-empty-function - return () => {} + return () => null } const observerCallback: MutationCallback = (mutationList) => { @@ -177,8 +182,10 @@ const TooltipController = ({ positionStrategy: tooltipPositionStrategy, delayShow: tooltipDelayShow, delayHide: tooltipDelayHide, + float: tooltipFloat, noArrow, style, + position, isOpen, setIsOpen, } diff --git a/src/components/TooltipController/TooltipControllerTypes.d.ts b/src/components/TooltipController/TooltipControllerTypes.d.ts index 82427fd4e..5901b2054 100644 --- a/src/components/TooltipController/TooltipControllerTypes.d.ts +++ b/src/components/TooltipController/TooltipControllerTypes.d.ts @@ -7,6 +7,7 @@ import type { ChildrenType, EventsType, PositionStrategy, + IPosition, } from 'components/Tooltip/TooltipTypes' export interface ITooltipController { @@ -25,8 +26,10 @@ export interface ITooltipController { positionStrategy?: PositionStrategy delayShow?: number delayHide?: number + float?: boolean noArrow?: boolean style?: CSSProperties + position?: IPosition isOpen?: boolean setIsOpen?: (value: boolean) => void } @@ -43,5 +46,6 @@ declare module 'react' { 'data-tooltip-position-strategy'?: PositionStrategy 'data-tooltip-delay-show'?: number 'data-tooltip-delay-hide'?: number + 'data-tooltip-float'?: boolean } } diff --git a/src/index.tsx b/src/index.tsx index 9fcde48ca..32b260291 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,6 +7,7 @@ import type { PositionStrategy, VariantType, WrapperType, + IPosition, } from './components/Tooltip/TooltipTypes' import type { ITooltipController } from './components/TooltipController/TooltipControllerTypes' import type { ITooltipWrapper } from './components/TooltipProvider/TooltipProviderTypes' @@ -23,4 +24,5 @@ export type { WrapperType, ITooltipController as ITooltip, ITooltipWrapper, + IPosition, } diff --git a/src/styles.module.css b/src/styles.module.css index 8a3b67ae1..e2b323224 100644 --- a/src/styles.module.css +++ b/src/styles.module.css @@ -3,3 +3,12 @@ width: 100%; height: 100vh; } + +.big-anchor { + width: 500px; + height: 300px; + background-color: #d3d3d3; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/src/test/__snapshots__/index.spec.js.snap b/src/test/__snapshots__/index.spec.js.snap index 6b000732c..555b5e87f 100644 --- a/src/test/__snapshots__/index.spec.js.snap +++ b/src/test/__snapshots__/index.spec.js.snap @@ -69,6 +69,27 @@ exports[`tooltip props tooltip component - html 1`] = ` ] `; +exports[`tooltip props tooltip component - position props 1`] = ` +[ + + Lorem Ipsum + , +
+ Hello World! +
+
, +] +`; + exports[`tooltip props tooltip component - without anchorId 1`] = ` [ diff --git a/src/test/index.spec.js b/src/test/index.spec.js index 9a466cfdf..db87b46eb 100644 --- a/src/test/index.spec.js +++ b/src/test/index.spec.js @@ -54,6 +54,14 @@ describe('tooltip props', () => { const tree = component.toJSON() expect(tree).toMatchSnapshot() }) + + test('tooltip component - position props', () => { + const component = renderer.create( + , + ) + const tree = component.toJSON() + expect(tree).toMatchSnapshot() + }) }) describe('compute positions', () => { @@ -109,12 +117,12 @@ describe('compute positions', () => { expect(value).toEqual({ tooltipArrowStyles: { bottom: '-4px', - left: '0px', + left: '5px', right: '', top: '', }, tooltipStyles: { - left: '5px', + left: '-5px', top: '-10px', }, }) diff --git a/src/utils/compute-positions.ts b/src/utils/compute-positions.ts index 34f6fc905..435ad45a9 100644 --- a/src/utils/compute-positions.ts +++ b/src/utils/compute-positions.ts @@ -23,7 +23,7 @@ export const computeTooltipPosition = async ({ const middleware = [offset(Number(offsetValue)), flip(), shift({ padding: 5 })] if (tooltipArrowReference) { - middleware.push(arrow({ element: tooltipArrowReference as HTMLElement })) + middleware.push(arrow({ element: tooltipArrowReference as HTMLElement, padding: 5 })) return computePosition(elementReference as HTMLElement, tooltipReference as HTMLElement, { placement: place, strategy,