Skip to content

Commit

Permalink
fix(Tooltip): merge style property with internal (#1591)
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker authored Sep 30, 2022
1 parent deff7c0 commit b3e3901
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 36 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
85 changes: 50 additions & 35 deletions packages/dnb-eufemia/src/components/tooltip/TooltipContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type TooltipContainerProps = {
style?: React.CSSProperties
useHover?: boolean
internalId?: string
attributes?: Record<string, unknown>
attributes?: Record<string, unknown> & { style: React.CSSProperties }
}

export default function TooltipContainer(
Expand All @@ -33,34 +33,40 @@ export default function TooltipContainer(
children,
} = props

const [style, setStyle] = React.useState(null)
const [hover, setHover] = React.useState(false)
const isActive = isTrue(active) || hover
const [wasActive, makeActive] = React.useState(false)
const [renewStyles, forceRerender] = React.useState(0)
const [renewStyles, forceRerender] = React.useState(getBodySize)

const elementRef = React.useRef<HTMLSpanElement>(null)
const offset = React.useRef(16)
const debounceTimeout = React.useRef<NodeJS.Timeout>()
const resizeObserver = React.useRef<ResizeObserver>(null)

const isActive = isTrue(active) || hover
function getBodySize() {
if (!isActive || typeof document === 'undefined') {
return 0 // stop here
}

const { width, height } = document.body.getBoundingClientRect()

return width + height
}

React.useEffect(() => {
React.useLayoutEffect(() => {
const addPositionObserver = () => {
if (resizeObserver.current || typeof document === 'undefined') {
return // stop here
}

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)
debounceTimeout.current = setTimeout(
() => forceRerender(getBodySize()),
100
)
})

resizeObserver.current.observe(document.body)
Expand All @@ -85,29 +91,33 @@ export default function TooltipContainer(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive])

const style = React.useMemo(() => {
if (typeof window === 'undefined' || !elementRef.current) {
return {}
React.useLayoutEffect(() => {
const { targetElement: target, align, fixedPosition } = props

if (
typeof window === 'undefined' ||
!elementRef.current ||
!target?.getBoundingClientRect
) {
return // stop here
}

const elementWidth = elementRef.current.offsetWidth
const elementHeight = elementRef.current.offsetHeight

let alignOffset = 0

const { targetElement: target, align, fixedPosition } = props

const rect = target.getBoundingClientRect()

const targetSize = {
const targetBodySize = {
width: target.offsetWidth,
height: target.offsetHeight,
}

// fix for svg
if (!target.offsetHeight) {
targetSize.width = rect.width
targetSize.height = rect.height
targetBodySize.width = rect.width
targetBodySize.height = rect.height
}

const scrollY =
Expand All @@ -117,26 +127,29 @@ export default function TooltipContainer(
const top = (isTrue(fixedPosition) ? 0 : scrollY) + rect.top

// Use Mouse position when target is too wide
const useMouseWhen = targetSize.width > 400
const useMouseWhen = targetBodySize.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
useMouseWhen && mousePos < targetBodySize.width
? mousePos
: widthBased

const style = { ...props.style }

if (align === 'left') {
alignOffset = -targetSize.width / 2
alignOffset = -targetBodySize.width / 2
} else if (align === 'right') {
alignOffset = targetSize.width / 2
alignOffset = targetBodySize.width / 2
}

const topHorizontal = top + targetSize.height / 2 - elementHeight / 2
const topHorizontal =
top + targetBodySize.height / 2 - elementHeight / 2
const leftVertical =
left - elementWidth / 2 + targetSize.width / 2 + alignOffset
left - elementWidth / 2 + targetBodySize.width / 2 + alignOffset

const stylesFromPosition = {
left: () => {
Expand All @@ -145,37 +158,37 @@ export default function TooltipContainer(
},
right: () => {
style.top = topHorizontal
style.left = left + targetSize.width + offset.current
style.left = left + targetBodySize.width + offset.current
},
top: () => {
style.left = leftVertical
style.top = top - elementHeight - offset.current
},
bottom: () => {
style.left = leftVertical
style.top = top + targetSize.height + offset.current
style.top = top + targetBodySize.height + offset.current
},
}

const stylesFromArrow = {
left: () => {
style.left =
left + targetSize.width / 2 - offset.current + alignOffset
left + targetBodySize.width / 2 - offset.current + alignOffset
},
right: () => {
style.left =
left -
elementWidth +
targetSize.width / 2 +
targetBodySize.width / 2 +
offset.current +
alignOffset
},
top: () => {
style.top = top + targetSize.height / 2 - offset.current
style.top = top + targetBodySize.height / 2 - offset.current
},
bottom: () => {
style.top =
top + targetSize.height / 2 - elementHeight + offset.current
top + targetBodySize.height / 2 - elementHeight + offset.current
},
}

Expand All @@ -199,10 +212,12 @@ export default function TooltipContainer(
style.top = 0
}

return style
if (isActive) {
setStyle(style)
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive, hover, children, elementRef, renewStyles])
}, [isActive, hover, arrow, position, children, renewStyles])

const handleMouseEnter = () => {
if (isTrue(active) && useHover !== false) {
Expand All @@ -220,7 +235,6 @@ export default function TooltipContainer(
<span
role="tooltip"
aria-hidden // make sure SR does not find it in the DOM, because we use "aria-describedby" for that
style={style}
ref={elementRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
Expand All @@ -233,6 +247,7 @@ export default function TooltipContainer(
isActive && 'dnb-tooltip--active',
!isActive && wasActive && 'dnb-tooltip--hide'
)}
style={{ ...style, ...attributes.style }}
>
{arrow && (
<span
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe('Tooltip', () => {
})

describe('with targetSelector', () => {
const Tooltip = (props: TooltipProps = {}) => (
const Tooltip = (props: TooltipProps) => (
<>
<button id="button-id">Button</button>
<OriginalTooltip
Expand All @@ -77,6 +77,88 @@ describe('Tooltip', () => {
const Component = render(<Tooltip active />)
expect(await axeComponent(Component)).toHaveNoViolations()
})

it('should merge style prop', () => {
render(
<>
<a className="anchor" href="/">
anchor
</a>

{/**
* The ignore is only temporary
* and will be removed when rebasing with this PR https://github.com/dnbexperience/eufemia/pull/1590
*
* eslint-disable-line @typescript-eslint/ban-ts-comment
* @ts-ignore */}
<Tooltip active style={{ zIndex: 10 }} targetSelector=".anchor">
Tooltip
</Tooltip>
</>
)

expect(
document.querySelector('.dnb-tooltip').getAttribute('style')
).toBe('z-index: 10; left: 0px; top: 0px;')
})

it('should set size class', () => {
render(
<Tooltip active size="large">
Tooltip
</Tooltip>
)

expect(
Array.from(document.querySelector('.dnb-tooltip').classList)
).toEqual(expect.arrayContaining(['dnb-tooltip--large']))
})

it('should set fixed position class', () => {
render(
<Tooltip active fixedPosition>
Tooltip
</Tooltip>
)

expect(
Array.from(document.querySelector('.dnb-tooltip').classList)
).toEqual(expect.arrayContaining(['dnb-tooltip--fixed']))
})

it('should set position class', () => {
render(
<Tooltip active position="right">
Tooltip
</Tooltip>
)

expect(
Array.from(document.querySelector('.dnb-tooltip__arrow').classList)
).toEqual(
expect.arrayContaining([
'dnb-tooltip__arrow__arrow--center',
'dnb-tooltip__arrow__position--right',
])
)
})

it('should set arrow class', () => {
render(
<Tooltip active arrow="right">
Tooltip
</Tooltip>
)

expect(
Array.from(document.querySelector('.dnb-tooltip__arrow').classList)
).toEqual(
expect.arrayContaining([
'dnb-tooltip__arrow__arrow--right',
'dnb-tooltip__arrow__position--top',
])
)
})
})

describe('with targetElement', () => {
Expand Down

0 comments on commit b3e3901

Please sign in to comment.