From 4d996426acc6e3fc8bdd9f52611a0baba9f52630 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Tue, 16 Jun 2020 00:41:27 +0200
Subject: [PATCH] WIP

---
 app/javascript/mastodon/components/button.js  |  22 +-
 app/javascript/mastodon/components/icon.js    |   8 +-
 .../mastodon/components/icon_button.js        |  34 +--
 .../mastodon/components/media_gallery.js      |  71 +++---
 .../mastodon/components/permalink.js          |  11 +-
 .../mastodon/components/relative_timestamp.js |  10 +-
 app/javascript/mastodon/components/status.js  |  15 +-
 app/javascript/mastodon/components/tooltip.js |  22 ++
 .../features/account/components/header.js     |  58 +++--
 .../compose/components/autosuggest_account.js |  11 +-
 .../components/emoji_picker_dropdown.js       |  17 +-
 .../compose/components/text_icon_button.js    |  22 +-
 .../containers/sensitive_button_container.js  |  29 ++-
 .../mastodon/features/compose/index.js        |  49 +++-
 app/javascript/styles/application.scss        |   1 +
 .../styles/mastodon/components.scss           |   3 +-
 app/javascript/styles/mastodon/tooltips.scss  | 233 ++++++++++++++++++
 package.json                                  |   1 +
 yarn.lock                                     |  74 +++++-
 19 files changed, 553 insertions(+), 138 deletions(-)
 create mode 100644 app/javascript/mastodon/components/tooltip.js
 create mode 100644 app/javascript/styles/mastodon/tooltips.scss

diff --git a/app/javascript/mastodon/components/button.js b/app/javascript/mastodon/components/button.js
index eb8dd7dc8eb..ac336bfbbf3 100644
--- a/app/javascript/mastodon/components/button.js
+++ b/app/javascript/mastodon/components/button.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
+import Tooltip from 'mastodon/components/tooltip';
 
 export default class Button extends React.PureComponent {
 
@@ -49,16 +50,17 @@ export default class Button extends React.PureComponent {
     });
 
     return (
-      <button
-        className={className}
-        disabled={this.props.disabled}
-        onClick={this.handleClick}
-        ref={this.setRef}
-        style={style}
-        title={this.props.title}
-      >
-        {this.props.text || this.props.children}
-      </button>
+      <Tooltip placement='bottom' overlay={this.props.title}>
+        <button
+          className={className}
+          disabled={this.props.disabled}
+          onClick={this.handleClick}
+          ref={this.setRef}
+          style={style}
+        >
+          {this.props.text || this.props.children}
+        </button>
+      </Tooltip>
     );
   }
 
diff --git a/app/javascript/mastodon/components/icon.js b/app/javascript/mastodon/components/icon.js
index d8a17722fed..073bd17cffc 100644
--- a/app/javascript/mastodon/components/icon.js
+++ b/app/javascript/mastodon/components/icon.js
@@ -1,20 +1,24 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
+import Tooltip from 'mastodon/components/tooltip';
 
 export default class Icon extends React.PureComponent {
 
   static propTypes = {
     id: PropTypes.string.isRequired,
     className: PropTypes.string,
+    title: PropTypes.node,
     fixedWidth: PropTypes.bool,
   };
 
   render () {
-    const { id, className, fixedWidth, ...other } = this.props;
+    const { id, className, fixedWidth, title, ...other } = this.props;
 
     return (
-      <i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />
+      <Tooltip placement='top' overlay={title}>
+        <i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />
+      </Tooltip>
     );
   }
 
diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js
index fd715bc3c83..d56d117b450 100644
--- a/app/javascript/mastodon/components/icon_button.js
+++ b/app/javascript/mastodon/components/icon_button.js
@@ -2,6 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import Icon from 'mastodon/components/icon';
+import Tooltip from 'mastodon/components/tooltip';
 
 export default class IconButton extends React.PureComponent {
 
@@ -114,22 +115,23 @@ export default class IconButton extends React.PureComponent {
     });
 
     return (
-      <button
-        aria-label={title}
-        aria-pressed={pressed}
-        aria-expanded={expanded}
-        title={title}
-        className={classes}
-        onClick={this.handleClick}
-        onMouseDown={this.handleMouseDown}
-        onKeyDown={this.handleKeyDown}
-        onKeyPress={this.handleKeyPress}
-        style={style}
-        tabIndex={tabIndex}
-        disabled={disabled}
-      >
-        <Icon id={icon} fixedWidth aria-hidden='true' />
-      </button>
+      <Tooltip placement='bottom' overlay={title}>
+        <button
+          aria-label={title}
+          aria-pressed={pressed}
+          aria-expanded={expanded}
+          className={classes}
+          onClick={this.handleClick}
+          onMouseDown={this.handleMouseDown}
+          onKeyDown={this.handleKeyDown}
+          onKeyPress={this.handleKeyPress}
+          style={style}
+          tabIndex={tabIndex}
+          disabled={disabled}
+        >
+          <Icon id={icon} fixedWidth aria-hidden='true' />
+        </button>
+      </Tooltip>
     );
   }
 
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index a31de206b43..93c8e2abe32 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -8,6 +8,7 @@ import { isIOS } from '../is_mobile';
 import classNames from 'classnames';
 import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state';
 import { decode } from 'blurhash';
