Skip to content

Commit a13fc2f

Browse files
committed
refactor(CDropdown, CPopover, CTooltip): improve RTL direction handling
1 parent 7931dd2 commit a13fc2f

File tree

5 files changed

+146
-60
lines changed

5 files changed

+146
-60
lines changed

packages/coreui-react/src/components/dropdown/CDropdownMenu.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Alignments, CDropdownContext } from './CDropdown'
77
import { CConditionalPortal } from '../conditional-portal'
88

99
import type { Placements } from '../../types'
10+
import { isRTL } from '../../utils'
1011

1112
export interface CDropdownMenuProps
1213
extends HTMLAttributes<HTMLDivElement | HTMLUListElement>,
@@ -109,23 +110,23 @@ export const CDropdownMenu: FC<CDropdownMenuProps> = ({
109110
}
110111

111112
if (direction === 'dropup') {
112-
_placement = 'top-start'
113+
_placement = isRTL(dropdownMenuRef.current) ? 'top-end' : 'top-start'
113114
}
114115

115116
if (direction === 'dropup-center') {
116117
_placement = 'top'
117118
}
118119

119120
if (direction === 'dropend') {
120-
_placement = 'right-start'
121+
_placement = isRTL(dropdownMenuRef.current) ? 'left-start' : 'right-start'
121122
}
122123

123124
if (direction === 'dropstart') {
124-
_placement = 'left-start'
125+
_placement = isRTL(dropdownMenuRef.current) ? 'right-start' : 'left-start'
125126
}
126127

127128
if (alignment === 'end') {
128-
_placement = 'bottom-end'
129+
_placement = isRTL(dropdownMenuRef.current) ? 'bottom-start' : 'bottom-end'
129130
}
130131

131132
const dropdownMenuComponent = (style?: React.CSSProperties, ref?: React.Ref<HTMLDivElement>) => (

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

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React, { FC, HTMLAttributes, ReactNode, useRef, useEffect, useState } from 'react'
22
import { createPortal } from 'react-dom'
3-
import PropTypes from 'prop-types'
43
import classNames from 'classnames'
5-
import { usePopper } from 'react-popper'
4+
import PropTypes from 'prop-types'
65
import { Transition } from 'react-transition-group'
6+
import { createPopper, Instance, Placement } from '@popperjs/core'
77

88
import { triggerPropType } from '../../props'
99
import type { Triggers } from '../../types'
10+
import { isRTL } from '../../utils'
1011

1112
export interface CPopoverProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'content'> {
1213
/**
@@ -49,6 +50,21 @@ export interface CPopoverProps extends Omit<HTMLAttributes<HTMLDivElement>, 'tit
4950
visible?: boolean
5051
}
5152

53+
const getPlacement = (placement: string, element: HTMLDivElement | null): Placement => {
54+
console.log(element)
55+
switch (placement) {
56+
case 'right': {
57+
return isRTL(element) ? 'left' : 'right'
58+
}
59+
case 'left': {
60+
return isRTL(element) ? 'right' : 'left'
61+
}
62+
default: {
63+
return placement as Placement
64+
}
65+
}
66+
}
67+
5268
export const CPopover: FC<CPopoverProps> = ({
5369
children,
5470
className,
@@ -62,33 +78,53 @@ export const CPopover: FC<CPopoverProps> = ({
6278
visible,
6379
...rest
6480
}) => {
81+
const popoverRef = useRef(null)
82+
const togglerRef = useRef(null)
83+
const popper = useRef<Instance>()
6584
const [_visible, setVisible] = useState(visible)
66-
const popoverRef = useRef()
67-
68-
const [referenceElement, setReferenceElement] = useState(null)
69-
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
70-
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
71-
const { styles, attributes } = usePopper(referenceElement, popperElement, {
72-
modifiers: [
73-
{ name: 'arrow', options: { element: arrowElement } },
74-
{
75-
name: 'offset',
76-
options: {
77-
offset: offset,
78-
},
79-
},
80-
],
81-
placement: placement,
82-
})
8385

8486
useEffect(() => {
8587
setVisible(visible)
8688
}, [visible])
8789

90+
useEffect(() => {
91+
if (_visible) {
92+
initPopper()
93+
}
94+
95+
return () => {
96+
destroyPopper()
97+
}
98+
}, [_visible])
99+
100+
const initPopper = () => {
101+
if (togglerRef.current && popoverRef.current) {
102+
popper.current = createPopper(togglerRef.current, popoverRef.current, {
103+
modifiers: [
104+
{
105+
name: 'offset',
106+
options: {
107+
offset: offset,
108+
},
109+
},
110+
],
111+
placement: getPlacement(placement, togglerRef.current),
112+
})
113+
}
114+
}
115+
116+
const destroyPopper = () => {
117+
if (popper.current) {
118+
popper.current.destroy()
119+
}
120+
121+
popper.current = undefined
122+
}
123+
88124
return (
89125
<>
90126
{React.cloneElement(children as React.ReactElement<any>, {
91-
ref: setReferenceElement,
127+
ref: togglerRef,
92128
...((trigger === 'click' || trigger.includes('click')) && {
93129
onClick: () => setVisible(!_visible),
94130
}),
@@ -119,20 +155,20 @@ export const CPopover: FC<CPopoverProps> = ({
119155
<div
120156
className={classNames(
121157
'popover',
122-
`bs-popover-${placement.replace('left', 'start').replace('right', 'end')}`,
158+
`bs-popover-${getPlacement(placement, togglerRef.current)
159+
.replace('left', 'start')
160+
.replace('right', 'end')}`,
123161
'fade',
124162
{
125163
show: state === 'entered',
126164
},
127165
className,
128166
)}
129-
ref={setPopperElement}
167+
ref={popoverRef}
130168
role="tooltip"
131-
style={styles.popper}
132-
{...attributes.popper}
133169
{...rest}
134170
>
135-
<div className="popover-arrow" style={styles.arrow} ref={setArrowElement}></div>
171+
<div data-popper-arrow className="popover-arrow"></div>
136172
<div className="popover-header">{title}</div>
137173
<div className="popover-body">{content}</div>
138174
</div>

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

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import React, { FC, HTMLAttributes, ReactNode, useEffect, useRef, useState } from 'react'
1+
import React, { FC, HTMLAttributes, ReactNode, useRef, useEffect, useState } from 'react'
22
import { createPortal } from 'react-dom'
3-
import PropTypes from 'prop-types'
43
import classNames from 'classnames'
5-
import { usePopper } from 'react-popper'
4+
import PropTypes from 'prop-types'
65
import { Transition } from 'react-transition-group'
6+
import { createPopper, Instance, Placement } from '@popperjs/core'
77

88
import { triggerPropType } from '../../props'
99
import type { Triggers } from '../../types'
10+
import { isRTL } from '../../utils'
1011

1112
export interface CTooltipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'content'> {
1213
/**
@@ -18,7 +19,7 @@ export interface CTooltipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'con
1819
*/
1920
content: ReactNode | string
2021
/**
21-
* Offset of the popover relative to its target.
22+
* Offset of the tooltip relative to its target.
2223
*/
2324
offset?: [number, number]
2425
/**
@@ -40,50 +41,85 @@ export interface CTooltipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'con
4041
*/
4142
placement?: 'auto' | 'top' | 'right' | 'bottom' | 'left'
4243
/**
43-
* Toggle the visibility of popover component.
44+
* Toggle the visibility of tooltip component.
4445
*/
4546
visible?: boolean
4647
}
4748

49+
const getPlacement = (placement: string, element: HTMLDivElement | null): Placement => {
50+
console.log(element)
51+
switch (placement) {
52+
case 'right': {
53+
return isRTL(element) ? 'left' : 'right'
54+
}
55+
case 'left': {
56+
return isRTL(element) ? 'right' : 'left'
57+
}
58+
default: {
59+
return placement as Placement
60+
}
61+
}
62+
}
63+
4864
export const CTooltip: FC<CTooltipProps> = ({
4965
children,
5066
className,
5167
content,
52-
offset = [0, 0],
68+
offset = [0, 6],
5369
onHide,
5470
onShow,
5571
placement = 'top',
5672
trigger = 'hover',
5773
visible,
5874
...rest
5975
}) => {
60-
const tooltipRef = useRef()
76+
const tooltipRef = useRef(null)
77+
const togglerRef = useRef(null)
78+
const popper = useRef<Instance>()
6179
const [_visible, setVisible] = useState(visible)
6280

63-
const [referenceElement, setReferenceElement] = useState(null)
64-
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
65-
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
66-
const { styles, attributes } = usePopper(referenceElement, popperElement, {
67-
modifiers: [
68-
{ name: 'arrow', options: { element: arrowElement } },
69-
{
70-
name: 'offset',
71-
options: {
72-
offset: offset,
73-
},
74-
},
75-
],
76-
placement: placement,
77-
})
78-
7981
useEffect(() => {
8082
setVisible(visible)
8183
}, [visible])
8284

85+
useEffect(() => {
86+
if (_visible) {
87+
initPopper()
88+
}
89+
90+
return () => {
91+
destroyPopper()
92+
}
93+
}, [_visible])
94+
95+
const initPopper = () => {
96+
if (togglerRef.current && tooltipRef.current) {
97+
popper.current = createPopper(togglerRef.current, tooltipRef.current, {
98+
modifiers: [
99+
{
100+
name: 'offset',
101+
options: {
102+
offset: offset,
103+
},
104+
},
105+
],
106+
placement: getPlacement(placement, togglerRef.current),
107+
})
108+
}
109+
}
110+
111+
const destroyPopper = () => {
112+
if (popper.current) {
113+
popper.current.destroy()
114+
}
115+
116+
popper.current = undefined
117+
}
118+
83119
return (
84120
<>
85121
{React.cloneElement(children as React.ReactElement<any>, {
86-
ref: setReferenceElement,
122+
ref: togglerRef,
87123
...((trigger === 'click' || trigger.includes('click')) && {
88124
onClick: () => setVisible(!_visible),
89125
}),
@@ -101,7 +137,6 @@ export const CTooltip: FC<CTooltipProps> = ({
101137
<Transition
102138
in={_visible}
103139
mountOnEnter
104-
nodeRef={tooltipRef}
105140
onEnter={onShow}
106141
onExit={onHide}
107142
timeout={{
@@ -114,20 +149,20 @@ export const CTooltip: FC<CTooltipProps> = ({
114149
<div
115150
className={classNames(
116151
'tooltip',
117-
`bs-popover-${placement.replace('left', 'start').replace('right', 'end')}`,
152+
`bs-tooltip-${getPlacement(placement, togglerRef.current)
153+
.replace('left', 'start')
154+
.replace('right', 'end')}`,
118155
'fade',
119156
{
120157
show: state === 'entered',
121158
},
122159
className,
123160
)}
124-
ref={setPopperElement}
161+
ref={tooltipRef}
125162
role="tooltip"
126-
style={styles.popper}
127-
{...attributes.popper}
128163
{...rest}
129164
>
130-
<div className="tooltip-arrow" style={styles.arrow} ref={setArrowElement}></div>
165+
<div data-popper-arrow className="tooltip-arrow"></div>
131166
<div className="tooltip-inner">{content}</div>
132167
</div>
133168
)}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import isInViewport from './isInViewport'
2+
import isRTL from './isRTL'
23

3-
export { isInViewport }
4+
export { isInViewport, isRTL }
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const isRTL = (element?: HTMLElement | HTMLDivElement | null) => {
2+
if (document.documentElement.dir === 'rtl') {
3+
return true
4+
}
5+
6+
if (element) {
7+
return element.closest('[dir="rtl"]') !== null
8+
}
9+
10+
return false
11+
}
12+
13+
export default isRTL

0 commit comments

Comments
 (0)