Skip to content

Commit a2881c7

Browse files
committed
Add Tooltip component
Add tooltip component Add basic portal component for tooltip Add tooltip component in navigator buttons Add tooltip for button components Add translation for button tooltip texts Add tooltip delay Show tooltip arrow on edges at correct location Fix tooltip arrow center position issue
1 parent 974420a commit a2881c7

13 files changed

+312
-47
lines changed

src/components/atoms/BottomBarButton.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
import React, { MouseEventHandler, FC } from 'react'
22
import styled from '../../lib/styled'
33
import { flexCenter, borderLeft } from '../../lib/styled/styleFunctions'
4+
import Tooltip from './Tooltip'
45

56
interface BottomBarButtonProps {
67
className?: string
78
onClick?: MouseEventHandler<HTMLButtonElement>
9+
tooltipText?: string
810
}
911

1012
const BottomBarButton: FC<BottomBarButtonProps> = ({
1113
className,
1214
onClick,
1315
children,
16+
tooltipText,
1417
}) => {
1518
return (
16-
<Container className={className} onClick={onClick}>
17-
{children}
18-
</Container>
19+
<Tooltip space={10} text={tooltipText}>
20+
<Container className={className} onClick={onClick}>
21+
{children}
22+
</Container>
23+
</Tooltip>
1924
)
2025
}
2126

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react'
2+
import ReactDOM from 'react-dom'
3+
4+
interface GeneralPortalProps {
5+
portalKey?: string
6+
}
7+
8+
class GeneralPortal extends React.PureComponent<GeneralPortalProps> {
9+
private containerEl = document.createElement('div')
10+
constructor(props: GeneralPortalProps) {
11+
super(props)
12+
}
13+
14+
componentDidMount() {
15+
document.body.appendChild(this.containerEl)
16+
}
17+
18+
componentWillUnmount() {
19+
document.body.removeChild(this.containerEl)
20+
}
21+
22+
render() {
23+
return ReactDOM.createPortal(
24+
this.props.children,
25+
this.containerEl,
26+
this.props.portalKey
27+
)
28+
}
29+
}
30+
31+
export default GeneralPortal

src/components/atoms/NavigatorButton.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react'
22
import styled from '../../lib/styled'
33
import Icon from './Icon'
4+
import Tooltip from './Tooltip'
45

