Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Tooltip): merge style property with internal #1591

Merged
merged 1 commit into from
Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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