@@ -36,7 +36,7 @@ const BaseTooltipWrapper = styled.div`
3636const TooltipWrapper = styled ( BaseTooltipWrapper ) `
3737 align-items: center;
3838
39- margin-top : ${ spacing . small } ;
39+ margin: ${ spacing . small } ;
4040 padding: ${ spacing . small } ${ spacing . medium } ;
4141
4242 min-height: ${ componentSize . mini } ;
@@ -64,7 +64,7 @@ const ExpandedTooltipWrapper = styled(BaseTooltipWrapper)`
6464 align-items: flex-start;
6565 gap: ${ spacing . medium } ;
6666
67- margin: ${ spacing . medium } ${ spacing . small } ;
67+ margin: ${ spacing . medium } ;
6868 padding: ${ spacing . medium } ;
6969
7070 height: auto;
@@ -91,6 +91,7 @@ const ExpandedTooltipTitle = styled(Typography).attrs({
9191 variant : 'chip-tag-text' ,
9292} ) `
9393 font-weight: ${ font . fontWeight . semibold } ;
94+ white-space: nowrap;
9495`
9596
9697const ExpandedTooltipExtraInfo = styled ( Typography ) . attrs ( {
@@ -109,23 +110,98 @@ export const ExpandedTooltipTypography: React.FC<
109110 < StyledExpandedTooltipTypography > { children } </ StyledExpandedTooltipTypography >
110111)
111112
112- const ToolTipUpArrow = styled . div `
113+ const upDownArrowBase = css `
113114 width: 0;
114115 height: 0;
115- margin: 3px ${ spacing . medium } 0 ${ spacing . medium } ;
116116 border-left: 5px solid transparent;
117117 border-right: 5px solid transparent;
118+ `
119+ const TooltipUpArrow = styled . div `
120+ ${ upDownArrowBase } ;
121+ margin-top: 3px;
118122 border-bottom: 5px solid ${ ( { theme } ) => theme . color . background00 ( ) } ;
119123`
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 `
121131 width: 0;
122132 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 ( ) } ;
127145`
128146
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+
129205interface TooltipProps extends Omit < PopOverProps , 'anchorEl' > {
130206 /**
131207 * Optional Tooltip variant.
@@ -143,6 +219,11 @@ interface ExpandedTooltipProps extends Omit<PopOverProps, 'anchorEl'> {
143219 * Required Tooltip variant.
144220 */
145221 readonly variant : 'expanded'
222+ /**
223+ * Optional placement.
224+ * Default: `up-down`
225+ */
226+ readonly placement ?: 'up-down' | 'left-right'
146227 /**
147228 * Optional semibold title text inside the tooltip.
148229 */
@@ -162,16 +243,14 @@ export const Tooltip: React.FC<TooltipProps | ExpandedTooltipProps> = ({
162243 children,
163244 ...props
164245} ) => {
246+ const placement =
247+ ( props . variant === 'expanded' ? props . placement : undefined ) ?? 'up-down'
165248 const child = Children . only ( children ) as ReactElement
166249 const [ anchorEl , setAnchorEl ] = useState < HTMLElement | null > ( null )
167250
168251 const [ visible , show , hide ] = useBoolean ( false )
169-
170252 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' )
175254 const [ tooltipEl , setTooltipEl ] = useState < HTMLDivElement | null > ( null )
176255
177256 useEffect ( ( ) => {
@@ -199,20 +278,51 @@ export const Tooltip: React.FC<TooltipProps | ExpandedTooltipProps> = ({
199278 return
200279 }
201280
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+ }
208311
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+ }
214324 }
215- } , [ anchorEl , tooltipEl ] )
325+ } , [ anchorEl , tooltipEl , props , placement ] )
216326
217327 if ( props . variant !== 'expanded' ) {
218328 return (
@@ -221,14 +331,7 @@ export const Tooltip: React.FC<TooltipProps | ExpandedTooltipProps> = ({
221331 ref : setAnchorEl ,
222332 } ) }
223333 { 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 } >
232335 < TooltipWrapper ref = { setTooltipEl } >
233336 < Typography variant = "chip-tag-text" > { props . text } </ Typography >
234337 </ TooltipWrapper >
@@ -238,47 +341,34 @@ export const Tooltip: React.FC<TooltipProps | ExpandedTooltipProps> = ({
238341 )
239342 }
240343
344+ const { tipTitle, extraInfo, contents } = props
345+
241346 return (
242347 < >
243348 { React . cloneElement ( child , {
244349 ref : setAnchorEl ,
245350 } ) }
246351 { debouncedVisible ? (
247352 < >
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 } >
256354 < ExpandedTooltipWrapper ref = { setTooltipEl } >
257- { props . tipTitle !== undefined || props . extraInfo !== undefined ? (
258- props . extraInfo !== undefined ? (
355+ { tipTitle !== undefined || extraInfo !== undefined ? (
356+ extraInfo !== undefined ? (
259357 < ExpandedTooltipTop >
260- < ExpandedTooltipTitle >
261- { props . tipTitle }
262- </ ExpandedTooltipTitle >
358+ < ExpandedTooltipTitle > { tipTitle } </ ExpandedTooltipTitle >
263359 < ExpandedTooltipExtraInfo >
264- { props . extraInfo }
360+ { extraInfo }
265361 </ ExpandedTooltipExtraInfo >
266362 </ ExpandedTooltipTop >
267363 ) : (
268- < ExpandedTooltipTitle > { props . tipTitle } </ ExpandedTooltipTitle >
364+ < ExpandedTooltipTitle > { tipTitle } </ ExpandedTooltipTitle >
269365 )
270366 ) : null }
271- { props . contents }
367+ { contents }
272368 </ ExpandedTooltipWrapper >
273369 </ 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 ] }
282372 </ PopOver >
283373 </ >
284374 ) : null }
0 commit comments