@@ -83,7 +83,11 @@ class Tooltip extends Component {
8383 return ;
8484 }
8585 const open = useControlledStateWithValue ? props . defaultOpen : props . open ;
86- this . state = { open } ;
86+ this . state = {
87+ open,
88+ storedDirection : props . direction ,
89+ storedAlign : props . align ,
90+ } ;
8791 }
8892
8993 static propTypes = {
@@ -93,6 +97,11 @@ class Tooltip extends Component {
9397 */
9498 align : PropTypes . oneOf ( [ 'start' , 'center' , 'end' ] ) ,
9599
100+ /**
101+ * Whether or not to re-orientate the tooltip if it goes outside,
102+ * of the bounds of the parent.
103+ */
104+ autoOrientation : PropTypes . bool ,
96105 /**
97106 * Contents to put into the tooltip.
98107 */
@@ -263,6 +272,172 @@ class Tooltip extends Component {
263272 document . addEventListener ( 'keydown' , this . handleEscKeyPress , false ) ;
264273 }
265274
275+ componentDidUpdate ( prevProps , prevState ) {
276+ if ( prevProps . direction != this . props . direction ) {
277+ this . setState ( { storedDirection : this . props . direction } ) ;
278+ }
279+ if ( prevProps . align != this . props . align ) {
280+ this . setState ( { storedAlign : this . props . align } ) ;
281+ }
282+ if ( prevState . open && ! this . state . open ) {
283+ // Reset orientation when closing
284+ this . setState ( {
285+ storedDirection : this . props . direction ,
286+ storedAlign : this . props . align ,
287+ } ) ;
288+ }
289+ }
290+
291+ updateOrientation = ( params ) => {
292+ if ( this . props . autoOrientation ) {
293+ const newOrientation = this . getBestDirection ( params ) ;
294+ const { direction, align } = newOrientation ;
295+
296+ if ( direction !== this . state . storedDirection ) {
297+ this . setState ( { open : false } , ( ) => {
298+ this . setState ( { open : true , storedDirection : direction } ) ;
299+ } ) ;
300+ }
301+
302+ if ( align === 'original' ) {
303+ this . setState ( { storedAlign : this . props . align } ) ;
304+ } else {
305+ this . setState ( { storedAlign : align } ) ;
306+ }
307+ }
308+ } ;
309+
310+ getBestDirection = ( {
311+ menuSize,
312+ refPosition = { } ,
313+ offset = { } ,
314+ direction = DIRECTION_BOTTOM ,
315+ scrollX : pageXOffset = 0 ,
316+ scrollY : pageYOffset = 0 ,
317+ container,
318+ } ) => {
319+ const {
320+ left : refLeft = 0 ,
321+ top : refTop = 0 ,
322+ right : refRight = 0 ,
323+ bottom : refBottom = 0 ,
324+ } = refPosition ;
325+ const scrollX = container . position !== 'static' ? 0 : pageXOffset ;
326+ const scrollY = container . position !== 'static' ? 0 : pageYOffset ;
327+ const relativeDiff = {
328+ top : container . position !== 'static' ? container . rect . top : 0 ,
329+ left : container . position !== 'static' ? container . rect . left : 0 ,
330+ } ;
331+ const { width, height } = menuSize ;
332+ const { top = 0 , left = 0 } = offset ;
333+ const refCenterHorizontal = ( refLeft + refRight ) / 2 ;
334+ const refCenterVertical = ( refTop + refBottom ) / 2 ;
335+
336+ // Calculate whether a new direction is needed to stay in parent.
337+ // It will switch the current direction to the opposite i.e.
338+ // If the direction="top" and the top boundary is overflowed
339+ // then it switches the direction to "bottom".
340+ const newDirection = ( ) => {
341+ switch ( direction ) {
342+ case DIRECTION_LEFT :
343+ return refLeft - width + scrollX - left - relativeDiff . left < 0
344+ ? DIRECTION_RIGHT
345+ : direction ;
346+ case DIRECTION_TOP :
347+ return refTop - height + scrollY - top - relativeDiff . top < 0
348+ ? DIRECTION_BOTTOM
349+ : direction ;
350+ case DIRECTION_RIGHT :
351+ return refRight + scrollX + left - relativeDiff . left + width >
352+ container . rect . width
353+ ? DIRECTION_LEFT
354+ : direction ;
355+ case DIRECTION_BOTTOM :
356+ return refBottom + scrollY + top - relativeDiff . top + height >
357+ container . rect . height
358+ ? DIRECTION_TOP
359+ : direction ;
360+ default :
361+ // If there is a new direction then ignore the logic above
362+ return direction ;
363+ }
364+ } ;
365+
366+ // Calculate whether a new alignment is needed to stay in parent
367+ // If the direction is left or right this involves checking the
368+ // overflow in the vertical direction. If the direction is top or
369+ // bottom, this involves checking overflow in the horizontal direction.
370+ // "original" is used to signify no change.
371+ const newAlignment = ( ) => {
372+ switch ( direction ) {
373+ case DIRECTION_LEFT :
374+ case DIRECTION_RIGHT :
375+ if (
376+ refCenterVertical -
377+ height / 2 +
378+ scrollY +
379+ top -
380+ 9 -
381+ relativeDiff . top <
382+ 0
383+ ) {
384+ // If goes above the top boundary
385+ return 'start' ;
386+ } else if (
387+ refCenterVertical -
388+ height / 2 +
389+ scrollY +
390+ top -
391+ 9 -
392+ relativeDiff . top +
393+ height >
394+ container . rect . height
395+ ) {
396+ // If goes below the bottom boundary
397+ return 'end' ;
398+ } else {
399+ // No need to change alignment
400+ return 'original' ;
401+ }
402+ case DIRECTION_TOP :
403+ case DIRECTION_BOTTOM :
404+ if (
405+ refCenterHorizontal -
406+ width / 2 +
407+ scrollX +
408+ left -
409+ relativeDiff . left <
410+ 0
411+ ) {
412+ // If goes below the left boundary
413+ return 'start' ;
414+ } else if (
415+ refCenterHorizontal -
416+ width / 2 +
417+ scrollX +
418+ left -
419+ relativeDiff . left +
420+ width >
421+ container . rect . width
422+ ) {
423+ // If it goes over the right boundary
424+ return 'end' ;
425+ } else {
426+ // No need to change alignment
427+ return 'original' ;
428+ }
429+ default :
430+ // No need to change alignment
431+ return 'original' ;
432+ }
433+ } ;
434+
435+ return {
436+ direction : newDirection ( ) ,
437+ align : newAlignment ( ) ,
438+ } ;
439+ } ;
440+
266441 componentWillUnmount ( ) {
267442 if ( this . _debouncedHandleFocus ) {
268443 this . _debouncedHandleFocus . cancel ( ) ;
@@ -430,8 +605,6 @@ class Tooltip extends Component {
430605 children,
431606 className,
432607 triggerClassName,
433- direction,
434- align,
435608 focusTrap,
436609 triggerText,
437610 showIcon,
@@ -447,13 +620,14 @@ class Tooltip extends Component {
447620 } = this . props ;
448621
449622 const { open } = this . isControlled ? this . props : this . state ;
623+ const { storedDirection, storedAlign } = this . state ;
450624
451625 const tooltipClasses = classNames (
452626 `${ prefix } --tooltip` ,
453627 {
454628 [ `${ prefix } --tooltip--shown` ] : open ,
455- [ `${ prefix } --tooltip--${ direction } ` ] : direction ,
456- [ `${ prefix } --tooltip--align-${ align } ` ] : align ,
629+ [ `${ prefix } --tooltip--${ storedDirection } ` ] : storedDirection ,
630+ [ `${ prefix } --tooltip--align-${ storedAlign } ` ] : storedAlign ,
457631 } ,
458632 className
459633 ) ;
@@ -523,16 +697,17 @@ class Tooltip extends Component {
523697 selectorPrimaryFocus = { this . props . selectorPrimaryFocus }
524698 target = { this . _getTarget }
525699 triggerRef = { this . _triggerRef }
526- menuDirection = { direction }
700+ menuDirection = { storedDirection }
527701 menuOffset = { menuOffset }
528702 menuRef = { ( node ) => {
529703 this . _tooltipEl = node ;
530- } } >
704+ } }
705+ updateOrientation = { this . updateOrientation } >
531706 < div
532707 className = { tooltipClasses }
533708 { ...other }
534709 id = { this . _tooltipId }
535- data-floating-menu-direction = { direction }
710+ data-floating-menu-direction = { storedDirection }
536711 onMouseOver = { this . handleMouse }
537712 onMouseOut = { this . handleMouse }
538713 onFocus = { this . handleMouse }
0 commit comments