56
const ButtonContainer = styled.button`
67
width: 24px;
@@ -45,14 +46,15 @@ const NavigatorButton = ({
4546
spin,
4647
}: NavigatorButtonProps) => {
4748
return (
48-
<ButtonContainer
49-
onClick={onClick}
50-
onContextMenu={onContextMenu}
51-
title={title}
52-
className={active ? 'active' : ''}
53-
>
54-
<Icon path={iconPath} spin={spin} />
55-
</ButtonContainer>
49+
<Tooltip space={10} text={title}>
50+
<ButtonContainer
51+
onClick={onClick}
52+
onContextMenu={onContextMenu}
53+
className={active ? 'active' : ''}
54+
>
55+
<Icon path={iconPath} spin={spin} />
56+
</ButtonContainer>
57+
</Tooltip>
5658
)
5759
}
5860

src/components/atoms/ToolbarButton.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import styled from '../../lib/styled'
33
import Icon from './Icon'
44
import { flexCenter, textOverflow } from '../../lib/styled/styleFunctions'
55
import cc from 'classcat'
6+
import Tooltip from './Tooltip'
67

78
interface ToolbarButtonProps {
89
iconPath?: string
@@ -22,16 +23,17 @@ const ToolbarButton = ({
2223
limitWidth,
2324
}: ToolbarButtonProps) => {
2425
return (
25-
<Container
26-
className={cc([active && 'active', limitWidth && 'limitWidth'])}
27-
title={title == null ? label : title}
28-
onClick={onClick}
29-
>
30-
{iconPath != null && <Icon className='icon' path={iconPath} />}
31-
{label != null && label.length > 0 && (
32-
<div className='label'>{label}</div>
33-
)}
34-
</Container>
26+
<Tooltip text={title == null ? label : title}>
27+
<Container
28+
className={cc([active && 'active', limitWidth && 'limitWidth'])}
29+
onClick={onClick}
30+
>
31+
{iconPath != null && <Icon className='icon' path={iconPath} />}
32+
{label != null && label.length > 0 && (
33+
<div className='label'>{label}</div>
34+
)}
35+
</Container>
36+
</Tooltip>
3537
)
3638
}
3739

src/components/atoms/ToolbarIconButton.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react'
22
import styled from '../../lib/styled'
33
import Icon from './Icon'
44
import { flexCenter } from '../../lib/styled/styleFunctions'
5+
import Tooltip from './Tooltip'
56

67
const Container = styled.button`
78
height: 32px;
@@ -12,7 +13,7 @@ const Container = styled.button`
1213
padding: 0 5px;
1314
1415
background-color: transparent;
15-
${flexCenter}
16+
${flexCenter};
1617
1718
border: none;
1819
border-radius: 3px;
@@ -49,15 +50,16 @@ const ToolbarIconButton = React.forwardRef(
4950
}: ToolbarButtonProps,
5051
ref
5152
) => (
52-
<Container
53-
onClick={onClick}
54-
onContextMenu={onContextMenu}
55-
className={active ? 'active' : ''}
56-
ref={ref}
57-
title={title}
58-
>
59-
<Icon size={18} path={iconPath} />
60-
</Container>
53+
<Tooltip space={10} text={title}>
54+
<Container
55+
onClick={onClick}
56+
onContextMenu={onContextMenu}
57+
className={active ? 'active' : ''}
58+
ref={ref}
59+
>
60+
<Icon size={18} path={iconPath} />
61+
</Container>
62+
</Tooltip>
6163
)
6264
)
6365

src/components/atoms/Tooltip.tsx

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import React from 'react'
2+
import GeneralPortal from './GeneralPortal'
3+
import styled from '../../lib/styled/styled'
4+
import { BaseTheme } from '../../lib/styled/BaseTheme'
5+
6+
interface TooltipProps {
7+
text?: string
8+
width?: number
9+
arrowWidth?: number
10+
space?: number
11+
tooltipDelayMs?: number
12+
}
13+
14+
interface TooltipStyle {
15+
width: number | string
16+
left?: number | string
17+
top?: number | string
18+
bottom?: number | string
19+
}
20+
21+
interface TooltipArrowStyle {
22+
width: number | string
23+
left?: number | string
24+
top?: number | string
25+
bottom?: number | string
26+
transform?: string
27+
display?: string
28+
}
29+
30+
interface TooltipState {
31+
visible: boolean
32+
style?: TooltipStyle
33+
arrowStyle?: TooltipArrowStyle
34+
}
35+
36+
interface TooltipBodyProps {
37+
theme: BaseTheme
38+
arrowStyle: TooltipArrowStyle
39+
}
40+
41+
const TooltipBody = styled.div<BaseTheme & TooltipBodyProps>`
42+
position: fixed;
43+
padding: 5px;
44+
background: ${({ theme }) => theme.tooltipBackgroundColor};
45+
color: white;
46+
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.3);
47+
text-align: center;
48+
font-size: 11px;
49+
border-radius: 6px;
50+
z-index: 6000;
51+
52+
&::after {
53+
display: ${({ arrowStyle }: TooltipBodyProps) => arrowStyle.display};
54+
content: '';
55+
position: absolute;
56+
bottom: ${({ arrowStyle }: TooltipBodyProps) => arrowStyle.bottom};
57+
top: ${({ arrowStyle }: TooltipBodyProps) => arrowStyle.top};
58+
left: ${({ arrowStyle }: TooltipBodyProps) => arrowStyle.left};
59+
transform: ${({ arrowStyle }: TooltipBodyProps) => arrowStyle.transform};
60+
margin-left: ${({ arrowStyle }: TooltipBodyProps) => -arrowStyle.width}px;
61+
border-width: ${({ arrowStyle }: TooltipBodyProps) => arrowStyle.width}px;
62+
border-style: solid;
63+
border-color: transparent transparent
64+
${({ theme }) => theme.tooltipBackgroundColor} transparent;
65+
}
66+
`
67+
68+
const TooltipContainer = styled.span`
69+
//border-bottom: 1px dashed grey;
70+
`
71+
72+
const DEFAULT_TOOLTIP_DELAY_MS = 500
73+
74+
class Tooltip extends React.Component<TooltipProps, TooltipState> {
75+
private readonly width: number
76+
private readonly arrowWidth: number
77+
private readonly space: number
78+
private readonly tooltipDelayMs: number
79+
private showTooltipTimer: any | null
80+
tooltipContainerRef = React.createRef<HTMLSpanElement>()
81+
tooltipRef = React.createRef<HTMLSpanElement>()
82+
constructor(props: TooltipProps) {
83+
super(props)
84+
85+
this.state = {
86+
visible: false,
87+
arrowStyle: {
88+
width: '5px',
89+
},
90+
}
91+
92+
this.arrowWidth = props.arrowWidth || 6
93+
this.width = props.width || 120
94+
this.space = props.space || 4
95+
this.tooltipDelayMs = props.tooltipDelayMs || DEFAULT_TOOLTIP_DELAY_MS
96+
97+
this.showTooltip = this.showTooltip.bind(this)
98+
this.hideTooltip = this.hideTooltip.bind(this)
99+
}
100+
101+
showTooltip() {
102+
if (!this.tooltipContainerRef.current) return
103+
const style: TooltipStyle = {
104+
width: `${this.width}px`,
105+
}
106+
const arrowStyle: TooltipArrowStyle = {
107+
width: `${this.arrowWidth}`,
108+
}
109+
const dimensions = this.tooltipContainerRef.current.getBoundingClientRect()
110+
111+
style.left = dimensions.left + dimensions.width / 2 - this.width / 2
112+
arrowStyle.left = '50%'
113+
114+
// this.space might better be of width size not height offset like now
115+
const leftMargin = document.body.clientWidth - this.width - this.space
116+
if (style.left > leftMargin) {
117+
// we could decide to not show arrow when not in center
118+
// or use below logic to position it at the center of element which tooltip is shown
119+
// arrowStyle.display = 'none'
120+
let newLeftPosition = Math.max(this.space, style.left)
121+
newLeftPosition = Math.min(newLeftPosition, leftMargin)
122+
arrowStyle.left = `${
123+
Math.abs(dimensions.left - newLeftPosition) + dimensions.width / 2
124+
}px`
125+
} else {
126+
arrowStyle.display = 'block'
127+
}
128+
129+
style.left = Math.max(this.space, style.left)
130+
style.left = Math.min(style.left, leftMargin)
131+
132+
const topOfThePagePercent = 0.9
133+
if (dimensions.top < topOfThePagePercent * window.innerHeight) {
134+
style.top = dimensions.top + dimensions.height + this.space
135+
if (this.state.arrowStyle) {
136+
arrowStyle.bottom = '100%'
137+
arrowStyle.top = 'auto'
138+
}
139+
} else {
140+
style.bottom = window.innerHeight - dimensions.top + this.space
141+
if (this.state.arrowStyle) {
142+
arrowStyle.top = '100%'
143+
arrowStyle.bottom = 'auto'
144+
arrowStyle.transform = 'rotate(180deg)'
145+
}
146+
}
147+
148+
this.setState({
149+
visible: true,
150+
style,
151+
arrowStyle,
152+
})
153+
}
154+
155+
hideTooltip() {
156+
clearTimeout(this.showTooltipTimer)
157+
this.setState({ visible: false })
158+
}
159+
160+
render() {
161+
return (
162+
<TooltipContainer
163+
onMouseOver={() => {
164+
this.showTooltipTimer = setTimeout(
165+
this.showTooltip,
166+
this.tooltipDelayMs
167+
)
168+
}}
169+
onMouseOut={this.hideTooltip}
170+
ref={this.tooltipContainerRef}
171+
>
172+
{this.props.children}
173+
174+
{this.state.visible && this.props.text && (
175+
<GeneralPortal>
176+
<TooltipBody
177+
arrowStyle={this.state.arrowStyle}
178+
style={this.state.style}
179+
>
180+
{this.props.text}
181+
</TooltipBody>
182+
</GeneralPortal>
183+
)}
184+
</TooltipContainer>
185+
)
186+
}
187+
}
188+
189+
export default Tooltip

src/components/molecules/EditorIndentationStatus.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { openContextMenu } from '../../lib/electronOnly'
44
import { MenuItemConstructorOptions } from 'electron'
55
import { capitalize } from '../../lib/string'
66
import BottomBarButton from '../atoms/BottomBarButton'
7+
import { useTranslation } from 'react-i18next'
78

89
const EditorIndentationStatus = () => {
910
const { preferences, setPreferences } = usePreferences()
1011
const currentIndentType = preferences['editor.indentType']
1112
const currentIndentSize = preferences['editor.indentSize']
13+
const { t } = useTranslation()
1214

1315
const openEditorIndentationContextMenu = useCallback(() => {
1416
openContextMenu({
@@ -60,7 +62,10 @@ const EditorIndentationStatus = () => {
6062
}, [currentIndentType, currentIndentSize, setPreferences])
6163

6264
return (
63-
<BottomBarButton onClick={openEditorIndentationContextMenu}>
65+
<BottomBarButton
66+
tooltipText={t('editor.editorIndentStatus')}
67+
onClick={openEditorIndentationContextMenu}
68+
>
6469
{capitalize(currentIndentType)}: {currentIndentSize}
6570
</BottomBarButton>
6671
)

0 commit comments

Comments
 (0)