Skip to content

Commit 71fbeae

Browse files
committed
fix(CPopover, CTooltip): prevent setting the wrong component position on the initial transition
1 parent 1888df5 commit 71fbeae

File tree

3 files changed

+116
-136
lines changed

3 files changed

+116
-136
lines changed

packages/coreui-react/src/components/popover/CPopover.tsx

+51-66
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import React, { forwardRef, HTMLAttributes, ReactNode, useRef, useEffect, useState } from 'react'
22
import classNames from 'classnames'
33
import PropTypes from 'prop-types'
4-
import { Transition } from 'react-transition-group'
54

65
import { CConditionalPortal } from '../conditional-portal'
76
import { useForkedRef, usePopper } from '../../hooks'
87
import { fallbackPlacementsPropType, triggerPropType } from '../../props'
98
import type { Placements, Triggers } from '../../types'
10-
import { getRTLPlacement, getTransitionDurationFromElement } from '../../utils'
9+
import { executeAfterTransition, getRTLPlacement } from '../../utils'
1110

1211
export interface CPopoverProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'content'> {
1312
/**
@@ -101,6 +100,7 @@ export const CPopover = forwardRef<HTMLDivElement, CPopoverProps>(
101100
const uID = useRef(`popover${Math.floor(Math.random() * 1_000_000)}`)
102101

103102
const { initPopper, destroyPopper } = usePopper()
103+
const [mounted, setMounted] = useState(false)
104104
const [_visible, setVisible] = useState(visible)
105105

106106
const _delay = typeof delay === 'number' ? { show: delay, hide: delay } : delay
@@ -133,14 +133,39 @@ export const CPopover = forwardRef<HTMLDivElement, CPopoverProps>(
133133
setVisible(visible)
134134
}, [visible])
135135

136-
const toggleVisible = (visible: boolean) => {
137-
if (visible) {
138-
setTimeout(() => setVisible(true), _delay.show)
139-
return
136+
useEffect(() => {
137+
if (_visible) {
138+
setMounted(true)
139+
140+
if (popoverRef.current) {
141+
popoverRef.current.classList.remove('fade', 'show')
142+
destroyPopper()
143+
}
144+
145+
setTimeout(() => {
146+
if (togglerRef.current && popoverRef.current) {
147+
if (animation) {
148+
popoverRef.current.classList.add('fade')
149+
}
150+
151+
initPopper(togglerRef.current, popoverRef.current, popperConfig)
152+
popoverRef.current.classList.add('show')
153+
onShow && onShow()
154+
}
155+
}, _delay.show)
140156
}
141157

142-
setTimeout(() => setVisible(false), _delay.hide)
143-
}
158+
return () => {
159+
if (popoverRef.current) {
160+
popoverRef.current.classList.remove('show')
161+
onHide && onHide()
162+
executeAfterTransition(() => {
163+
destroyPopper()
164+
setMounted(false)
165+
}, popoverRef.current)
166+
}
167+
}
168+
}, [_visible])
144169

145170
return (
146171
<>
@@ -150,71 +175,31 @@ export const CPopover = forwardRef<HTMLDivElement, CPopoverProps>(
150175
}),
151176
ref: togglerRef,
152177
...((trigger === 'click' || trigger.includes('click')) && {
153-
onClick: () => toggleVisible(!_visible),
178+
onClick: () => setVisible(!_visible),
154179
}),
155180
...((trigger === 'focus' || trigger.includes('focus')) && {
156-
onFocus: () => toggleVisible(true),
157-
onBlur: () => toggleVisible(false),
181+
onFocus: () => setVisible(true),
182+
onBlur: () => setVisible(false),
158183
}),
159184
...((trigger === 'hover' || trigger.includes('hover')) && {
160-
onMouseEnter: () => toggleVisible(true),
161-
onMouseLeave: () => toggleVisible(false),
185+
onMouseEnter: () => setVisible(true),
186+
onMouseLeave: () => setVisible(false),
162187
}),
163188
})}
164189
<CConditionalPortal container={container} portal={true}>
165-
<Transition
166-
in={_visible}
167-
mountOnEnter
168-
nodeRef={popoverRef}
169-
onEnter={() => {
170-
if (togglerRef.current && popoverRef.current) {
171-
initPopper(togglerRef.current, popoverRef.current, popperConfig)
172-
}
173-
174-
onShow
175-
}}
176-
onEntering={() => {
177-
if (togglerRef.current && popoverRef.current) {
178-
popoverRef.current.style.display = 'initial'
179-
}
180-
}}
181-
onExit={onHide}
182-
onExited={() => {
183-
destroyPopper()
184-
}}
185-
timeout={{
186-
enter: 0,
187-
exit: popoverRef.current
188-
? getTransitionDurationFromElement(popoverRef.current) + 50
189-
: 200,
190-
}}
191-
unmountOnExit
192-
>
193-
{(state) => (
194-
<div
195-
className={classNames(
196-
'popover',
197-
'bs-popover-auto',
198-
{
199-
fade: animation,
200-
show: state === 'entered',
201-
},
202-
className,
203-
)}
204-
id={uID.current}
205-
ref={forkedRef}
206-
role="tooltip"
207-
style={{
208-
display: 'none',
209-
}}
210-
{...rest}
211-
>
212-
<div className="popover-arrow"></div>
213-
<div className="popover-header">{title}</div>
214-
<div className="popover-body">{content}</div>
215-
</div>
216-
)}
217-
</Transition>
190+
{mounted && (
191+
<div
192+
className={classNames('popover', 'bs-popover-auto', className)}
193+
id={uID.current}
194+
ref={forkedRef}
195+
role="tooltip"
196+
{...rest}
197+
>
198+
<div className="popover-arrow"></div>
199+
<div className="popover-header">{title}</div>
200+
<div className="popover-body">{content}</div>
201+
</div>
202+
)}
218203
</CConditionalPortal>
219204
</>
220205
)

packages/coreui-react/src/components/tooltip/CTooltip.tsx

+50-65
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import React, { forwardRef, HTMLAttributes, ReactNode, useRef, useEffect, useState } from 'react'
22
import classNames from 'classnames'
33
import PropTypes from 'prop-types'
4-
import { Transition } from 'react-transition-group'
54

65
import { CConditionalPortal } from '../conditional-portal'
76
import { useForkedRef, usePopper } from '../../hooks'
87
import { fallbackPlacementsPropType, triggerPropType } from '../../props'
98
import type { Placements, Triggers } from '../../types'
10-
import { getRTLPlacement, getTransitionDurationFromElement } from '../../utils'
9+
import { executeAfterTransition, getRTLPlacement } from '../../utils'
1110

1211
export interface CTooltipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'content'> {
1312
/**
@@ -96,6 +95,7 @@ export const CTooltip = forwardRef<HTMLDivElement, CTooltipProps>(
9695
const uID = useRef(`tooltip${Math.floor(Math.random() * 1_000_000)}`)
9796

9897
const { initPopper, destroyPopper } = usePopper()
98+
const [mounted, setMounted] = useState(false)
9999
const [_visible, setVisible] = useState(visible)
100100

101101
const _delay = typeof delay === 'number' ? { show: delay, hide: delay } : delay
@@ -128,14 +128,39 @@ export const CTooltip = forwardRef<HTMLDivElement, CTooltipProps>(
128128
setVisible(visible)
129129
}, [visible])
130130

131-
const toggleVisible = (visible: boolean) => {
132-
if (visible) {
133-
setTimeout(() => setVisible(true), _delay.show)
134-
return
131+
useEffect(() => {
132+
if (_visible) {
133+
setMounted(true)
134+
135+
if (tooltipRef.current) {
136+
tooltipRef.current.classList.remove('fade', 'show')
137+
destroyPopper()
138+
}
139+
140+
setTimeout(() => {
141+
if (togglerRef.current && tooltipRef.current) {
142+
if (animation) {
143+
tooltipRef.current.classList.add('fade')
144+
}
145+
146+
initPopper(togglerRef.current, tooltipRef.current, popperConfig)
147+
tooltipRef.current.classList.add('show')
148+
onShow && onShow()
149+
}
150+
}, _delay.show)
135151
}
136152

137-
setTimeout(() => setVisible(false), _delay.hide)
138-
}
153+
return () => {
154+
if (tooltipRef.current) {
155+
tooltipRef.current.classList.remove('show')
156+
onHide && onHide()
157+
executeAfterTransition(() => {
158+
destroyPopper()
159+
setMounted(false)
160+
}, tooltipRef.current)
161+
}
162+
}
163+
}, [_visible])
139164

140165
return (
141166
<>
@@ -145,70 +170,30 @@ export const CTooltip = forwardRef<HTMLDivElement, CTooltipProps>(
145170
}),
146171
ref: togglerRef,
147172
...((trigger === 'click' || trigger.includes('click')) && {
148-
onClick: () => toggleVisible(!_visible),
173+
onClick: () => setVisible(!_visible),
149174
}),
150175
...((trigger === 'focus' || trigger.includes('focus')) && {
151-
onFocus: () => toggleVisible(true),
152-
onBlur: () => toggleVisible(false),
176+
onFocus: () => setVisible(true),
177+
onBlur: () => setVisible(false),
153178
}),
154179
...((trigger === 'hover' || trigger.includes('hover')) && {
155-
onMouseEnter: () => toggleVisible(true),
156-
onMouseLeave: () => toggleVisible(false),
180+
onMouseEnter: () => setVisible(true),
181+
onMouseLeave: () => setVisible(false),
157182
}),
158183
})}
159184
<CConditionalPortal container={container} portal={true}>
160-
<Transition
161-
in={_visible}
162-
mountOnEnter
163-
nodeRef={tooltipRef}
164-
onEnter={() => {
165-
if (togglerRef.current && tooltipRef.current) {
166-
initPopper(togglerRef.current, tooltipRef.current, popperConfig)
167-
}
168-
169-
onShow
170-
}}
171-
onEntering={() => {
172-
if (togglerRef.current && tooltipRef.current) {
173-
tooltipRef.current.style.display = 'initial'
174-
}
175-
}}
176-
onExit={onHide}
177-
onExited={() => {
178-
destroyPopper()
179-
}}
180-
timeout={{
181-
enter: 0,
182-
exit: tooltipRef.current
183-
? getTransitionDurationFromElement(tooltipRef.current) + 50
184-
: 200,
185-
}}
186-
unmountOnExit
187-
>
188-
{(state) => (
189-
<div
190-
className={classNames(
191-
'tooltip',
192-
'bs-tooltip-auto',
193-
{
194-
fade: animation,
195-
show: state === 'entered',
196-
},
197-
className,
198-
)}
199-
id={uID.current}
200-
ref={forkedRef}
201-
role="tooltip"
202-
style={{
203-
display: 'none',
204-
}}
205-
{...rest}
206-
>
207-
<div className="tooltip-arrow"></div>
208-
<div className="tooltip-inner">{content}</div>
209-
</div>
210-
)}
211-
</Transition>
185+
{mounted && (
186+
<div
187+
className={classNames('tooltip', 'bs-tooltip-auto', className)}
188+
id={uID.current}
189+
ref={forkedRef}
190+
role="tooltip"
191+
{...rest}
192+
>
193+
<div className="tooltip-arrow"></div>
194+
<div className="tooltip-inner">{content}</div>
195+
</div>
196+
)}
212197
</CConditionalPortal>
213198
</>
214199
)

packages/coreui-react/src/hooks/usePopper.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import { useRef } from 'react'
22
import { createPopper } from '@popperjs/core'
33
import type { Instance, Options } from '@popperjs/core'
44

5-
import { executeAfterTransition } from '../utils'
6-
75
interface UsePopperOutput {
86
popper: Instance | undefined
97
initPopper: (reference: HTMLElement, popper: HTMLElement, options: Partial<Options>) => void
108
destroyPopper: () => void
9+
updatePopper: (options?: Partial<Options>) => void
1110
}
1211

1312
export const usePopper = (): UsePopperOutput => {
@@ -23,17 +22,28 @@ export const usePopper = (): UsePopperOutput => {
2322
const popperInstance = _popper.current
2423

2524
if (popperInstance && el.current) {
26-
executeAfterTransition(() => {
27-
popperInstance.destroy()
28-
}, el.current)
25+
popperInstance.destroy()
2926
}
3027

3128
_popper.current = undefined
3229
}
3330

31+
const updatePopper = (options?: Partial<Options>) => {
32+
const popperInstance = _popper.current
33+
34+
if (popperInstance && options) {
35+
popperInstance.setOptions(options)
36+
}
37+
38+
if (popperInstance) {
39+
popperInstance.update()
40+
}
41+
}
42+
3443
return {
3544
popper: _popper.current,
3645
initPopper,
3746
destroyPopper,
47+
updatePopper,
3848
}
3949
}

0 commit comments

Comments
 (0)