diff --git a/packages/dnb-eufemia/.eslintrc b/packages/dnb-eufemia/.eslintrc
index 662bd51500f..cbf2262049c 100644
--- a/packages/dnb-eufemia/.eslintrc
+++ b/packages/dnb-eufemia/.eslintrc
@@ -132,6 +132,7 @@
"error",
{ "args": "none", "ignoreRestSiblings": true }
],
+ "react/prop-types": "off",
"react/require-default-props": "off"
}
}
diff --git a/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap b/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap
index ab383d9dde4..5da21a736f2 100644
--- a/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap
+++ b/packages/dnb-eufemia/src/components/dialog/__tests__/__snapshots__/Dialog.test.tsx.snap
@@ -484,7 +484,7 @@ exports[`Dialog component snapshot should match component snapshot 1`] = `
group="dialog_id-tooltip"
hide_delay={500}
id="dialog_id-tooltip"
- internal_id="dialog_id-tooltip"
+ internalId="dialog_id-tooltip"
no_animation={false}
position="top"
show_delay={300}
@@ -587,12 +587,11 @@ exports[`Dialog component snapshot should match component snapshot 1`] = `
}
}
className={null}
- clientX={null}
fixed_position={false}
group="dialog_id-tooltip"
hide_delay={500}
id="dialog_id-tooltip"
- internal_id="dialog_id-tooltip"
+ internalId="dialog_id-tooltip"
key="tooltip"
no_animation={false}
position="top"
diff --git a/packages/dnb-eufemia/src/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap b/packages/dnb-eufemia/src/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap
index e45d9d0b50c..8e95dfce37e 100644
--- a/packages/dnb-eufemia/src/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap
+++ b/packages/dnb-eufemia/src/components/drawer/__tests__/__snapshots__/Drawer.test.tsx.snap
@@ -482,7 +482,7 @@ exports[`Drawer component snapshot should match component snapshot 1`] = `
group="drawer_id-tooltip"
hide_delay={500}
id="drawer_id-tooltip"
- internal_id="drawer_id-tooltip"
+ internalId="drawer_id-tooltip"
no_animation={false}
position="top"
show_delay={300}
@@ -585,12 +585,11 @@ exports[`Drawer component snapshot should match component snapshot 1`] = `
}
}
className={null}
- clientX={null}
fixed_position={false}
group="drawer_id-tooltip"
hide_delay={500}
id="drawer_id-tooltip"
- internal_id="drawer_id-tooltip"
+ internalId="drawer_id-tooltip"
key="tooltip"
no_animation={false}
position="top"
diff --git a/packages/dnb-eufemia/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap b/packages/dnb-eufemia/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap
index 55eef6b680b..607b37a4382 100644
--- a/packages/dnb-eufemia/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap
+++ b/packages/dnb-eufemia/src/components/modal/__tests__/__snapshots__/Modal.test.tsx.snap
@@ -473,7 +473,7 @@ exports[`Modal component have to match snapshot 1`] = `
group="modal_id-tooltip"
hide_delay={500}
id="modal_id-tooltip"
- internal_id="modal_id-tooltip"
+ internalId="modal_id-tooltip"
no_animation={false}
position="top"
show_delay={300}
@@ -576,12 +576,11 @@ exports[`Modal component have to match snapshot 1`] = `
}
}
className={null}
- clientX={null}
fixed_position={false}
group="modal_id-tooltip"
hide_delay={500}
id="modal_id-tooltip"
- internal_id="modal_id-tooltip"
+ internalId="modal_id-tooltip"
key="tooltip"
no_animation={false}
position="top"
diff --git a/packages/dnb-eufemia/src/components/tooltip/Tooltip.tsx b/packages/dnb-eufemia/src/components/tooltip/Tooltip.tsx
index 200bbcf93bc..4af046f1c2e 100644
--- a/packages/dnb-eufemia/src/components/tooltip/Tooltip.tsx
+++ b/packages/dnb-eufemia/src/components/tooltip/Tooltip.tsx
@@ -8,140 +8,113 @@ import classnames from 'classnames'
import Context from '../../shared/Context'
import {
makeUniqueId,
- registerElement,
validateDOMAttributes,
- processChildren,
isTrue,
} from '../../shared/component-helper'
import { createSpacingClasses } from '../space/SpacingHelper'
import TooltipContainer from './TooltipContainer'
import TooltipWithEvents from './TooltipWithEvents'
import TooltipPortal from './TooltipPortal'
-import { defaultProps, injectTooltipSemantic } from './TooltipHelpers'
+import {
+ defaultProps,
+ getPropsFromTooltipProp,
+ injectTooltipSemantic,
+} from './TooltipHelpers'
import { ISpacingProps } from '../../shared/interfaces'
import { TooltipProps } from './types'
import { includeValidProps } from '../form-row/FormRowHelpers'
-export { injectTooltipSemantic }
-
-export default class Tooltip extends React.PureComponent<
- TooltipProps & ISpacingProps
-> {
- _id: string
+function Tooltip(localProps: TooltipProps & ISpacingProps) {
+ const context = React.useContext(Context)
- static tagName = 'dnb-tooltip'
- static contextType = Context
+ const inherited = getPropsFromTooltipProp(localProps)
- static enableWebComponent() {
- registerElement(Tooltip?.tagName, Tooltip, defaultProps)
+ // use only the props from context, who are available here anyway
+ const props = {
+ ...defaultProps,
+ ...localProps,
+ ...inherited,
+ ...context.getTranslation(localProps).Tooltip,
+ ...includeValidProps(context.FormRow),
+ ...context.Tooltip,
}
- static getContent(props) {
- return processChildren(props)
+ const {
+ target_element,
+ target_selector,
+ className,
+ id, // eslint-disable-line
+ tooltip, // eslint-disable-line
+ group,
+ size,
+ animate_position, // eslint-disable-line
+ fixed_position, // eslint-disable-line
+ skip_portal,
+ no_animation, // eslint-disable-line
+ show_delay, // eslint-disable-line
+ hide_delay, // eslint-disable-line
+ active, // eslint-disable-line
+ position, // eslint-disable-line
+ arrow, // eslint-disable-line
+ align, // eslint-disable-line
+ ...params
+ } = props
+
+ const [internalId] = React.useState(() => props.id || makeUniqueId()) // cause we need an id anyway
+ props.internalId = internalId
+ props.group = group || localProps.id || 'main-' + props.internalId
+
+ const classes = classnames(
+ 'dnb-tooltip',
+ size === 'large' && 'dnb-tooltip--large',
+ createSpacingClasses(props),
+ className
+ )
+
+ const attributes = {
+ className: classes,
+ ...params,
}
- getPropsFromTooltipProp() {
- return this.props.tooltip
- ? React.isValidElement(this.props.tooltip) &&
- this.props.tooltip.props
- ? this.props.tooltip.props
- : { children: this.props.tooltip }
- : null
- }
+ // also used for code markup simulation
+ validateDOMAttributes(localProps, attributes)
- constructor(props) {
- super(props)
- this._id = props.id || makeUniqueId() // cause we need an id anyway
+ if (!isTrue(props.active)) {
+ delete props.active
}
- render() {
- const inherited = this.getPropsFromTooltipProp()
-
- // use only the props from context, who are available here anyway
- const props = {
- ...defaultProps,
- ...this.props,
- ...inherited,
- ...this.context.getTranslation(this.props).Tooltip,
- ...includeValidProps(this.context.FormRow),
- ...this.context.Tooltip,
- }
-
- const {
- target_element,
- target_selector,
- className,
- id, // eslint-disable-line
- tooltip, // eslint-disable-line
- group,
- size,
- animate_position, // eslint-disable-line
- fixed_position, // eslint-disable-line
- skip_portal,
- no_animation, // eslint-disable-line
- show_delay, // eslint-disable-line
- hide_delay, // eslint-disable-line
- active, // eslint-disable-line
- position, // eslint-disable-line
- arrow, // eslint-disable-line
- align, // eslint-disable-line
- ...params
- } = props
-
- props.internal_id = this._id
- props.group = this.props.id || group || 'main-' + this._id
-
- const content = Tooltip.getContent(props)
-
- const classes = classnames(
- 'dnb-tooltip',
- size === 'large' && 'dnb-tooltip--large',
- createSpacingClasses(props),
- className
- )
-
- const attributes = {
- className: classes,
- ...params,
- }
-
- // also used for code markup simulation
- validateDOMAttributes(this.props, attributes)
-
- if (!isTrue(props.active)) {
- delete props.active
- }
-
- return (
- <>
- {skip_portal ? (
-
+ {skip_portal ? (
+
+ {props.children}
+
+ ) : target_element ? (
+
+ {props.children}
+
+ ) : (
+ target_selector && (
+
- {content}
-
- ) : target_element ? (
-
- {content}
-
- ) : (
- target_selector && (
-
- {content}
-
- )
- )}
- >
- )
- }
+ {props.children}
+
+ )
+ )}
+ >
+ )
}
+
+export { injectTooltipSemantic }
+export default Tooltip
diff --git a/packages/dnb-eufemia/src/components/tooltip/TooltipContainer.tsx b/packages/dnb-eufemia/src/components/tooltip/TooltipContainer.tsx
index d6b5f2a2664..02bdfd5a40f 100644
--- a/packages/dnb-eufemia/src/components/tooltip/TooltipContainer.tsx
+++ b/packages/dnb-eufemia/src/components/tooltip/TooltipContainer.tsx
@@ -11,327 +11,242 @@ import { TooltipProps } from './types'
type TooltipContainerProps = {
targetElement: HTMLElement
- clientX?: number
style?: React.CSSProperties
useHover?: boolean
- internal_id?: string
+ internalId?: string
attributes?: Record
}
-type TooltipContainerState = {
- width: number
- height: number
- hover: boolean
- hide: boolean
- bodyWidth?: number
- bodyHeight?: number
- leaveInDOM?: boolean
-}
-
-export default class TooltipContainer extends React.PureComponent<
- TooltipProps & TooltipContainerProps,
- TooltipContainerState
-> {
- _rootRef = React.createRef()
- offset = 16
- state: TooltipContainerState = {
- hide: null,
- hover: null,
- width: 0,
- height: 0,
- }
+export default function TooltipContainer(
+ props: TooltipProps & TooltipContainerProps
+) {
+ const {
+ internalId,
+ active,
+ attributes,
+ arrow,
+ position,
+ animate_position,
+ fixed_position,
+ no_animation,
+ useHover,
+ children,
+ } = props
+
+ const [hover, setHover] = React.useState(false)
+ const [wasActive, makeActive] = React.useState(false)
+ const [renewStyles, forceRerender] = React.useState(0)
+
+ const elementRef = React.useRef(null)
+ const offset = React.useRef(16)
+ const debounceTimeout = React.useRef()
+ const resizeObserver = React.useRef(null)
+
+ const isActive = isTrue(active) || hover
+
+ React.useEffect(() => {
+ const addPositionObserver = () => {
+ if (resizeObserver.current || typeof document === 'undefined') {
+ return // stop here
+ }
- _ddt: NodeJS.Timeout
- resizeObserver: ResizeObserver
- _style: React.CSSProperties
+ try {
+ resizeObserver.current = new ResizeObserver((entries) => {
+ const run = () => {
+ const { width, height } = entries[0].contentRect
+ forceRerender(width + height)
+ }
+ if (!renewStyles) {
+ run()
+ }
+ clearTimeout(debounceTimeout.current)
+ debounceTimeout.current = setTimeout(run, 100)
+ })
- static getDerivedStateFromProps(
- props: TooltipProps & TooltipContainerProps,
- state: TooltipContainerState
- ) {
- if (state.leaveInDOM && !props.active && !state.hover) {
- state.hide = true
+ resizeObserver.current.observe(document.body)
+ } catch (e) {
+ //
+ }
}
- if (props.active || state.hover) {
- state.leaveInDOM = true
- state.hide = false
+ const removePositionObserver = () => {
+ clearTimeout(debounceTimeout.current)
+ resizeObserver.current?.disconnect()
}
- return state
- }
- componentDidMount() {
- if (isTrue(this.props.active)) {
- this.updateSize()
+ if (isActive) {
+ makeActive(true)
+ addPositionObserver()
+ } else {
+ removePositionObserver()
}
- this.addPositionObserver()
- }
+ return removePositionObserver
- componentDidUpdate(prevProps) {
- if (this.props !== prevProps) {
- this.updateSize()
- }
- }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isActive])
- componentWillUnmount() {
- this.removePositionObserver()
- }
-
- addPositionObserver() {
- if (this.resizeObserver || typeof document === 'undefined') {
- return // stop here
- }
-
- try {
- this.resizeObserver = new ResizeObserver((entries) => {
- // debounce
- clearTimeout(this._ddt)
- this._ddt = setTimeout(() => {
- // force re-render
- this.setState({
- bodyWidth: entries[0].contentRect.width,
- bodyHeight: entries[0].contentRect.height,
- })
- }, 30)
- })
-
- this.resizeObserver.observe(document.body)
- } catch (e) {
- //
+ const style = React.useMemo(() => {
+ if (typeof window === 'undefined' || !elementRef.current) {
+ return {}
}
- }
- removePositionObserver() {
- clearTimeout(this._ddt)
- if (this.resizeObserver) {
- this.resizeObserver.disconnect()
- this.resizeObserver = null
- }
- }
+ const elementWidth = elementRef.current.offsetWidth
+ const elementHeight = elementRef.current.offsetHeight
- getGlobalStyle() {
- return this.makeStyle(this.props.position, this.props.arrow)
- }
-
- makeStyle(position, arrow) {
- if (typeof window === 'undefined') {
- return {}
- }
let alignOffset = 0
- try {
- const {
- targetElement: target,
- align,
- fixed_position,
- clientX,
- } = this.props
+ const { targetElement: target, align, fixed_position } = props
- const rect = target.getBoundingClientRect()
+ const rect = target.getBoundingClientRect()
- const targetSize = {
- width: target.offsetWidth,
- height: target.offsetHeight,
- }
+ const targetSize = {
+ width: target.offsetWidth,
+ height: target.offsetHeight,
+ }
- // fix for svg
- if (!target.offsetHeight) {
- targetSize.width = rect.width
- targetSize.height = rect.height
- }
+ // fix for svg
+ if (!target.offsetHeight) {
+ targetSize.width = rect.width
+ targetSize.height = rect.height
+ }
- const scrollY =
- window.scrollY !== undefined ? window.scrollY : window.pageYOffset
- const scrollX =
- window.scrollX !== undefined ? window.scrollX : window.pageXOffset
- const top = (isTrue(fixed_position) ? 0 : scrollY) + rect.top
-
- // Use Mouse position when target is too wide
- const useMouseWhen = targetSize.width > 400
- const mousePos =
- clientX -
- getOffsetLeft(target) +
- rect.left / 2 +
- (this._rootRef.current ? this._rootRef.current.offsetWidth : 0)
- const widthBased = scrollX + rect.left
- const left =
- useMouseWhen && mousePos < targetSize.width ? mousePos : widthBased
-
- const style = { ...this.props.style }
-
- if (align === 'left') {
- alignOffset = -targetSize.width / 2
- } else if (align === 'right') {
- alignOffset = targetSize.width / 2
- }
+ const scrollY =
+ window.scrollY !== undefined ? window.scrollY : window.pageYOffset
+ const scrollX =
+ window.scrollX !== undefined ? window.scrollX : window.pageXOffset
+ const top = (isTrue(fixed_position) ? 0 : scrollY) + rect.top
+
+ // Use Mouse position when target is too wide
+ const useMouseWhen = targetSize.width > 400
+ const mousePos =
+ getOffsetLeft(target) +
+ rect.left / 2 +
+ (elementRef.current ? elementRef.current.offsetWidth : 0)
+ const widthBased = scrollX + rect.left
+ const left =
+ useMouseWhen && mousePos < targetSize.width ? mousePos : widthBased
+
+ const style = { ...props.style }
+
+ if (align === 'left') {
+ alignOffset = -targetSize.width / 2
+ } else if (align === 'right') {
+ alignOffset = targetSize.width / 2
+ }
- const stylesFromPosition = {
- left: () => {
- style.top = top + targetSize.height / 2 - this.state.height / 2
- style.left = left - this.state.width - this.offset
- },
- right: () => {
- style.top = top + targetSize.height / 2 - this.state.height / 2
- style.left = left + targetSize.width + this.offset
- },
- top: () => {
- style.left =
- left -
- this.state.width / 2 +
- targetSize.width / 2 +
- alignOffset
- style.top = top - this.state.height - this.offset
- },
- bottom: () => {
- style.left =
- left -
- this.state.width / 2 +
- targetSize.width / 2 +
- alignOffset
- style.top = top + targetSize.height + this.offset
- },
- }
+ const topHorizontal = top + targetSize.height / 2 - elementHeight / 2
+ const leftVertical =
+ left - elementWidth / 2 + targetSize.width / 2 + alignOffset
+
+ const stylesFromPosition = {
+ left: () => {
+ style.top = topHorizontal
+ style.left = left - elementWidth - offset.current
+ },
+ right: () => {
+ style.top = topHorizontal
+ style.left = left + targetSize.width + offset.current
+ },
+ top: () => {
+ style.left = leftVertical
+ style.top = top - elementHeight - offset.current
+ },
+ bottom: () => {
+ style.left = leftVertical
+ style.top = top + targetSize.height + offset.current
+ },
+ }
- const stylesFromArrow = {
- left: () => {
- style.left =
- left + targetSize.width / 2 - this.offset + alignOffset
- },
- right: () => {
- style.left =
- left -
- this.state.width +
- targetSize.width / 2 +
- this.offset +
- alignOffset
- },
- top: () => {
- style.top = top + targetSize.height / 2 - this.offset
- },
- bottom: () => {
- style.top =
- top + targetSize.height / 2 - this.state.height + this.offset
- },
- }
+ const stylesFromArrow = {
+ left: () => {
+ style.left =
+ left + targetSize.width / 2 - offset.current + alignOffset
+ },
+ right: () => {
+ style.left =
+ left -
+ elementWidth +
+ targetSize.width / 2 +
+ offset.current +
+ alignOffset
+ },
+ top: () => {
+ style.top = top + targetSize.height / 2 - offset.current
+ },
+ bottom: () => {
+ style.top =
+ top + targetSize.height / 2 - elementHeight + offset.current
+ },
+ }
- if (stylesFromPosition[position]) {
- stylesFromPosition[position]()
- }
- if (stylesFromArrow[arrow]) {
- stylesFromArrow[arrow]()
- }
+ if (stylesFromPosition[position]) {
+ stylesFromPosition[position]()
+ }
+ if (stylesFromArrow[arrow]) {
+ stylesFromArrow[arrow]()
+ }
- return style
- } catch (e) {
- return {}
+ const rightOffset =
+ parseFloat(String(style.left)) + elementWidth - window.innerWidth
+ if (rightOffset > 0) {
+ style.left = window.innerWidth - elementWidth
}
- }
- checkWindowPosition(style: React.CSSProperties) {
if (style.left < 0) {
- style.left = this.offset
- } else {
- try {
- const rightOffset =
- parseFloat(String(style.left)) +
- this.state.width -
- window.innerWidth
- if (rightOffset > 0) {
- style.left = window.innerWidth - this.state.width - this.offset
- }
- } catch (e) {
- //
- }
+ style.left = 0
+ }
+ if (style.top < 0) {
+ style.top = 0
}
return style
- }
-
- updateSize() {
- try {
- // to ensure we do not wrap the content before getting the height
- if (!this.state.height) {
- this._rootRef.current.style.left = ''
- }
- const width = this._rootRef.current.offsetWidth
- const height = this._rootRef.current.offsetHeight
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isActive, hover, children, elementRef, renewStyles])
- if (width !== this.state.width || height !== this.state.height) {
- this.setState({
- width,
- height,
- })
- }
- } catch (e) {
- //
+ const handleMouseEnter = () => {
+ if (isTrue(active) && useHover !== false) {
+ setHover(true)
}
}
- handleMouseEnter = () => {
- if (isTrue(this.props.active) && this.props.useHover !== false) {
- this.setState({ hover: true })
+ const handleMouseLeave = () => {
+ if (useHover !== false) {
+ setHover(false)
}
}
- handleMouseLeave = () => {
- if (this.props.useHover !== false) {
- this.setState({ hover: false })
- }
- }
-
- render() {
- const {
- internal_id,
- active,
- attributes,
- arrow,
- position,
- animate_position,
- fixed_position,
- no_animation,
- children,
- } = this.props
- const { hover, hide } = this.state
-
- const isActive = isTrue(active) || hover
-
- if (isActive) {
- this._style = this.checkWindowPosition(this.getGlobalStyle())
- }
-
- return (
-
- {arrow && (
-
- )}
-
-
- {children}
-
+ return (
+
+ {arrow && (
+
+ )}
+
+
+ {children}
- )
- }
+
+ )
}
diff --git a/packages/dnb-eufemia/src/components/tooltip/TooltipHelpers.tsx b/packages/dnb-eufemia/src/components/tooltip/TooltipHelpers.tsx
index 492ec7277d5..9da7fa340b7 100644
--- a/packages/dnb-eufemia/src/components/tooltip/TooltipHelpers.tsx
+++ b/packages/dnb-eufemia/src/components/tooltip/TooltipHelpers.tsx
@@ -3,6 +3,7 @@
*
*/
+import React from 'react'
import classnames from 'classnames'
export function injectTooltipSemantic(params) {
@@ -36,3 +37,11 @@ export const defaultProps = {
children: null,
tooltip: null,
}
+
+export function getPropsFromTooltipProp(localProps) {
+ return localProps.tooltip
+ ? React.isValidElement(localProps.tooltip) && localProps.tooltip.props
+ ? localProps.tooltip.props
+ : { children: localProps.tooltip }
+ : null
+}
diff --git a/packages/dnb-eufemia/src/components/tooltip/TooltipPortal.tsx b/packages/dnb-eufemia/src/components/tooltip/TooltipPortal.tsx
index 259f25819f5..e95d2d6d46d 100644
--- a/packages/dnb-eufemia/src/components/tooltip/TooltipPortal.tsx
+++ b/packages/dnb-eufemia/src/components/tooltip/TooltipPortal.tsx
@@ -27,70 +27,89 @@ type TooltipPortalProps = {
target: HTMLElement
active: boolean
group?: string
- internal_id?: string
+ internalId?: string
+ children?: React.ReactNode
}
-type TooltipPortalState = {
- isMounted: boolean
- isActive: boolean
-}
-
-export default class TooltipPortal extends React.PureComponent<
- TooltipProps & TooltipPortalProps,
- TooltipPortalState
-> {
- state: TooltipPortalState = { isMounted: false, isActive: null }
+function TooltipPortal(props: TooltipProps & TooltipPortalProps) {
+ const {
+ active,
+ group,
+ target,
+ hide_delay,
+ no_animation,
+ internalId,
+ children,
+ } = props
+
+ const [isMounted, setIsMounted] = React.useState(false)
+
+ React.useLayoutEffect(() => {
+ const getRootElement = () => {
+ if (typeof document !== 'undefined') {
+ try {
+ const elem = document.createElement('div')
+ elem.classList.add('dnb-tooltip__portal')
+ elem.classList.add('dnb-core-style')
+ document.body.appendChild(elem)
- init = () => {
- const { group, active } = this.props
+ return elem
+ } catch (e) {
+ warn(e)
+ }
+ }
+ }
tooltipPortal[group] = tooltipPortal[group] || {
- node: this.useRootElement(),
+ node: getRootElement(),
count: 0,
}
tooltipPortal[group].count++
- this.setState({ isMounted: true, isActive: active }, () => {
- if (!this.isMainGorup()) {
- this.renderPortal()
- }
- })
- }
+ setIsMounted(true)
- componentDidMount() {
- if (document.readyState === 'complete') {
- this.init()
- } else if (typeof window !== 'undefined') {
- window.addEventListener('load', this.init)
+ if (!isMainGorup(group) && active) {
+ renderPortal(true)
}
- }
- componentDidUpdate(prevProps: TooltipProps & TooltipPortalProps) {
- const { group, active, hide_delay } = this.props
+ return () => {
+ if (tooltipPortal[group]) {
+ tooltipPortal[group].count--
+
+ if (!isMainGorup(group)) {
+ clearTimeout(tooltipPortal[group].timeout)
+ ReactDOM.unmountComponentAtNode(tooltipPortal[group].node)
+ }
- if (this.props.children !== prevProps.children) {
- this.renderPortal()
+ if (tooltipPortal[group].count === 0) {
+ try {
+ document.body.removeChild(tooltipPortal[group].node)
+ } catch (e) {
+ //
+ }
+
+ tooltipPortal[group] = null
+ }
+ }
}
+ }, [])
- if (tooltipPortal[group] && active !== prevProps.active) {
+ React.useEffect(() => {
+ if (tooltipPortal?.[group]) {
clearTimeout(tooltipPortal[group].timeout)
- if (active && !prevProps.active) {
- this.setState({ isActive: true }, () => {
- if (!this.isMainGorup()) {
- this.renderPortal()
- }
- })
- } else if (!active && prevProps.active) {
+ if (active) {
+ if (!isMainGorup(group)) {
+ renderPortal(true)
+ }
+ } else if (!active) {
const run = () => {
- this.setState({ isActive: false }, () => {
- if (!this.isMainGorup()) {
- this.renderPortal()
- }
- })
+ if (!isMainGorup(group)) {
+ renderPortal(false)
+ }
}
- if (this.props.no_animation || globalThis.IS_TEST) {
+ if (no_animation || globalThis.IS_TEST) {
run()
} else {
tooltipPortal[group].timeout = setTimeout(
@@ -100,92 +119,24 @@ export default class TooltipPortal extends React.PureComponent<
}
}
}
- }
-
- isMainGorup() {
- const { group } = this.props
- return group.includes('main')
- }
-
- componentWillUnmount() {
- const { group } = this.props
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [children, active, group, hide_delay, no_animation])
- if (tooltipPortal[group]) {
- tooltipPortal[group].count--
+ const renderPortal = (isActive: boolean) => {
+ const targetElement = getTargetElement(target)
- if (!this.isMainGorup()) {
- clearTimeout(tooltipPortal[group].timeout)
- ReactDOM.unmountComponentAtNode(tooltipPortal[group].node)
- }
-
- if (tooltipPortal[group].count === 0) {
- try {
- document.body.removeChild(tooltipPortal[group].node)
- } catch (e) {
- //
- }
-
- tooltipPortal[group] = null
- }
- }
- }
-
- getTargetElement() {
- if (typeof document !== 'undefined') {
- const { target } = this.props
- return typeof target === 'string'
- ? typeof document !== 'undefined' && document.querySelector(target)
- : target
- }
- }
-
- useRootElement() {
- if (typeof document !== 'undefined') {
- try {
- const elem = document.createElement('div')
- elem.classList.add('dnb-tooltip__portal')
- elem.classList.add('dnb-core-style')
- document.body.appendChild(elem)
-
- return elem
- } catch (e) {
- warn(e)
- }
- }
- }
-
- handleAria(elem: HTMLElement) {
- try {
- if (!elem.classList.contains('dnb-tooltip__wrapper')) {
- const existing = {
- 'aria-describedby': elem.getAttribute('aria-describedby'),
- }
- elem.setAttribute(
- 'aria-describedby',
- combineDescribedBy(existing, this.props.internal_id)
- )
- }
- } catch (e) {
- //
- }
- }
-
- renderPortal = () => {
- const targetElement = this.getTargetElement()
- const { group } = this.props
-
- if (!this.isMainGorup() && tooltipPortal[group]) {
+ if (!isMainGorup(group) && tooltipPortal[group]) {
clearTimeout(tooltipPortal[group].timeout)
}
- this.handleAria(targetElement)
+ handleAria(targetElement, internalId)
- if (this.isMainGorup()) {
+ if (isMainGorup(group)) {
return ReactDOM.createPortal(
,
tooltipPortal[group].node
)
@@ -193,19 +144,47 @@ export default class TooltipPortal extends React.PureComponent<
ReactDOM.render(
,
tooltipPortal[group].node
)
}
}
- render() {
- if (this.state.isMounted && this.isMainGorup()) {
- return this.renderPortal()
- }
+ if (isMounted && isMainGorup(group)) {
+ return renderPortal(active)
+ }
+
+ return <>>
+}
+
+export default TooltipPortal
+
+const isMainGorup = (group) => {
+ return group.includes('main')
+}
+
+const getTargetElement = (target) => {
+ if (typeof document !== 'undefined') {
+ return typeof target === 'string'
+ ? typeof document !== 'undefined' && document.querySelector(target)
+ : target
+ }
+}
- return <>>
+const handleAria = (elem: HTMLElement, internalId: string) => {
+ try {
+ if (!elem.classList.contains('dnb-tooltip__wrapper')) {
+ const existing = {
+ 'aria-describedby': elem.getAttribute('aria-describedby'),
+ }
+ elem.setAttribute(
+ 'aria-describedby',
+ combineDescribedBy(existing, internalId)
+ )
+ }
+ } catch (e) {
+ //
}
}
diff --git a/packages/dnb-eufemia/src/components/tooltip/TooltipWithEvents.tsx b/packages/dnb-eufemia/src/components/tooltip/TooltipWithEvents.tsx
index e1bd2ec9d23..9f664ae8fba 100644
--- a/packages/dnb-eufemia/src/components/tooltip/TooltipWithEvents.tsx
+++ b/packages/dnb-eufemia/src/components/tooltip/TooltipWithEvents.tsx
@@ -4,92 +4,72 @@
*/
import React from 'react'
-import {
- combineDescribedBy,
- getInnerRef,
- warn,
-} from '../../shared/component-helper'
+import { combineDescribedBy, warn } from '../../shared/component-helper'
import { injectTooltipSemantic } from './TooltipHelpers'
import TooltipPortal from './TooltipPortal'
import { TooltipProps } from './types'
type TooltipWithEventsProps = {
- target: HTMLElement
+ target: React.ReactElement & React.RefObject
active: boolean
- clientX: number
- internal_id: string
+ internalId: string
}
-type TooltipWithEventsState = {
- isActive: boolean
- isNotSemanticElement: boolean
- _isMounted: boolean
- clientX: number
-}
+function TooltipWithEvents(props: TooltipProps & TooltipWithEventsProps) {
+ const { children, target, ...restProps } = props
-export default class TooltipWithEvents extends React.PureComponent<
- TooltipProps & TooltipWithEventsProps
-> {
- _onEnterTimeout: NodeJS.Timeout
- _ref: HTMLElement
-
- state: TooltipWithEventsState = {
- isActive: false,
- isNotSemanticElement: false,
- _isMounted: false,
- clientX: null,
- }
+ const [isActive, setIsActive] = React.useState(false)
+ const [isNotSemanticElement, setIsNotSemanticElement] =
+ React.useState(false)
+ const [isMounted, setIsMounted] = React.useState(false)
- constructor(props) {
- super(props)
+ const onEnterTimeout = React.useRef()
+ const elementRef = React.useRef()
+ const cloneRef = React.useRef()
- this._ref = Object.prototype.hasOwnProperty.call(
- props.target,
- 'current'
- )
- ? props.target
- : React.createRef()
- }
+ React.useEffect(() => {
+ elementRef.current = getRefElement(cloneRef)
- componentDidMount() {
- this.setState(
- {
- _isMounted: true,
- },
- () => {
- this.addEvents()
- this.handleSemanticElement()
- }
- )
- }
+ // When used internal
+ if (!elementRef.current) {
+ elementRef.current = target.current
+ }
- componentWillUnmount() {
- clearTimeout(this._onEnterTimeout)
-
- const domElement = getInnerRef(this._ref).current
- if (domElement) {
- try {
- domElement.removeEventListener('click', this.onMouseLeave)
- domElement.removeEventListener('focus', this.onFocus)
- domElement.removeEventListener('blur', this.onMouseLeave)
- domElement.removeEventListener('mouseenter', this.onMouseEnter)
- domElement.removeEventListener('mouseleave', this.onMouseLeave)
- domElement.removeEventListener('touchstart', this.onMouseEnter)
- domElement.removeEventListener('touchend', this.onMouseLeave)
- } catch (e) {
- warn(e)
+ if (elementRef.current) {
+ setIsMounted(true)
+ addEvents(elementRef.current)
+ handleSemanticElement()
+ }
+
+ return () => {
+ clearTimeout(onEnterTimeout.current)
+
+ const element = elementRef.current
+ if (element) {
+ try {
+ element.removeEventListener('click', onMouseLeave)
+ element.removeEventListener('focus', onFocus)
+ element.removeEventListener('blur', onMouseLeave)
+ element.removeEventListener('mouseenter', onMouseEnter)
+ element.removeEventListener('mouseleave', onMouseLeave)
+ element.removeEventListener('touchstart', onMouseEnter)
+ element.removeEventListener('touchend', onMouseLeave)
+ } catch (e) {
+ warn(e)
+ }
}
}
- }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
/**
* Make the element focus able by keyboard, if it is not a semantic element
* This will enable keyboard access to the tooltip by adding focus posibility
*/
- handleSemanticElement = () => {
+ const handleSemanticElement = () => {
try {
const targetElement = document.querySelector(
- `*[aria-describedby*="${this.props.internal_id}"]`
+ `*[aria-describedby*="${props.internalId}"]`
)
if (targetElement) {
const role = targetElement.getAttribute('role')
@@ -97,9 +77,7 @@ export default class TooltipWithEvents extends React.PureComponent<
/div|p|span/i.test(targetElement?.tagName) &&
(!role || role === 'text')
) {
- this.setState({
- isNotSemanticElement: true,
- })
+ setIsNotSemanticElement(true)
}
}
} catch (e) {
@@ -107,42 +85,37 @@ export default class TooltipWithEvents extends React.PureComponent<
}
}
- addEvents = () => {
- const domElement = getInnerRef(this._ref).current
+ const addEvents = (element: HTMLElement) => {
try {
- domElement.addEventListener('click', this.onMouseLeave)
- domElement.addEventListener('focus', this.onFocus)
- domElement.addEventListener('blur', this.onMouseLeave)
- domElement.addEventListener('mouseenter', this.onMouseEnter)
- domElement.addEventListener('mouseleave', this.onMouseLeave)
- domElement.addEventListener('touchstart', this.onMouseEnter)
- domElement.addEventListener('touchend', this.onMouseLeave)
+ element.addEventListener('click', onMouseLeave)
+ element.addEventListener('focus', onFocus)
+ element.addEventListener('blur', onMouseLeave)
+ element.addEventListener('mouseenter', onMouseEnter)
+ element.addEventListener('mousedown', onMouseEnter)
+ element.addEventListener('mouseleave', onMouseLeave)
+ element.addEventListener('touchstart', onMouseEnter)
+ element.addEventListener('touchend', onMouseLeave)
} catch (e) {
warn(e)
}
}
- isTouch = (type: string) => {
- return /touch/i.test(type)
- }
-
- onFocus = (e: MouseEvent) => {
+ const onFocus = (e: MouseEvent) => {
try {
if (
document.documentElement.getAttribute('data-whatintent') ===
'keyboard'
) {
- return this.onMouseEnter(e)
+ return onMouseEnter(e)
}
} catch (e) {
warn(e)
}
}
- onMouseEnter = (e: MouseEvent) => {
+ const onMouseEnter = (e: MouseEvent) => {
try {
- const isTouch = this.isTouch(e.type)
- if (isTouch) {
+ if (isTouch(e.type)) {
const elem = e.currentTarget as HTMLElement
elem.style.userSelect = 'none'
}
@@ -151,24 +124,23 @@ export default class TooltipWithEvents extends React.PureComponent<
}
const run = () => {
- this.setState({ isActive: true, clientX: e.clientX })
+ setIsActive(true)
}
- if (this.props.no_animation || globalThis.IS_TEST) {
+ if (props.no_animation || globalThis.IS_TEST) {
run()
} else {
- clearTimeout(this._onEnterTimeout)
- this._onEnterTimeout = setTimeout(
+ clearTimeout(onEnterTimeout.current)
+ onEnterTimeout.current = setTimeout(
run,
- parseFloat(String(this.props.show_delay)) || 1
+ parseFloat(String(props.show_delay)) || 1
) // have min 1 to make sure we are after onMouseLeave
}
}
- onMouseLeave = (e: MouseEvent) => {
+ const onMouseLeave = (e: MouseEvent) => {
try {
- const isTouch = this.isTouch(e.type)
- if (isTouch) {
+ if (isTouch(e.type)) {
const elem = e.currentTarget as HTMLElement
elem.style.userSelect = ''
}
@@ -176,51 +148,68 @@ export default class TooltipWithEvents extends React.PureComponent<
warn(e)
}
- clearTimeout(this._onEnterTimeout)
- this.setState({ isActive: false })
+ clearTimeout(onEnterTimeout.current)
+ setIsActive(false)
}
- render() {
- const {
- children,
- target,
- // internal_id,// NB: Do not remove internal_id from props!
- ...props
- } = this.props
-
- let componentWrapper = null
-
+ const componentWrapper = React.useMemo(() => {
// we could also check against && target.props && !target.props.tooltip
if (React.isValidElement(target)) {
- const params = this.state.isNotSemanticElement
+ const params = isNotSemanticElement
? injectTooltipSemantic({ className: props.className })
: {}
- componentWrapper = React.cloneElement(target, {
- ref: this._ref,
+ return React.cloneElement(target, {
+ ref: cloneRef,
...params,
'aria-describedby': combineDescribedBy(
target.props,
- this.props.internal_id
+ props.internalId
),
})
}
- return (
- <>
- {componentWrapper}
- {this.state._isMounted && (
-
- {children}
-
- )}
- >
- )
+ return null
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [target])
+
+ return (
+ <>
+ {componentWrapper}
+ {isMounted && (
+
+ {children}
+
+ )}
+ >
+ )
+}
+
+export default TooltipWithEvents
+
+const isTouch = (type: string) => {
+ return /touch/i.test(type)
+}
+
+function getRefElement(target: React.RefObject) {
+ const unknownTarget = target as unknown as React.RefObject<{
+ _ref: React.RefObject
+ }>
+ let element = target as HTMLElement | React.RefObject
+
+ // "_ref" is set inside e.g. the Button component (among many others)
+ if (unknownTarget?.current?._ref) {
+ element = getRefElement(unknownTarget.current._ref)
}
+
+ if (Object.prototype.hasOwnProperty.call(element, 'current')) {
+ element = (element as React.RefObject).current
+ }
+
+ return element as HTMLElement
}
diff --git a/packages/dnb-eufemia/src/components/tooltip/types.ts b/packages/dnb-eufemia/src/components/tooltip/types.ts
index 22a335a96af..263e82f9413 100644
--- a/packages/dnb-eufemia/src/components/tooltip/types.ts
+++ b/packages/dnb-eufemia/src/components/tooltip/types.ts
@@ -1,11 +1,24 @@
+export type TooltipPosition = 'top' | 'right' | 'bottom' | 'left'
+
+export type TooltipArrow =
+ | null
+ | 'center'
+ | 'top'
+ | 'right'
+ | 'bottom'
+ | 'left'
+export type TooltipAlign = null | 'center' | 'right' | 'left'
+
+export type TooltipSize = 'basis' | 'large'
+
export type TooltipProps = {
id?: string
group?: string
- size?: 'basis' | 'large'
+ size?: TooltipSize
active?: boolean
- position?: 'top' | 'right' | 'bottom' | 'left'
- arrow?: null | 'center' | 'top' | 'right' | 'bottom' | 'left'
- align?: null | 'center' | 'right' | 'left'
+ position?: TooltipPosition
+ arrow?: TooltipArrow
+ align?: TooltipAlign
animate_position?: boolean
fixed_position?: boolean
skip_portal?: boolean
diff --git a/packages/dnb-eufemia/src/shared/Context.tsx b/packages/dnb-eufemia/src/shared/Context.tsx
index 2fcd250f655..b4b138d6094 100644
--- a/packages/dnb-eufemia/src/shared/Context.tsx
+++ b/packages/dnb-eufemia/src/shared/Context.tsx
@@ -24,6 +24,7 @@ import type { TimelineItemProps } from '../components/timeline/TimelineItem'
import type { VisuallyHiddenProps } from '../components/visually-hidden/VisuallyHidden'
import type { DrawerProps } from '../components/drawer/types'
import type { DialogProps } from '../components/dialog/types'
+import type { TooltipProps } from '../components/tooltip/types'
// All TypeScript based Eufemia elements
import type { AnchorProps } from '../elements/Anchor'
@@ -47,6 +48,7 @@ export type ContextProps = {
VisuallyHidden?: Partial
Drawer?: Partial
Dialog?: Partial
+ Tooltip?: Partial
// -- TODO: Not converted yet --
diff --git a/packages/dnb-eufemia/src/shared/component-helper.js b/packages/dnb-eufemia/src/shared/component-helper.js
index a7ae0dd6f67..a790aae5cb5 100644
--- a/packages/dnb-eufemia/src/shared/component-helper.js
+++ b/packages/dnb-eufemia/src/shared/component-helper.js
@@ -931,24 +931,6 @@ export function getStatusState(status) {
)
}
-export function getInnerRef(ref) {
- let ret = ref
-
- if (
- ref &&
- ref.current &&
- !React.isValidElement(ref.current) &&
- ref.current._ref
- ) {
- const tmp = getInnerRef(ref.current._ref)
- if (tmp && Object.prototype.hasOwnProperty.call(tmp, 'current')) {
- ret = tmp
- }
- }
-
- return ret
-}
-
export function combineLabelledBy(...params) {
return combineAriaBy('aria-labelledby', params)
}
diff --git a/packages/dnb-eufemia/src/shared/interfaces.tsx b/packages/dnb-eufemia/src/shared/interfaces.tsx
index afd1d239657..3a377f325bb 100644
--- a/packages/dnb-eufemia/src/shared/interfaces.tsx
+++ b/packages/dnb-eufemia/src/shared/interfaces.tsx
@@ -1,8 +1,8 @@
-export interface ISpacingProps extends ISpacingElementProps {
+export type ISpacingProps = ISpacingElementProps & {
space?: SpaceTypes | ISpacingElementProps
}
-export interface ISpacingElementProps {
+export type ISpacingElementProps = {
top?: SpaceTypes
right?: SpaceTypes
bottom?: SpaceTypes