From 8d2f0017ca8f7b8f3451af879ecfe870a22a0f07 Mon Sep 17 00:00:00 2001 From: MoveRoad Date: Sun, 25 Jun 2023 17:53:44 +0900 Subject: [PATCH 1/9] feat: add border options (with arrow) --- src/components/Tooltip/Tooltip.tsx | 3 +++ src/components/Tooltip/TooltipTypes.d.ts | 1 + .../TooltipController/TooltipController.tsx | 2 ++ .../TooltipController/TooltipControllerTypes.d.ts | 1 + src/test/utils.spec.js | 1 + src/utils/compute-positions-types.d.ts | 1 + src/utils/compute-positions.ts | 15 +++++++++++++-- 7 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 4b79105f..dd707a77 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -43,6 +43,7 @@ const Tooltip = ({ setIsOpen, activeAnchor, setActiveAnchor, + border, }: ITooltip) => { const tooltipRef = useRef(null) const tooltipArrowRef = useRef(null) @@ -236,6 +237,7 @@ const Tooltip = ({ tooltipArrowReference: tooltipArrowRef.current, strategy: positionStrategy, middlewares, + border, }).then((computedStylesData) => { if (Object.keys(computedStylesData.tooltipStyles).length) { setInlineStyles(computedStylesData.tooltipStyles) @@ -502,6 +504,7 @@ const Tooltip = ({ tooltipArrowReference: tooltipArrowRef.current, strategy: positionStrategy, middlewares, + border, }).then((computedStylesData) => { if (!mounted.current) { // invalidate computed positions after remount diff --git a/src/components/Tooltip/TooltipTypes.d.ts b/src/components/Tooltip/TooltipTypes.d.ts index 5d35edc5..0817bae5 100644 --- a/src/components/Tooltip/TooltipTypes.d.ts +++ b/src/components/Tooltip/TooltipTypes.d.ts @@ -77,4 +77,5 @@ export interface ITooltip { afterHide?: () => void activeAnchor: HTMLElement | null setActiveAnchor: (anchor: HTMLElement | null) => void + border?: string | null } diff --git a/src/components/TooltipController/TooltipController.tsx b/src/components/TooltipController/TooltipController.tsx index 0bdb3e72..086ab3c6 100644 --- a/src/components/TooltipController/TooltipController.tsx +++ b/src/components/TooltipController/TooltipController.tsx @@ -44,6 +44,7 @@ const TooltipController = ({ style, position, isOpen, + border, setIsOpen, afterShow, afterHide, @@ -283,6 +284,7 @@ const TooltipController = ({ style, position, isOpen, + border, setIsOpen, afterShow, afterHide, diff --git a/src/components/TooltipController/TooltipControllerTypes.d.ts b/src/components/TooltipController/TooltipControllerTypes.d.ts index 674225e1..e1659fa6 100644 --- a/src/components/TooltipController/TooltipControllerTypes.d.ts +++ b/src/components/TooltipController/TooltipControllerTypes.d.ts @@ -60,6 +60,7 @@ export interface ITooltipController { style?: CSSProperties position?: IPosition isOpen?: boolean + border?: string | null setIsOpen?: (value: boolean) => void afterShow?: () => void afterHide?: () => void diff --git a/src/test/utils.spec.js b/src/test/utils.spec.js index 8c4cde1a..f8759bff 100644 --- a/src/test/utils.spec.js +++ b/src/test/utils.spec.js @@ -63,6 +63,7 @@ describe('compute positions', () => { top: '', }, tooltipStyles: { + border: null, left: '5px', top: '-10px', }, diff --git a/src/utils/compute-positions-types.d.ts b/src/utils/compute-positions-types.d.ts index baa0596c..afef9d4d 100644 --- a/src/utils/compute-positions-types.d.ts +++ b/src/utils/compute-positions-types.d.ts @@ -8,4 +8,5 @@ export interface IComputePositions { offset?: number strategy?: 'absolute' | 'fixed' middlewares?: Middleware[] + border?: string | null } diff --git a/src/utils/compute-positions.ts b/src/utils/compute-positions.ts index 78c71138..e4e6500c 100644 --- a/src/utils/compute-positions.ts +++ b/src/utils/compute-positions.ts @@ -9,6 +9,7 @@ export const computeTooltipPosition = async ({ offset: offsetValue = 10, strategy = 'absolute', middlewares = [offset(Number(offsetValue)), flip(), shift({ padding: 5 })], + border = null, }: IComputePositions) => { if (!elementReference) { // elementReference can be null or undefined and we will not compute the position @@ -31,7 +32,7 @@ export const computeTooltipPosition = async ({ strategy, middleware, }).then(({ x, y, placement, middlewareData }) => { - const styles = { left: `${x}px`, top: `${y}px` } + const styles = { left: `${x}px`, top: `${y}px`, border } const { x: arrowX, y: arrowY } = middlewareData.arrow ?? { x: 0, y: 0 } @@ -43,12 +44,22 @@ export const computeTooltipPosition = async ({ left: 'right', }[placement.split('-')[0]] ?? 'bottom' + const borderSide = + border && + { + top: { borderBottom: border, borderRight: border }, + right: { borderBottom: border, borderLeft: border }, + bottom: { borderTop: border, borderLeft: border }, + left: { borderTop: border, borderRight: border }, + }[placement.split('-')[0]] + const arrowStyle = { left: arrowX != null ? `${arrowX}px` : '', top: arrowY != null ? `${arrowY}px` : '', right: '', bottom: '', - [staticSide]: '-4px', + ...{ ...borderSide }, + [staticSide]: '-5px', } return { tooltipStyles: styles, tooltipArrowStyles: arrowStyle, place: placement } From bbc59f4b6c947de43ca8ca18e026f4e8c8e07a5e Mon Sep 17 00:00:00 2001 From: MoveRoad Date: Sun, 25 Jun 2023 18:09:10 +0900 Subject: [PATCH 2/9] fix: change position of arrow when border option --- src/utils/compute-positions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/compute-positions.ts b/src/utils/compute-positions.ts index e4e6500c..a7029b1c 100644 --- a/src/utils/compute-positions.ts +++ b/src/utils/compute-positions.ts @@ -59,7 +59,7 @@ export const computeTooltipPosition = async ({ right: '', bottom: '', ...{ ...borderSide }, - [staticSide]: '-5px', + [staticSide]: border ? '-5px' : '-4px', } return { tooltipStyles: styles, tooltipArrowStyles: arrowStyle, place: placement } From fc269cee98bda6c79df1957fd615e6087edc1508 Mon Sep 17 00:00:00 2001 From: MoveRoad Date: Mon, 26 Jun 2023 19:19:30 +0900 Subject: [PATCH 3/9] docs: add options for border prop --- docs/docs/options.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/options.mdx b/docs/docs/options.mdx index c767c4e1..266a9650 100644 --- a/docs/docs/options.mdx +++ b/docs/docs/options.mdx @@ -121,3 +121,4 @@ import { Tooltip } from 'react-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` | `string` | no | | a React border style | Change style of the tooltip border including arrows | From c4be60b813d2ccb1c055722f172ab8d6b0159fbf Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Sun, 2 Jul 2023 13:30:11 -0300 Subject: [PATCH 4/9] docs: improve `border` description --- docs/docs/options.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/options.mdx b/docs/docs/options.mdx index 266a9650..97483226 100644 --- a/docs/docs/options.mdx +++ b/docs/docs/options.mdx @@ -114,11 +114,11 @@ import { Tooltip } from 'react-tooltip'; | `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) | | `closeOnEsc` | `boolean` | no | `false` | `true` `false` | Resizing the window will close the tooltip | -| `style` | `CSSProperties` | no | | a React inline style | Add inline styles directly to the tooltip | +| `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`) | | `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` | `string` | no | | a React border style | Change style of the tooltip border including arrows | +| `border` | `CSSProperties['border']` | no | | a CSS border style | Change the style of the tooltip border (including the arrow) | From 5d4c31139ce0ea75dd1777ba67dccd6153a0039f Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Sun, 2 Jul 2023 13:30:29 -0300 Subject: [PATCH 5/9] feat: `border` typing/validation/compute position --- src/components/Tooltip/TooltipTypes.d.ts | 2 +- .../TooltipController/TooltipController.tsx | 19 ++++++++++++++ .../TooltipControllerTypes.d.ts | 8 +++++- src/utils/compute-positions-types.d.ts | 3 ++- src/utils/compute-positions.ts | 19 +++++++++++--- src/utils/css-attr-is-valid.ts | 25 +++++++++++++++++++ 6 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 src/utils/css-attr-is-valid.ts diff --git a/src/components/Tooltip/TooltipTypes.d.ts b/src/components/Tooltip/TooltipTypes.d.ts index 0817bae5..fa4c7d16 100644 --- a/src/components/Tooltip/TooltipTypes.d.ts +++ b/src/components/Tooltip/TooltipTypes.d.ts @@ -77,5 +77,5 @@ export interface ITooltip { afterHide?: () => void activeAnchor: HTMLElement | null setActiveAnchor: (anchor: HTMLElement | null) => void - border?: string | null + border?: CSSProperties['border'] } diff --git a/src/components/TooltipController/TooltipController.tsx b/src/components/TooltipController/TooltipController.tsx index 086ab3c6..2c07a1f2 100644 --- a/src/components/TooltipController/TooltipController.tsx +++ b/src/components/TooltipController/TooltipController.tsx @@ -12,6 +12,7 @@ import type { } from 'components/Tooltip/TooltipTypes' import { useTooltip } from 'components/TooltipProvider' import { TooltipContent } from 'components/TooltipContent' +import { cssAttrIsValid } from 'utils/css-attr-is-valid' import type { ITooltipController } from './TooltipControllerTypes' const TooltipController = ({ @@ -236,6 +237,24 @@ const TooltipController = ({ } }, [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.') + } + if (border) { + if (!cssAttrIsValid('border', border)) { + // eslint-disable-next-line no-console + console.warn( + `[react-tooltip] "${border}" is not a valid \`border\`. See https://developer.mozilla.org/en-US/docs/Web/CSS/border`, + ) + } + } + }, []) + /** * content priority: children < render or content < html * children should be lower priority so that it can be used as the "default" content diff --git a/src/components/TooltipController/TooltipControllerTypes.d.ts b/src/components/TooltipController/TooltipControllerTypes.d.ts index e1659fa6..088bf7ac 100644 --- a/src/components/TooltipController/TooltipControllerTypes.d.ts +++ b/src/components/TooltipController/TooltipControllerTypes.d.ts @@ -60,7 +60,13 @@ export interface ITooltipController { style?: CSSProperties position?: IPosition isOpen?: boolean - border?: string | null + /** + * @description see https://developer.mozilla.org/en-US/docs/Web/CSS/border. + * + * Adding a border with width > 3px, or with `em/cm/rem/...` instead of `px` + * might break the tooltip arrow positioning. + */ + border?: CSSProperties['border'] setIsOpen?: (value: boolean) => void afterShow?: () => void afterHide?: () => void diff --git a/src/utils/compute-positions-types.d.ts b/src/utils/compute-positions-types.d.ts index afef9d4d..f848bd1b 100644 --- a/src/utils/compute-positions-types.d.ts +++ b/src/utils/compute-positions-types.d.ts @@ -1,3 +1,4 @@ +import { CSSProperties } from 'react' import type { Middleware } from '../components/Tooltip/TooltipTypes' export interface IComputePositions { @@ -8,5 +9,5 @@ export interface IComputePositions { offset?: number strategy?: 'absolute' | 'fixed' middlewares?: Middleware[] - border?: string | null + border?: CSSProperties['border'] } diff --git a/src/utils/compute-positions.ts b/src/utils/compute-positions.ts index a7029b1c..8e0048ae 100644 --- a/src/utils/compute-positions.ts +++ b/src/utils/compute-positions.ts @@ -9,7 +9,7 @@ export const computeTooltipPosition = async ({ offset: offsetValue = 10, strategy = 'absolute', middlewares = [offset(Number(offsetValue)), flip(), shift({ padding: 5 })], - border = null, + border, }: IComputePositions) => { if (!elementReference) { // elementReference can be null or undefined and we will not compute the position @@ -53,13 +53,26 @@ export const computeTooltipPosition = async ({ left: { borderTop: border, borderRight: border }, }[placement.split('-')[0]] + let borderWidth = 0 + if (border) { + const match = `${border}`.match(/(\d+)px/) + if (match?.[1]) { + borderWidth = Number(match[1]) + } else { + /** + * this means `border` was set without `width`, or non-px value + */ + borderWidth = 1 + } + } + const arrowStyle = { left: arrowX != null ? `${arrowX}px` : '', top: arrowY != null ? `${arrowY}px` : '', right: '', bottom: '', - ...{ ...borderSide }, - [staticSide]: border ? '-5px' : '-4px', + ...borderSide, + [staticSide]: `-${4 + borderWidth}px`, } return { tooltipStyles: styles, tooltipArrowStyles: arrowStyle, place: placement } diff --git a/src/utils/css-attr-is-valid.ts b/src/utils/css-attr-is-valid.ts new file mode 100644 index 00000000..0421d406 --- /dev/null +++ b/src/utils/css-attr-is-valid.ts @@ -0,0 +1,25 @@ +export const cssAttrIsValid = (attr: string, value: unknown) => { + const iframe = document.createElement('iframe') + Object.apply(iframe.style, { + display: 'none', + // in case `display: none` not supported + width: '0px', + height: '0px', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any) + document.body.appendChild(iframe) + if (!iframe.contentDocument) { + return true + } + const style = iframe.contentDocument.createElement('style') + style.innerHTML = `.test-css { ${attr}: ${value}; }` + iframe.contentDocument.head.appendChild(style) + const { sheet } = style + if (!sheet) { + return true + } + const result = sheet.cssRules[0].cssText + iframe.remove() + const match = result.match(new RegExp(`${attr}:`)) + return !!match +} From d414ea1ad3a8b9e80fdde841678f32d3ea3ac162 Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Sun, 2 Jul 2023 13:39:27 -0300 Subject: [PATCH 6/9] fix: `border` validation --- .../TooltipController/TooltipController.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/TooltipController/TooltipController.tsx b/src/components/TooltipController/TooltipController.tsx index 2c07a1f2..c23fd114 100644 --- a/src/components/TooltipController/TooltipController.tsx +++ b/src/components/TooltipController/TooltipController.tsx @@ -245,13 +245,11 @@ const TooltipController = ({ // eslint-disable-next-line no-console console.warn('[react-tooltip] Do not set `style.border`. Use `border` prop instead.') } - if (border) { - if (!cssAttrIsValid('border', border)) { - // eslint-disable-next-line no-console - console.warn( - `[react-tooltip] "${border}" is not a valid \`border\`. See https://developer.mozilla.org/en-US/docs/Web/CSS/border`, - ) - } + if (border && !cssAttrIsValid('border', border)) { + // eslint-disable-next-line no-console + console.warn( + `[react-tooltip] "${border}" is not a valid \`border\`. See https://developer.mozilla.org/en-US/docs/Web/CSS/border`, + ) } }, []) From 9336ad0e484504542d07e89c59cfed1846b3cff0 Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Sun, 2 Jul 2023 13:44:00 -0300 Subject: [PATCH 7/9] text: remover `border` from testing --- src/test/utils.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/utils.spec.js b/src/test/utils.spec.js index f8759bff..8c4cde1a 100644 --- a/src/test/utils.spec.js +++ b/src/test/utils.spec.js @@ -63,7 +63,6 @@ describe('compute positions', () => { top: '', }, tooltipStyles: { - border: null, left: '5px', top: '-10px', }, From 60ed92fdcfd6b6746b74d175fea5a80bbf85c17b Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Sun, 2 Jul 2023 16:26:44 -0300 Subject: [PATCH 8/9] feat: use `CSS.supports()` to validate `border` --- .../TooltipController/TooltipController.tsx | 7 ++---- src/utils/css-attr-is-valid.ts | 25 ------------------- 2 files changed, 2 insertions(+), 30 deletions(-) delete mode 100644 src/utils/css-attr-is-valid.ts diff --git a/src/components/TooltipController/TooltipController.tsx b/src/components/TooltipController/TooltipController.tsx index c23fd114..01e2785f 100644 --- a/src/components/TooltipController/TooltipController.tsx +++ b/src/components/TooltipController/TooltipController.tsx @@ -12,7 +12,6 @@ import type { } from 'components/Tooltip/TooltipTypes' import { useTooltip } from 'components/TooltipProvider' import { TooltipContent } from 'components/TooltipContent' -import { cssAttrIsValid } from 'utils/css-attr-is-valid' import type { ITooltipController } from './TooltipControllerTypes' const TooltipController = ({ @@ -245,11 +244,9 @@ const TooltipController = ({ // eslint-disable-next-line no-console console.warn('[react-tooltip] Do not set `style.border`. Use `border` prop instead.') } - if (border && !cssAttrIsValid('border', border)) { + if (border && !CSS.supports('border', `${border}`)) { // eslint-disable-next-line no-console - console.warn( - `[react-tooltip] "${border}" is not a valid \`border\`. See https://developer.mozilla.org/en-US/docs/Web/CSS/border`, - ) + console.warn(`[react-tooltip] "${border}" is not a valid \`border\`.`) } }, []) diff --git a/src/utils/css-attr-is-valid.ts b/src/utils/css-attr-is-valid.ts deleted file mode 100644 index 0421d406..00000000 --- a/src/utils/css-attr-is-valid.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const cssAttrIsValid = (attr: string, value: unknown) => { - const iframe = document.createElement('iframe') - Object.apply(iframe.style, { - display: 'none', - // in case `display: none` not supported - width: '0px', - height: '0px', - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any) - document.body.appendChild(iframe) - if (!iframe.contentDocument) { - return true - } - const style = iframe.contentDocument.createElement('style') - style.innerHTML = `.test-css { ${attr}: ${value}; }` - iframe.contentDocument.head.appendChild(style) - const { sheet } = style - if (!sheet) { - return true - } - const result = sheet.cssRules[0].cssText - iframe.remove() - const match = result.match(new RegExp(`${attr}:`)) - return !!match -} From cd0a869c959893d339e8622e5370205a99321357 Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Sun, 2 Jul 2023 19:28:48 -0300 Subject: [PATCH 9/9] fix: allow `null` for `data-tooltip-content` --- src/components/TooltipController/TooltipControllerTypes.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TooltipController/TooltipControllerTypes.d.ts b/src/components/TooltipController/TooltipControllerTypes.d.ts index 088bf7ac..3e9a161d 100644 --- a/src/components/TooltipController/TooltipControllerTypes.d.ts +++ b/src/components/TooltipController/TooltipControllerTypes.d.ts @@ -76,8 +76,8 @@ declare module 'react' { interface HTMLAttributes extends AriaAttributes, DOMAttributes { 'data-tooltip-id'?: string 'data-tooltip-place'?: PlacesType - 'data-tooltip-content'?: string - 'data-tooltip-html'?: string + 'data-tooltip-content'?: string | null + 'data-tooltip-html'?: string | null 'data-tooltip-variant'?: VariantType 'data-tooltip-offset'?: number 'data-tooltip-wrapper'?: WrapperType