@@ -36,7 +36,7 @@ const BaseTooltipWrapper = styled.div`
36
36
const TooltipWrapper = styled ( BaseTooltipWrapper ) `
37
37
align-items: center;
38
38
39
- margin-top : ${ spacing . small } ;
39
+ margin: ${ spacing . small } ;
40
40
padding: ${ spacing . small } ${ spacing . medium } ;
41
41
42
42
min-height: ${ componentSize . mini } ;
@@ -64,7 +64,7 @@ const ExpandedTooltipWrapper = styled(BaseTooltipWrapper)`
64
64
align-items: flex-start;
65
65
gap: ${ spacing . medium } ;
66
66
67
- margin: ${ spacing . medium } ${ spacing . small } ;
67
+ margin: ${ spacing . medium } ;
68
68
padding: ${ spacing . medium } ;
69
69
70
70
height: auto;
@@ -91,6 +91,7 @@ const ExpandedTooltipTitle = styled(Typography).attrs({
91
91
variant : 'chip-tag-text' ,
92
92
} ) `
93
93
font-weight: ${ font . fontWeight . semibold } ;
94
+ white-space: nowrap;
94
95
`
95
96
96
97
const ExpandedTooltipExtraInfo = styled ( Typography ) . attrs ( {
@@ -109,23 +110,98 @@ export const ExpandedTooltipTypography: React.FC<
109
110
< StyledExpandedTooltipTypography > { children } </ StyledExpandedTooltipTypography >
110
111
)
111
112
112
- const ToolTipUpArrow = styled . div `
113
+ const upDownArrowBase = css `
113
114
width: 0;
114
115
height: 0;
115
- margin: 3px ${ spacing . medium } 0 ${ spacing . medium } ;
116
116
border-left: 5px solid transparent;
117
117
border-right: 5px solid transparent;
118
+ `
119
+ const TooltipUpArrow = styled . div `
120
+ ${ upDownArrowBase } ;
121
+ margin-top: 3px;
118
122
border-bottom: 5px solid ${ ( { theme } ) => theme . color . background00 ( ) } ;
119
123
`
120
- const ToolTipDownArrow = styled . div `
124
+ const TooltipDownArrow = styled . div `
125
+ ${ upDownArrowBase } ;
126
+ margin-bottom: 3px;
127
+ border-top: 5px solid ${ ( { theme } ) => theme . color . background00 ( ) } ;
128
+ `
129
+
130
+ const leftRightArrowBase = css `
121
131
width: 0;
122
132
height: 0;
123
- margin: 0 ${ spacing . medium } 3px ${ spacing . medium } ;
124
- border-left: 5px solid transparent;
125
- border-right: 5px solid transparent;
126
- border-top: 5px solid ${ ( { theme } ) => theme . color . background00 ( ) } ;
133
+ border-top: 5px solid transparent;
134
+ border-bottom: 5px solid transparent;
135
+ `
136
+ const TooltipLeftArrow = styled . div `
137
+ ${ leftRightArrowBase } ;
138
+ margin-left: 3px;
139
+ border-right: 5px solid ${ ( { theme } ) => theme . color . background00 ( ) } ;
140
+ `
141
+ const TooltipRightArrow = styled . div `
142
+ ${ leftRightArrowBase } ;
143
+ margin-right: 3px;
144
+ border-left: 5px solid ${ ( { theme } ) => theme . color . background00 ( ) } ;
127
145
`
128
146
147
+ type Placement = 'up' | 'right' | 'down' | 'left'
148
+
149
+ const pointInBounds = ( pos : readonly [ number , number ] ) =>
150
+ pos [ 0 ] >= 0 &&
151
+ pos [ 0 ] <= document . documentElement . clientWidth &&
152
+ pos [ 1 ] >= 0 &&
153
+ pos [ 1 ] <= document . documentElement . clientHeight
154
+
155
+ const rectInBounds = (
156
+ pos : readonly [ number , number ] ,
157
+ size : readonly [ number , number ]
158
+ ) => pointInBounds ( pos ) && pointInBounds ( [ pos [ 0 ] + size [ 0 ] , pos [ 1 ] + size [ 1 ] ] )
159
+
160
+ const alignments : Record <
161
+ Placement ,
162
+ Required <
163
+ Pick <
164
+ PopOverProps ,
165
+ | 'horizontalPosition'
166
+ | 'horizontalAlignment'
167
+ | 'verticalPosition'
168
+ | 'verticalAlignment'
169
+ >
170
+ >
171
+ > = {
172
+ up : {
173
+ horizontalPosition : 'center' ,
174
+ horizontalAlignment : 'center' ,
175
+ verticalPosition : 'top' ,
176
+ verticalAlignment : 'bottom' ,
177
+ } ,
178
+ down : {
179
+ horizontalPosition : 'center' ,
180
+ horizontalAlignment : 'center' ,
181
+ verticalPosition : 'bottom' ,
182
+ verticalAlignment : 'top' ,
183
+ } ,
184
+ left : {
185
+ horizontalPosition : 'left' ,
186
+ horizontalAlignment : 'right' ,
187
+ verticalPosition : 'center' ,
188
+ verticalAlignment : 'center' ,
189
+ } ,
190
+ right : {
191
+ horizontalPosition : 'right' ,
192
+ horizontalAlignment : 'left' ,
193
+ verticalPosition : 'center' ,
194
+ verticalAlignment : 'center' ,
195
+ } ,
196
+ }
197
+
198
+ const arrows : Record < Placement , ReactElement > = {
199
+ left : < TooltipRightArrow /> ,
200
+ right : < TooltipLeftArrow /> ,
201
+ up : < TooltipDownArrow /> ,
202
+ down : < TooltipUpArrow /> ,
203
+ }
204
+
129
205
interface TooltipProps extends Omit < PopOverProps , 'anchorEl' > {
130
206
/**
131
207
* Optional Tooltip variant.
@@ -143,6 +219,11 @@ interface ExpandedTooltipProps extends Omit<PopOverProps, 'anchorEl'> {
143
219
* Required Tooltip variant.
144
220
*/
145
221
readonly variant : 'expanded'
222
+ /**
223
+ * Optional placement.
224
+ * Default: `up-down`
225
+ */
226
+ readonly placement ?: 'up-down' | 'left-right'
146
227
/**
147
228
* Optional semibold title text inside the tooltip.
148
229
*/
@@ -162,16 +243,14 @@ export const Tooltip: React.FC<TooltipProps | ExpandedTooltipProps> = ({
162
243
children,
163
244
...props
164
245
} ) => {
246
+ const placement =
247
+ ( props . variant === 'expanded' ? props . placement : undefined ) ?? 'up-down'
165
248
const child = Children . only ( children ) as ReactElement
166
249
const [ anchorEl , setAnchorEl ] = useState < HTMLElement | null > ( null )
167
250
168
251
const [ visible , show , hide ] = useBoolean ( false )
169
-
170
252
const [ debouncedVisible , setDebouncedVisible ] = useState ( visible )
171
- const [ hasOverflow , setHasOverflow ] = useState ( false )
172
- const [ horizontalLayout , setHorizontalLayout ] = useState <
173
- 'left' | 'right' | 'center'
174
- > ( 'center' )
253
+ const [ layout , setLayout ] = useState < Placement > ( 'down' )
175
254
const [ tooltipEl , setTooltipEl ] = useState < HTMLDivElement | null > ( null )
176
255
177
256
useEffect ( ( ) => {
@@ -199,20 +278,51 @@ export const Tooltip: React.FC<TooltipProps | ExpandedTooltipProps> = ({
199
278
return
200
279
}
201
280
202
- const { bottom } = anchorEl . getBoundingClientRect ( )
203
-
204
- const bottomSpace = document . documentElement . clientHeight - bottom
205
- // See if `bottomSpace` is smaller than Tooltip height.
206
- // "8" is margin of the TooltipWrapper.
207
- setHasOverflow ( tooltipEl . clientHeight + 8 > bottomSpace )
281
+ const bounds = anchorEl . getBoundingClientRect ( )
282
+
283
+ // "16" is for space of margin of ExpandedTooltipWrapper + arrow size.
284
+ const tooltipSize : [ number , number ] = [
285
+ tooltipEl . clientWidth + 16 ,
286
+ tooltipEl . clientHeight + 16 ,
287
+ ]
288
+ const tooltipMid = [
289
+ bounds . left + ( bounds . right - bounds . left ) / 2 ,
290
+ bounds . top + ( bounds . bottom - bounds . top ) / 2 ,
291
+ ]
292
+
293
+ const spaces : Record < Placement , boolean > = {
294
+ down : rectInBounds (
295
+ [ tooltipMid [ 0 ] - tooltipSize [ 0 ] / 2 , bounds . bottom ] ,
296
+ tooltipSize
297
+ ) ,
298
+ up : rectInBounds (
299
+ [ tooltipMid [ 0 ] - tooltipSize [ 0 ] / 2 , bounds . top - tooltipSize [ 1 ] ] ,
300
+ tooltipSize
301
+ ) ,
302
+ left : rectInBounds (
303
+ [ bounds . left - tooltipSize [ 0 ] , tooltipMid [ 1 ] - tooltipSize [ 1 ] / 2 ] ,
304
+ tooltipSize
305
+ ) ,
306
+ right : rectInBounds (
307
+ [ bounds . right , tooltipMid [ 1 ] - tooltipSize [ 1 ] / 2 ] ,
308
+ tooltipSize
309
+ ) ,
310
+ }
208
311
209
- const { left, right } = tooltipEl . getBoundingClientRect ( )
210
- if ( left < 0 ) {
211
- setHorizontalLayout ( 'left' )
212
- } else if ( right > document . documentElement . clientWidth ) {
213
- setHorizontalLayout ( 'right' )
312
+ if ( placement === 'up-down' ) {
313
+ if ( spaces . up || spaces . down ) {
314
+ setLayout ( spaces . down ? 'down' : 'up' )
315
+ } else {
316
+ setLayout ( spaces . right ? 'right' : 'left' )
317
+ }
318
+ } else if ( placement === 'left-right' ) {
319
+ if ( spaces . right || spaces . left ) {
320
+ setLayout ( spaces . right ? 'right' : 'left' )
321
+ } else {
322
+ setLayout ( spaces . up ? 'up' : 'down' )
323
+ }
214
324
}
215
- } , [ anchorEl , tooltipEl ] )
325
+ } , [ anchorEl , tooltipEl , props , placement ] )
216
326
217
327
if ( props . variant !== 'expanded' ) {
218
328
return (
@@ -221,14 +331,7 @@ export const Tooltip: React.FC<TooltipProps | ExpandedTooltipProps> = ({
221
331
ref : setAnchorEl ,
222
332
} ) }
223
333
{ debouncedVisible ? (
224
- < PopOver
225
- anchorEl = { anchorEl }
226
- horizontalPosition = { horizontalLayout }
227
- horizontalAlignment = { horizontalLayout }
228
- verticalPosition = { hasOverflow ? 'top' : 'bottom' }
229
- verticalAlignment = { hasOverflow ? 'bottom' : 'top' }
230
- { ...props }
231
- >
334
+ < PopOver anchorEl = { anchorEl } { ...alignments [ layout ] } { ...props } >
232
335
< TooltipWrapper ref = { setTooltipEl } >
233
336
< Typography variant = "chip-tag-text" > { props . text } </ Typography >
234
337
</ TooltipWrapper >
@@ -238,47 +341,34 @@ export const Tooltip: React.FC<TooltipProps | ExpandedTooltipProps> = ({
238
341
)
239
342
}
240
343
344
+ const { tipTitle, extraInfo, contents } = props
345
+
241
346
return (
242
347
< >
243
348
{ React . cloneElement ( child , {
244
349
ref : setAnchorEl ,
245
350
} ) }
246
351
{ debouncedVisible ? (
247
352
< >
248
- < PopOver
249
- anchorEl = { anchorEl }
250
- horizontalPosition = { horizontalLayout }
251
- horizontalAlignment = { horizontalLayout }
252
- verticalPosition = { hasOverflow ? 'top' : 'bottom' }
253
- verticalAlignment = { hasOverflow ? 'bottom' : 'top' }
254
- { ...props }
255
- >
353
+ < PopOver anchorEl = { anchorEl } { ...alignments [ layout ] } { ...props } >
256
354
< ExpandedTooltipWrapper ref = { setTooltipEl } >
257
- { props . tipTitle !== undefined || props . extraInfo !== undefined ? (
258
- props . extraInfo !== undefined ? (
355
+ { tipTitle !== undefined || extraInfo !== undefined ? (
356
+ extraInfo !== undefined ? (
259
357
< ExpandedTooltipTop >
260
- < ExpandedTooltipTitle >
261
- { props . tipTitle }
262
- </ ExpandedTooltipTitle >
358
+ < ExpandedTooltipTitle > { tipTitle } </ ExpandedTooltipTitle >
263
359
< ExpandedTooltipExtraInfo >
264
- { props . extraInfo }
360
+ { extraInfo }
265
361
</ ExpandedTooltipExtraInfo >
266
362
</ ExpandedTooltipTop >
267
363
) : (
268
- < ExpandedTooltipTitle > { props . tipTitle } </ ExpandedTooltipTitle >
364
+ < ExpandedTooltipTitle > { tipTitle } </ ExpandedTooltipTitle >
269
365
)
270
366
) : null }
271
- { props . contents }
367
+ { contents }
272
368
</ ExpandedTooltipWrapper >
273
369
</ PopOver >
274
- < PopOver
275
- anchorEl = { anchorEl }
276
- horizontalPosition = "center"
277
- horizontalAlignment = "center"
278
- verticalPosition = { hasOverflow ? 'top' : 'bottom' }
279
- verticalAlignment = { hasOverflow ? 'bottom' : 'top' }
280
- >
281
- { hasOverflow ? < ToolTipDownArrow /> : < ToolTipUpArrow /> }
370
+ < PopOver anchorEl = { anchorEl } { ...alignments [ layout ] } >
371
+ { arrows [ layout ] }
282
372
</ PopOver >
283
373
</ >
284
374
) : null }
0 commit comments