@coreui/react 4.9.2 → 4.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,20 @@
1
- import React, { FC, HTMLAttributes, ReactNode, useRef, useEffect, useState } from 'react'
1
+ import React, {
2
+ forwardRef,
3
+ HTMLAttributes,
4
+ ReactNode,
5
+ useRef,
6
+ useEffect,
7
+ useState,
8
+ } from 'react'
2
9
  import { createPortal } from 'react-dom'
3
10
  import classNames from 'classnames'
4
11
  import PropTypes from 'prop-types'
5
12
  import { Transition } from 'react-transition-group'
6
13
 
7
- import { usePopper } from '../../hooks'
14
+ import { useForkedRef, usePopper } from '../../hooks'
8
15
  import { fallbackPlacementsPropType, triggerPropType } from '../../props'
9
16
  import type { Placements, Triggers } from '../../types'
10
- import { getRTLPlacement } from '../../utils'
17
+ import { getRTLPlacement, getTransitionDurationFromElement } from '../../utils'
11
18
 
12
19
  export interface CTooltipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'content'> {
13
20
  /**
@@ -64,129 +71,139 @@ export interface CTooltipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'con
64
71
  visible?: boolean
65
72
  }
66
73
 
67
- export const CTooltip: FC<CTooltipProps> = ({
68
- children,
69
- animation = true,
70
- className,
71
- content,
72
- delay = 0,
73
- fallbackPlacements = ['top', 'right', 'bottom', 'left'],
74
- offset = [0, 6],
75
- onHide,
76
- onShow,
77
- placement = 'top',
78
- trigger = ['hover', 'focus'],
79
- visible,
80
- ...rest
81
- }) => {
82
- const tooltipRef = useRef(null)
83
- const togglerRef = useRef(null)
84
- const { initPopper, destroyPopper } = usePopper()
85
- const [_visible, setVisible] = useState(visible)
74
+ export const CTooltip = forwardRef<HTMLDivElement, CTooltipProps>(
75
+ (
76
+ {
77
+ children,
78
+ animation = true,
79
+ className,
80
+ content,
81
+ delay = 0,
82
+ fallbackPlacements = ['top', 'right', 'bottom', 'left'],
83
+ offset = [0, 6],
84
+ onHide,
85
+ onShow,
86
+ placement = 'top',
87
+ trigger = ['hover', 'focus'],
88
+ visible,
89
+ ...rest
90
+ },
91
+ ref,
92
+ ) => {
93
+ const tooltipRef = useRef(null)
94
+ const togglerRef = useRef(null)
95
+ const forkedRef = useForkedRef(ref, tooltipRef)
86
96
 
87
- const _delay = typeof delay === 'number' ? { show: delay, hide: delay } : delay
97
+ const { initPopper, destroyPopper } = usePopper()
98
+ const [_visible, setVisible] = useState(visible)
88
99
 
89
- const popperConfig = {
90
- modifiers: [
91
- {
92
- name: 'arrow',
93
- options: {
94
- element: '.tooltip-arrow',
100
+ const _delay = typeof delay === 'number' ? { show: delay, hide: delay } : delay
101
+
102
+ const popperConfig = {
103
+ modifiers: [
104
+ {
105
+ name: 'arrow',
106
+ options: {
107
+ element: '.tooltip-arrow',
108
+ },
95
109
  },
96
- },
97
- {
98
- name: 'flip',
99
- options: {
100
- fallbackPlacements: fallbackPlacements,
110
+ {
111
+ name: 'flip',
112
+ options: {
113
+ fallbackPlacements: fallbackPlacements,
114
+ },
101
115
  },
102
- },
103
- {
104
- name: 'offset',
105
- options: {
106
- offset: offset,
116
+ {
117
+ name: 'offset',
118
+ options: {
119
+ offset: offset,
120
+ },
107
121
  },
108
- },
109
- ],
110
- placement: getRTLPlacement(placement, togglerRef.current),
111
- }
122
+ ],
123
+ placement: getRTLPlacement(placement, togglerRef.current),
124
+ }
112
125
 
113
- useEffect(() => {
114
- setVisible(visible)
115
- }, [visible])
126
+ useEffect(() => {
127
+ setVisible(visible)
128
+ }, [visible])
116
129
 
117
- useEffect(() => {
118
- if (_visible && togglerRef.current && tooltipRef.current) {
119
- initPopper(togglerRef.current, tooltipRef.current, popperConfig)
120
- }
130
+ useEffect(() => {
131
+ if (_visible && togglerRef.current && tooltipRef.current) {
132
+ initPopper(togglerRef.current, tooltipRef.current, popperConfig)
133
+ }
121
134
 
122
- return () => {
123
- destroyPopper()
124
- }
125
- }, [_visible])
135
+ return () => {
136
+ destroyPopper()
137
+ }
138
+ }, [_visible])
126
139
 
127
- const toggleVisible = (visible: boolean) => {
128
- if (visible) {
129
- setTimeout(() => setVisible(true), _delay.show)
130
- return
131
- }
140
+ const toggleVisible = (visible: boolean) => {
141
+ if (visible) {
142
+ setTimeout(() => setVisible(true), _delay.show)
143
+ return
144
+ }
132
145
 
133
- setTimeout(() => setVisible(false), _delay.hide)
134
- }
146
+ setTimeout(() => setVisible(false), _delay.hide)
147
+ }
135
148
 
136
- return (
137
- <>
138
- {React.cloneElement(children as React.ReactElement<any>, {
139
- ref: togglerRef,
140
- ...((trigger === 'click' || trigger.includes('click')) && {
141
- onClick: () => toggleVisible(!_visible),
142
- }),
143
- ...((trigger === 'focus' || trigger.includes('focus')) && {
144
- onFocus: () => toggleVisible(true),
145
- onBlur: () => toggleVisible(false),
146
- }),
147
- ...((trigger === 'hover' || trigger.includes('hover')) && {
148
- onMouseEnter: () => toggleVisible(true),
149
- onMouseLeave: () => toggleVisible(false),
150
- }),
151
- })}
152
- {typeof window !== 'undefined' &&
153
- createPortal(
154
- <Transition
155
- in={_visible}
156
- mountOnEnter
157
- onEnter={onShow}
158
- onExit={onHide}
159
- timeout={{
160
- enter: 0,
161
- exit: 200,
162
- }}
163
- unmountOnExit
164
- >
165
- {(state) => (
166
- <div
167
- className={classNames(
168
- 'tooltip',
169
- 'bs-tooltip-auto',
170
- {
171
- fade: animation,
172
- show: state === 'entered',
173
- },
174
- className,
175
- )}
176
- ref={tooltipRef}
177
- role="tooltip"
178
- {...rest}
179
- >
180
- <div className="tooltip-arrow"></div>
181
- <div className="tooltip-inner">{content}</div>
182
- </div>
183
- )}
184
- </Transition>,
185
- document.body,
186
- )}
187
- </>
188
- )
189
- }
149
+ return (
150
+ <>
151
+ {React.cloneElement(children as React.ReactElement<any>, {
152
+ ref: togglerRef,
153
+ ...((trigger === 'click' || trigger.includes('click')) && {
154
+ onClick: () => toggleVisible(!_visible),
155
+ }),
156
+ ...((trigger === 'focus' || trigger.includes('focus')) && {
157
+ onFocus: () => toggleVisible(true),
158
+ onBlur: () => toggleVisible(false),
159
+ }),
160
+ ...((trigger === 'hover' || trigger.includes('hover')) && {
161
+ onMouseEnter: () => toggleVisible(true),
162
+ onMouseLeave: () => toggleVisible(false),
163
+ }),
164
+ })}
165
+ {typeof window !== 'undefined' &&
166
+ createPortal(
167
+ <Transition
168
+ in={_visible}
169
+ mountOnEnter
170
+ nodeRef={tooltipRef}
171
+ onEnter={onShow}
172
+ onExit={onHide}
173
+ timeout={{
174
+ enter: 0,
175
+ exit: tooltipRef.current
176
+ ? getTransitionDurationFromElement(tooltipRef.current) + 50
177
+ : 200,
178
+ }}
179
+ unmountOnExit
180
+ >
181
+ {(state) => (
182
+ <div
183
+ className={classNames(
184
+ 'tooltip',
185
+ 'bs-tooltip-auto',
186
+ {
187
+ fade: animation,
188
+ show: state === 'entered',
189
+ },
190
+ className,
191
+ )}
192
+ ref={forkedRef}
193
+ role="tooltip"
194
+ {...rest}
195
+ >
196
+ <div className="tooltip-arrow"></div>
197
+ <div className="tooltip-inner">{content}</div>
198
+ </div>
199
+ )}
200
+ </Transition>,
201
+ document.body,
202
+ )}
203
+ </>
204
+ )
205
+ },
206
+ )
190
207
 
191
208
  CTooltip.propTypes = {
192
209
  animation: PropTypes.bool,
@@ -2,6 +2,8 @@ import { useRef } from 'react'
2
2
  import { createPopper } from '@popperjs/core'
3
3
  import type { Instance, Options } from '@popperjs/core'
4
4
 
5
+ import { executeAfterTransition } from '../utils'
6
+
5
7
  interface UsePopperOutput {
6
8
  popper: Instance | undefined
7
9
  initPopper: (reference: HTMLElement, popper: HTMLElement, options: Partial<Options>) => void
@@ -10,14 +12,20 @@ interface UsePopperOutput {
10
12
 
11
13
  export const usePopper = (): UsePopperOutput => {
12
14
  const _popper = useRef<Instance>()
15
+ const el = useRef<HTMLElement>()
13
16
 
14
17
  const initPopper = (reference: HTMLElement, popper: HTMLElement, options: Partial<Options>) => {
15
18
  _popper.current = createPopper(reference, popper, options)
19
+ el.current = popper
16
20
  }
17
21
 
18
22
  const destroyPopper = () => {
19
- if (_popper.current) {
20
- _popper.current.destroy()
23
+ const popperInstance = _popper.current
24
+
25
+ if (popperInstance && el.current) {
26
+ executeAfterTransition(() => {
27
+ popperInstance.destroy()
28
+ }, el.current)
21
29
  }
22
30
 
23
31
  _popper.current = undefined
@@ -0,0 +1,46 @@
1
+ import getTransitionDurationFromElement from './getTransitionDurationFromElement'
2
+
3
+ const execute = (callback: () => void) => {
4
+ if (typeof callback === 'function') {
5
+ callback()
6
+ }
7
+ }
8
+
9
+ const triggerTransitionEnd = (element: HTMLElement) => {
10
+ element.dispatchEvent(new Event('transitionend'))
11
+ }
12
+
13
+ const executeAfterTransition = (
14
+ callback: () => void,
15
+ transitionElement: HTMLElement,
16
+ waitForTransition = true,
17
+ ) => {
18
+ if (!waitForTransition) {
19
+ execute(callback)
20
+ return
21
+ }
22
+
23
+ const durationPadding = 5
24
+ const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding
25
+
26
+ let called = false
27
+
28
+ const handler = ({ target }: { target: any }) => {
29
+ if (target !== transitionElement) {
30
+ return
31
+ }
32
+
33
+ called = true
34
+ transitionElement.removeEventListener('transitionend', handler)
35
+ execute(callback)
36
+ }
37
+
38
+ transitionElement.addEventListener('transitionend', handler)
39
+ setTimeout(() => {
40
+ if (!called) {
41
+ triggerTransitionEnd(transitionElement)
42
+ }
43
+ }, emulatedDuration)
44
+ }
45
+
46
+ export default executeAfterTransition
@@ -0,0 +1,24 @@
1
+ const getTransitionDurationFromElement = (element: HTMLElement) => {
2
+ if (!element) {
3
+ return 0
4
+ }
5
+
6
+ // Get transition-duration of the element
7
+ let { transitionDuration, transitionDelay } = window.getComputedStyle(element)
8
+
9
+ const floatTransitionDuration = Number.parseFloat(transitionDuration)
10
+ const floatTransitionDelay = Number.parseFloat(transitionDelay)
11
+
12
+ // Return 0 if element or transition duration is not found
13
+ if (!floatTransitionDuration && !floatTransitionDelay) {
14
+ return 0
15
+ }
16
+
17
+ // If multiple durations are defined, take the first
18
+ transitionDuration = transitionDuration.split(',')[0]
19
+ transitionDelay = transitionDelay.split(',')[0]
20
+
21
+ return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * 1000
22
+ }
23
+
24
+ export default getTransitionDurationFromElement
@@ -1,5 +1,13 @@
1
+ import executeAfterTransition from './executeAfterTransition'
1
2
  import getRTLPlacement from './getRTLPlacement'
3
+ import getTransitionDurationFromElement from './getTransitionDurationFromElement'
2
4
  import isInViewport from './isInViewport'
3
5
  import isRTL from './isRTL'
4
6
 
5
- export { getRTLPlacement, isInViewport, isRTL }
7
+ export {
8
+ executeAfterTransition,
9
+ getRTLPlacement,
10
+ getTransitionDurationFromElement,
11
+ isInViewport,
12
+ isRTL,
13
+ }