+import Tooltip from 'mastodon/components/tooltip';
 
 const messages = defineMessages({
   toggle_visible: { id: 'media_gallery.toggle_visible',
@@ -165,9 +166,11 @@ class Item extends React.PureComponent {
     if (attachment.get('type') === 'unknown') {
       return (
         <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
-          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'>
-            <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
-          </a>
+          <Tooltip placement='bottom' overlay={attachment.get('description')}>
+            <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} aria-label={attachment.get('description')} target='_blank' rel='noopener noreferrer'>
+              <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
+            </a>
+          </Tooltip>
         </div>
       );
     } else if (attachment.get('type') === 'image') {
@@ -188,42 +191,44 @@ class Item extends React.PureComponent {
       const y      = ((focusY / -2) + .5) * 100;
 
       thumbnail = (
-        <a
-          className='media-gallery__item-thumbnail'
-          href={attachment.get('remote_url') || originalUrl}
-          onClick={this.handleClick}
-          target='_blank'
-          rel='noopener noreferrer'
-        >
-          <img
-            src={previewUrl}
-            srcSet={srcSet}
-            sizes={sizes}
-            alt={attachment.get('description')}
-            title={attachment.get('description')}
-            style={{ objectPosition: `${x}% ${y}%` }}
-            onLoad={this.handleImageLoad}
-          />
-        </a>
+        <Tooltip placement='bottom' overlay={attachment.get('description')}>
+          <a
+            className='media-gallery__item-thumbnail'
+            href={attachment.get('remote_url') || originalUrl}
+            onClick={this.handleClick}
+            target='_blank'
+            rel='noopener noreferrer'
+          >
+            <img
+              src={previewUrl}
+              srcSet={srcSet}
+              sizes={sizes}
+              alt={attachment.get('description')}
+              style={{ objectPosition: `${x}% ${y}%` }}
+              onLoad={this.handleImageLoad}
+            />
+          </a>
+        </Tooltip>
       );
     } else if (attachment.get('type') === 'gifv') {
       const autoPlay = !isIOS() && this.getAutoPlay();
 
       thumbnail = (
         <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
-          <video
-            className='media-gallery__item-gifv-thumbnail'
-            aria-label={attachment.get('description')}
-            title={attachment.get('description')}
-            role='application'
-            src={attachment.get('url')}
-            onClick={this.handleClick}
-            onMouseEnter={this.handleMouseEnter}
-            onMouseLeave={this.handleMouseLeave}
-            autoPlay={autoPlay}
-            loop
-            muted
-          />
+          <Tooltip placement='bottom' overlay={attachment.get('description')}>
+            <video
+              className='media-gallery__item-gifv-thumbnail'
+              aria-label={attachment.get('description')}
+              role='application'
+              src={attachment.get('url')}
+              onClick={this.handleClick}
+              onMouseEnter={this.handleMouseEnter}
+              onMouseLeave={this.handleMouseLeave}
+              autoPlay={autoPlay}
+              loop
+              muted
+            />
+          </Tooltip>
 
           <span className='media-gallery__gifv__label'>GIF</span>
         </div>
diff --git a/app/javascript/mastodon/components/permalink.js b/app/javascript/mastodon/components/permalink.js
index b369e98126d..4e484198843 100644
--- a/app/javascript/mastodon/components/permalink.js
+++ b/app/javascript/mastodon/components/permalink.js
@@ -1,5 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import Tooltip from 'mastodon/components/tooltip';
 
 export default class Permalink extends React.PureComponent {
 
@@ -28,12 +29,14 @@ export default class Permalink extends React.PureComponent {
   }
 
   render () {
-    const { href, children, className, onInterceptClick, ...other } = this.props;
+    const { href, children, className, onInterceptClick, title, ...other } = this.props;
 
     return (
-      <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
-        {children}
-      </a>
+      <Tooltip placement='top' overlay={title}>
+        <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
+          {children}
+        </a>
+      </Tooltip>
     );
   }
 
diff --git a/app/javascript/mastodon/components/relative_timestamp.js b/app/javascript/mastodon/components/relative_timestamp.js
index 711181dcdbe..4c2fca1bbed 100644
--- a/app/javascript/mastodon/components/relative_timestamp.js
+++ b/app/javascript/mastodon/components/relative_timestamp.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import { injectIntl, defineMessages } from 'react-intl';
 import PropTypes from 'prop-types';
+import Tooltip from 'mastodon/components/tooltip';
 
 const messages = defineMessages({
   today: { id: 'relative_time.today', defaultMessage: 'today' },
@@ -181,11 +182,14 @@ class RelativeTimestamp extends React.Component {
     const timeGiven    = timestamp.includes('T');
     const date         = new Date(timestamp);
     const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven);
+    const formatted    = intl.formatDate(date, dateFormatOptions);
 
     return (
-      <time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>
-        {relativeTime}
-      </time>
+      <Tooltip placement='top' overlay={formatted}>
+        <time dateTime={timestamp} aria-label={formatted}>
+          {relativeTime}
+        </time>
+      </Tooltip>
     );
   }
 
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index f99ccd39a63..e94df4c2757 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -16,6 +16,7 @@ import { MediaGallery, Video, Audio } from '../features/ui/util/async-components
 import { HotKeys } from 'react-hotkeys';
 import classNames from 'classnames';
 import Icon from 'mastodon/components/icon';
+import Tooltip from 'mastodon/components/tooltip';
 import { displayMedia } from '../initial_state';
 
 // We use the component (and not the container) since we do not want
@@ -424,13 +425,15 @@ class Status extends ImmutablePureComponent {
             <div className='status__info'>
               <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
 
-              <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
-                <div className='status__avatar'>
-                  {statusAvatar}
-                </div>
+              <Tooltip placement='top' overlay={status.getIn(['account', 'acct'])}>
+                <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
+                  <div className='status__avatar'>
+                    {statusAvatar}
+                  </div>
 
-                <DisplayName account={status.get('account')} others={otherAccounts} />
-              </a>
+                  <DisplayName account={status.get('account')} others={otherAccounts} />
+                </a>
+              </Tooltip>
             </div>
 
             <StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} showThread={showThread} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} />
diff --git a/app/javascript/mastodon/components/tooltip.js b/app/javascript/mastodon/components/tooltip.js
new file mode 100644
index 00000000000..16f3e78aab1
--- /dev/null
+++ b/app/javascript/mastodon/components/tooltip.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Tooltip from 'rc-tooltip';
+
+const OptionalTooltip = ({ overlay, children, ...other }) => {
+  if (overlay) {
+    return (
+      <Tooltip animation='zoom' mouseEnterDelay={0.2} destroyTooltipOnHide {...other} overlay={overlay}>
+        {children}
+      </Tooltip>
+    );
+  } else {
+    return children;
+  }
+};
+
+OptionalTooltip.propTypes = {
+  children: PropTypes.node,
+  overlay: PropTypes.node,
+};
+
+export default OptionalTooltip;
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index 8c85bbc393d..9775fac8401 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -11,6 +11,7 @@ import Avatar from 'mastodon/components/avatar';
 import { shortNumberFormat } from 'mastodon/utils/numbers';
 import { NavLink } from 'react-router-dom';
 import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
+import Tooltip from 'mastodon/components/tooltip';
 
 const messages = defineMessages({
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@@ -293,19 +294,34 @@ class Header extends ImmutablePureComponent {
                       <dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
 
                       <dd className='verified'>
-                        <a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
-                          <Icon id='check' className='verified__mark' />
-                        </span></a>
+                        <Tooltip placement='top' overlay={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
+                          <a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'>
+                            <Icon id='check' className='verified__mark' />
+                          </a>
+                        </Tooltip>
+
                         <a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
                       </dd>
                     </dl>
                   ))}
                   {fields.map((pair, i) => (
                     <dl key={i}>
-                      <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
-
-                      <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
-                        {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
+                      <dt>
+                        <Tooltip placement='top' overlay={pair.get('name')}>
+                          <span dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} />
+                        </Tooltip>
+                      </dt>
+
+                      <dd className={pair.get('verified_at') && 'verified'}>
+                        {pair.get('verified_at') && (
+                          <Tooltip placement='top' overlay={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}>
+                            <Icon id='check' className='verified__mark' />
+                          </Tooltip>
+                        )}
+                        {' '}
+                        <Tooltip placement='top' overlay={pair.get('value_plain')}>
+                          <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
+                        </Tooltip>
                       </dd>
                     </dl>
                   ))}
@@ -316,17 +332,23 @@ class Header extends ImmutablePureComponent {
             </div>
 
             <div className='account__header__extra__links'>
-              <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
-                <strong>{shortNumberFormat(account.get('statuses_count'))}</strong> <FormattedMessage id='account.posts' defaultMessage='Toots' />
-              </NavLink>
-
-              <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
-                <strong>{shortNumberFormat(account.get('following_count'))}</strong> <FormattedMessage id='account.follows' defaultMessage='Follows' />
-              </NavLink>
-
-              <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
-                <strong>{shortNumberFormat(account.get('followers_count'))}</strong> <FormattedMessage id='account.followers' defaultMessage='Followers' />
-              </NavLink>
+              <Tooltip placement='bottom' overlay={intl.formatNumber(account.get('statuses_count'))}>
+                <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`}>
+                  <strong>{shortNumberFormat(account.get('statuses_count'))}</strong> <FormattedMessage id='account.posts' defaultMessage='Toots' />
+                </NavLink>
+              </Tooltip>
+
+              <Tooltip placement='bottom' overlay={intl.formatNumber(account.get('following_count'))}>
+                <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`}>
+                  <strong>{shortNumberFormat(account.get('following_count'))}</strong> <FormattedMessage id='account.follows' defaultMessage='Follows' />
+                </NavLink>
+              </Tooltip>
+
+              <Tooltip placement='bottom' overlay={intl.formatNumber(account.get('followers_count'))}>
+                <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`}>
+                  <strong>{shortNumberFormat(account.get('followers_count'))}</strong> <FormattedMessage id='account.followers' defaultMessage='Followers' />
+                </NavLink>
+              </Tooltip>
             </div>
           </div>
         </div>
diff --git a/app/javascript/mastodon/features/compose/components/autosuggest_account.js b/app/javascript/mastodon/features/compose/components/autosuggest_account.js
index 1451be0e646..b144a2c3b66 100644
--- a/app/javascript/mastodon/features/compose/components/autosuggest_account.js
+++ b/app/javascript/mastodon/features/compose/components/autosuggest_account.js
@@ -3,6 +3,7 @@ import Avatar from '../../../components/avatar';
 import DisplayName from '../../../components/display_name';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
+import Tooltip from 'mastodon/components/tooltip';
 
 export default class AutosuggestAccount extends ImmutablePureComponent {
 
@@ -14,10 +15,12 @@ export default class AutosuggestAccount extends ImmutablePureComponent {
     const { account } = this.props;
 
     return (
-      <div className='autosuggest-account' title={account.get('acct')}>
-        <div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div>
-        <DisplayName account={account} />
-      </div>
+      <Tooltip placement='top' overlay={account.get('acct')}>
+        <div className='autosuggest-account'>
+          <div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div>
+          <DisplayName account={account} />
+        </div>
+      </Tooltip>
     );
   }
 
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index a6186010b4f..ed61c904fac 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
@@ -7,6 +7,7 @@ import classNames from 'classnames';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import detectPassiveEvents from 'detect-passive-events';
 import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
+import Tooltip from 'mastodon/components/tooltip';
 
 const messages = defineMessages({
   emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
@@ -358,13 +359,15 @@ class EmojiPickerDropdown extends React.PureComponent {
 
     return (
       <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
-        <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
-          {button || <img
-            className={classNames('emojione', { 'pulse-loading': active && loading })}
-            alt='🙂'
-            src={`${assetHost}/emoji/1f602.svg`}
-          />}
-        </div>
+        <Tooltip placement='top' overlay={title}>
+          <div ref={this.setTargetRef} className='emoji-button' aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
+            {button || <img
+              className={classNames('emojione', { 'pulse-loading': active && loading })}
+              alt='🙂'
+              src={`${assetHost}/emoji/1f602.svg`}
+            />}
+          </div>
+        </Tooltip>
 
         <Overlay show={active} placement={placement} target={this.findTarget}>
           <EmojiPickerMenu
diff --git a/app/javascript/mastodon/features/compose/components/text_icon_button.js b/app/javascript/mastodon/features/compose/components/text_icon_button.js
index f0b1335386d..e9be26cfccc 100644
--- a/app/javascript/mastodon/features/compose/components/text_icon_button.js
+++ b/app/javascript/mastodon/features/compose/components/text_icon_button.js
@@ -1,5 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import Tooltip from 'mastodon/components/tooltip';
 
 const iconStyle = {
   height: null,
@@ -26,16 +27,17 @@ export default class TextIconButton extends React.PureComponent {
     const { label, title, active, ariaControls } = this.props;
 
     return (
-      <button
-        title={title}
-        aria-label={title}
-        className={`text-icon-button ${active ? 'active' : ''}`}
-        aria-expanded={active}
-        onClick={this.handleClick}
-        aria-controls={ariaControls} style={iconStyle}
-      >
-        {label}
-      </button>
+      <Tooltip placement='bottom' overlay={title}>
+        <button
+          aria-label={title}
+          className={`text-icon-button ${active ? 'active' : ''}`}
+          aria-expanded={active}
+          onClick={this.handleClick}
+          aria-controls={ariaControls} style={iconStyle}
+        >
+          {label}
+        </button>
+      </Tooltip>
     );
   }
 
diff --git a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
index 7073f76c2e8..bd83a034b06 100644
--- a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
+++ b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import { changeComposeSensitivity } from 'mastodon/actions/compose';
 import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
+import Tooltip from 'mastodon/components/tooltip';
 
 const messages = defineMessages({
   marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' },
@@ -37,19 +38,21 @@ class SensitiveButton extends React.PureComponent {
 
     return (
       <div className='compose-form__sensitive-button'>
-        <label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}>
-          <input
-            name='mark-sensitive'
-            type='checkbox'
-            checked={active}
-            onChange={onClick}
-            disabled={disabled}
-          />
-
-          <span className={classNames('checkbox', { active })} />
-
-          <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' />
-        </label>
+        <Tooltip placement='top' overlay={intl.formatMessage(active ? messages.marked : messages.unmarked)}>
+          <label className={classNames('icon-button', { active })} aria-label={intl.formatMessage(active ? messages.marked : messages.unmarked)}>
+            <input
+              name='mark-sensitive'
+              type='checkbox'
+              checked={active}
+              onChange={onClick}
+              disabled={disabled}
+            />
+
+            <span className={classNames('checkbox', { active })} />
+
+            <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' />
+          </label>
+        </Tooltip>
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index e2de8b0e6a2..133e15f4193 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -16,6 +16,7 @@ import { openModal } from 'mastodon/actions/modal';
 import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
 import { mascot } from '../../initial_state';
 import Icon from 'mastodon/components/icon';
+import Tooltip from 'mastodon/components/tooltip';
 import { logOut } from 'mastodon/utils/log_out';
 
 const messages = defineMessages({
@@ -97,21 +98,55 @@ class Compose extends React.PureComponent {
       const { columns } = this.props;
       header = (
         <nav className='drawer__header'>
-          <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' fixedWidth /></Link>
+          <Tooltip placement='bottom' overlay={intl.formatMessage(messages.start)}>
+            <Link to='/getting-started' className='drawer__tab' aria-label={intl.formatMessage(messages.start)}>
+              <Icon id='bars' fixedWidth />
+            </Link>
+          </Tooltip>
+
           {!columns.some(column => column.get('id') === 'HOME') && (
-            <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link>
+            <Tooltip placement='bottom' overlay={intl.formatMessage(messages.home_timeline)}>
+              <Link to='/timelines/home' className='drawer__tab' aria-label={intl.formatMessage(messages.home_timeline)}>
+                <Icon id='home' fixedWidth />
+              </Link>
+            </Tooltip>
           )}
+
           {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
-            <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link>
+            <Tooltip placement='bottom' overlay={intl.formatMessage(messages.notifications)}>
+              <Link to='/notifications' className='drawer__tab' aria-label={intl.formatMessage(messages.notifications)}>
+                <Icon id='bell' fixedWidth />
+              </Link>
+            </Tooltip>
           )}
+
           {!columns.some(column => column.get('id') === 'COMMUNITY') && (
-            <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link>
+            <Tooltip placement='bottom' overlay={intl.formatMessage(messages.community)}>
+              <Link to='/timelines/public/local' className='drawer__tab' aria-label={intl.formatMessage(messages.community)}>
+                <Icon id='users' fixedWidth />
+              </Link>
+            </Tooltip>
           )}
+
           {!columns.some(column => column.get('id') === 'PUBLIC') && (
-            <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
+            <Tooltip placement='bottom' overlay={intl.formatMessage(messages.public)}>
+              <Link to='/timelines/public' className='drawer__tab' aria-label={intl.formatMessage(messages.public)}>
+                <Icon id='globe' fixedWidth />
+              </Link>
+            </Tooltip>
           )}
-          <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
-          <a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' fixedWidth /></a>
+
+          <Tooltip placement='bottom' overlay={intl.formatMessage(messages.preferences)}>
+            <a href='/settings/preferences' className='drawer__tab' aria-label={intl.formatMessage(messages.preferences)}>
+              <Icon id='cog' fixedWidth />
+            </a>
+          </Tooltip>
+
+          <Tooltip placement='bottom' overlay={intl.formatMessage(messages.logout)}>
+            <a href='/auth/sign_out' className='drawer__tab' aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}>
+              <Icon id='sign-out' fixedWidth />
+            </a>
+          </Tooltip>
         </nav>
       );
     }
diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss
index 8ebc45b62d1..03f23cfcf0c 100644
--- a/app/javascript/styles/application.scss
+++ b/app/javascript/styles/application.scss
@@ -24,5 +24,6 @@
 @import 'mastodon/tables';
 @import 'mastodon/admin';
 @import 'mastodon/dashboard';
+@import 'mastodon/tooltips';
 @import 'mastodon/rtl';
 @import 'mastodon/accessibility';
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 5d725b908f3..340537dae2a 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1076,8 +1076,7 @@
 }
 
 .status__info .status__display-name {
-  display: block;
-  max-width: 100%;
+  display: inline-flex;
   padding-right: 25px;
 }
 
diff --git a/app/javascript/styles/mastodon/tooltips.scss b/app/javascript/styles/mastodon/tooltips.scss
new file mode 100644
index 00000000000..84eca211c49
--- /dev/null
+++ b/app/javascript/styles/mastodon/tooltips.scss
@@ -0,0 +1,233 @@
+//
+// Tooltips
+// --------------------------------------------------
+$font-size-base: 14px;
+$line-height-base: 1.5;
+$border-radius-base: 5px;
+$overlay-shadow: 0 0 4px rgba(0, 0, 0, 0.17);
+//** Tooltip text color
+$tooltip-color: #fff;
+//** Tooltip background color
+$tooltip-bg: #000;
+$tooltip-opacity: 0.9;
+
+//** Tooltip arrow width
+$tooltip-arrow-width: 5px;
+//** Tooltip distance with trigger
+$tooltip-distance: $tooltip-arrow-width + 4;
+//** Tooltip arrow color
+$tooltip-arrow-color: $tooltip-bg;
+
+// Base class
+.rc-tooltip {
+  position: absolute;
+  z-index: 1070;
+  display: block;
+  visibility: visible;
+  // remove left/top by yiminghe
+  // left: -9999px;
+  // top: -9999px;
+  font-size: $font-size-base;
+  line-height: $line-height-base;
+  font-weight: 500;
+  opacity: $tooltip-opacity;
+  will-change: opacity, transform;
+  pointer-events: none;
+
+  &-hidden {
+    display: none;
+  }
+
+  &-placement-top, &-placement-topLeft, &-placement-topRight {
+    padding: $tooltip-arrow-width 0 $tooltip-distance 0;
+  }
+  &-placement-right, &-placement-rightTop, &-placement-rightBottom {
+    padding: 0 $tooltip-arrow-width 0 $tooltip-distance;
+  }
+  &-placement-bottom, &-placement-bottomLeft, &-placement-bottomRight {
+    padding: $tooltip-distance 0 $tooltip-arrow-width 0;
+  }
+  &-placement-left, &-placement-leftTop, &-placement-leftBottom {
+    padding: 0 $tooltip-distance 0 $tooltip-arrow-width;
+  }
+}
+
+// Wrapper for the tooltip content
+.rc-tooltip-inner {
+  padding: 8px 12px;
+  color: $tooltip-color;
+  text-align: left;
+  text-decoration: none;
+  background-color: $tooltip-bg;
+  border-radius: $border-radius-base;
+  box-shadow: $overlay-shadow;
+  max-width: 290px;
+  box-sizing: border-box;
+  min-height: 34px;
+}
+
+// Arrows
+.rc-tooltip-arrow {
+  position: absolute;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+
+.rc-tooltip {
+  &-placement-top &-arrow,
+  &-placement-topLeft &-arrow,
+  &-placement-topRight &-arrow {
+    bottom: $tooltip-distance - $tooltip-arrow-width;
+    margin-left: -$tooltip-arrow-width;
+    border-width: $tooltip-arrow-width $tooltip-arrow-width 0;
+    border-top-color: $tooltip-arrow-color;
+  }
+
+  &-placement-top &-arrow {
+    left: 50%;
+  }
+
+  &-placement-topLeft &-arrow {
+    left: 15%;
+  }
+
+  &-placement-topRight &-arrow {
+    right: 15%;
+  }
+
+  &-placement-right &-arrow,
+  &-placement-rightTop &-arrow,
+  &-placement-rightBottom &-arrow {
+    left: $tooltip-distance - $tooltip-arrow-width;
+    margin-top: -$tooltip-arrow-width;
+    border-width: $tooltip-arrow-width $tooltip-arrow-width $tooltip-arrow-width 0;
+    border-right-color: $tooltip-arrow-color;
+  }
+
+  &-placement-right &-arrow {
+    top: 50%;
+  }
+
+  &-placement-rightTop &-arrow {
+    top: 15%;
+    margin-top: 0;
+  }
+
+  &-placement-rightBottom &-arrow {
+    bottom: 15%;
+  }
+
+  &-placement-left &-arrow,
+  &-placement-leftTop &-arrow,
+  &-placement-leftBottom &-arrow {
+    right: $tooltip-distance - $tooltip-arrow-width;
+    margin-top: -$tooltip-arrow-width;
+    border-width: $tooltip-arrow-width 0 $tooltip-arrow-width $tooltip-arrow-width;
+    border-left-color: $tooltip-arrow-color;
+  }
+
+  &-placement-left &-arrow {
+    top: 50%;
+  }
+
+  &-placement-leftTop &-arrow {
+    top: 15%;
+    margin-top: 0;
+  }
+
+  &-placement-leftBottom &-arrow {
+    bottom: 15%;
+  }
+
+  &-placement-bottom &-arrow,
+  &-placement-bottomLeft &-arrow,
+  &-placement-bottomRight &-arrow {
+    top: $tooltip-distance - $tooltip-arrow-width;
+    margin-left: -$tooltip-arrow-width;
+    border-width: 0 $tooltip-arrow-width $tooltip-arrow-width;
+    border-bottom-color: $tooltip-arrow-color;
+  }
+
+  &-placement-bottom &-arrow {
+    left: 50%;
+  }
+
+  &-placement-bottomLeft &-arrow {
+    left: 15%;
+  }
+
+  &-placement-bottomRight &-arrow {
+    right: 15%;
+  }
+
+  @mixin effect(){
+    animation-duration: 50ms;
+    animation-fill-mode: both;
+  }
+
+  & {
+    &-zoom-enter,
+    &-zoom-leave {
+      display: block;
+    }
+  }
+
+  &-zoom-enter,
+  &-zoom-appear {
+    opacity: 0;
+    @include effect();
+    animation-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28);
+    animation-play-state: paused;
+  }
+
+  &-zoom-leave {
+    @include effect();
+    animation-timing-function: cubic-bezier(0.6, -0.3, 0.74, 0.05);
+    animation-play-state: paused;
+  }
+
+  &-zoom-enter,
+  &-zoom-appear {
+    &-active {
+      animation-name: rcToolTipZoomIn;
+      animation-play-state: running;
+    }
+  }
+
+  &-zoom-leave {
+    &-active {
+      animation-name: rcToolTipZoomOut;
+      animation-play-state: running;
+    }
+  }
+
+  @keyframes rcToolTipZoomIn {
+    0% {
+      opacity: 0;
+      transform-origin: 50% 50%;
+      transform: scale(0, 0);
+    }
+
+    100% {
+      opacity: 1;
+      transform-origin: 50% 50%;
+      transform: scale(1, 1);
+    }
+  }
+
+  @keyframes rcToolTipZoomOut {
+    0% {
+      opacity: 1;
+      transform-origin: 50% 50%;
+      transform: scale(1, 1);
+    }
+
+    100% {
+      opacity: 0;
+      transform-origin: 50% 50%;
+      transform: scale(0, 0);
+    }
+  }
+}
diff --git a/package.json b/package.json
index 65be5a0410e..9b162677dad 100644
--- a/package.json
+++ b/package.json
@@ -124,6 +124,7 @@
     "promise.prototype.finally": "^3.1.2",
     "prop-types": "^15.5.10",
     "punycode": "^2.1.0",
+    "rc-tooltip": "^4.2.1",
     "react": "^16.13.1",
     "react-dom": "^16.13.1",
     "react-hotkeys": "^1.1.4",
diff --git a/yarn.lock b/yarn.lock
index 06b9c5b189c..73e8fb80590 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,11 @@
 # yarn lockfile v1
 
 
+"@ant-design/css-animation@^1.7.2":
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/@ant-design/css-animation/-/css-animation-1.7.2.tgz#4ee5d2ec0fb7cc0a78b44e1c82628bd4621ac7e3"
+  integrity sha512-bvVOe7A+r7lws58B7r+fgnQDK90cV45AXuvGx6i5CCSX1W/M3AJnHsNggDANBxEtWdNdFWcDd5LorB+RdSIlBw==
+
 "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.1":
   version "7.10.1"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.1.tgz#d5481c5095daa1c57e16e54c6f9198443afb49ff"
@@ -988,7 +993,7 @@
   dependencies:
     regenerator-runtime "^0.12.0"
 
-"@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
   version "7.10.2"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839"
   integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==
@@ -2840,7 +2845,7 @@ class-utils@^0.3.5:
     isobject "^3.0.0"
     static-extend "^0.1.1"
 
-classnames@^2.2.5:
+classnames@2.x, classnames@^2.2.5, classnames@^2.2.6:
   version "2.2.6"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
   integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
@@ -3744,6 +3749,11 @@ doctrine@^3.0.0:
   dependencies:
     esutils "^2.0.2"
 
+dom-align@^1.7.0:
+  version "1.12.0"
+  resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.0.tgz#56fb7156df0b91099830364d2d48f88963f5a29c"
+  integrity sha512-YkoezQuhp3SLFGdOlr5xkqZ640iXrnHAwVYcDg8ZKRUtO7mSzSC2BA5V0VuyAwPSJA4CLIc6EDDJh4bEsD2+zA==
+
 dom-helpers@^3.2.1, dom-helpers@^3.4.0:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
@@ -8769,7 +8779,7 @@ quote@^0.4.0:
   resolved "https://registry.yarnpkg.com/quote/-/quote-0.4.0.tgz#10839217f6c1362b89194044d29b233fd7f32f01"
   integrity sha1-EIOSF/bBNiuJGUBE0psjP9fzLwE=
 
-raf@^3.1.0, raf@^3.4.1:
+raf@^3.1.0, raf@^3.4.0, raf@^3.4.1:
   version "3.4.1"
   resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
   integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
@@ -8819,6 +8829,54 @@ raw-body@2.4.0:
     iconv-lite "0.4.24"
     unpipe "1.0.0"
 
+rc-align@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.0.tgz#7a5b212051bdd840b406a6ad547076534a843691"
+  integrity sha512-0mKKfiZGo7VNiRCmnI4MTOG72pBFF0H08zebqcJyXcAm2hgAqTUtvt4I0pjMHh1WdYg+iQDjowpB5X8mZTN2vw==
+  dependencies:
+    "@babel/runtime" "^7.10.1"
+    classnames "2.x"
+    dom-align "^1.7.0"
+    rc-util "^5.0.1"
+    resize-observer-polyfill "^1.5.1"
+
+rc-animate@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-3.1.0.tgz#051b689c2c7194e4c8ae016d32a0e5f9de6c8baa"
+  integrity sha512-8FsM+3B1H+0AyTyGggY6JyVldHTs1CyYT8CfTmG/nGHHXlecvSLeICJhcKgRLjUiQlctNnRtB1rwz79cvBVmrw==
+  dependencies:
+    "@ant-design/css-animation" "^1.7.2"
+    classnames "^2.2.6"
+    raf "^3.4.0"
+    rc-util "^5.0.1"
+
+rc-tooltip@^4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-4.2.1.tgz#c1a2d5017ee03a771a9301c0dfdb46dfdf8fef94"
+  integrity sha512-oykuaGsHg7RFvPUaxUpxo7ScEqtH61C66x4JUmjlFlSS8gSx2L8JFtfwM1D68SLBxUqGqJObtxj4TED75gQTiA==
+  dependencies:
+    rc-trigger "^4.2.1"
+
+rc-trigger@^4.2.1:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-4.3.0.tgz#94ea1851d123359716d1dc3030083c015a92ecfb"
+  integrity sha512-jnGNzosXmDdivMBjPCYe/AfOXTpJU2/xQ9XukgoXDQEoZq/9lcI1r7eUIfq70WlWpLxlUEqQktiV3hwyy6Nw9g==
+  dependencies:
+    "@babel/runtime" "^7.10.1"
+    classnames "^2.2.6"
+    raf "^3.4.1"
+    rc-align "^4.0.0"
+    rc-animate "^3.0.0"
+    rc-util "^5.0.1"
+
+rc-util@^5.0.1:
+  version "5.0.4"
+  resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.0.4.tgz#297bd719b1bd00b3c947a884ab7ef0a07c55dce6"
+  integrity sha512-cd19RCrE0DJH6UcJ9+V3eaXA/5sNWyVKOKkWl8ZM2OqgNzVb8fv0obf/TkuvSN43tmTsgqY8k7OqpFYHhmef8g==
+  dependencies:
+    react-is "^16.12.0"
+    shallowequal "^1.1.0"
+
 react-dom@^16.13.1:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
@@ -9425,6 +9483,11 @@ reselect@^4.0.0:
   resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
   integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
 
+resize-observer-polyfill@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+  integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
 resolve-cwd@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@@ -9872,6 +9935,11 @@ shallow-equal@^1.2.1:
   resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da"
   integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==
 
+shallowequal@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
+  integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
+
 shebang-command@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
-- 
GitLab