Use material tooltips

Tooltips will be used in the future for other features, and this allows
existing plain tooltips to be shown as soon as the mouse enters the
element, without the long delay inherent to standard tooltips.

Bug: twpowertools:45
Change-Id: Ifa7bf1ee8db8da7afaf36b9d19448f5a0cdd4ebc
diff --git a/src/common/tooltip.js b/src/common/tooltip.js
new file mode 100644
index 0000000..6657c58
--- /dev/null
+++ b/src/common/tooltip.js
@@ -0,0 +1,62 @@
+import {MDCTooltip} from '@material/tooltip';
+
+const probCleanOrphanTooltips = 0.07;
+
+const currentTooltips = new Set();
+
+// For each tooltip, if the element which is being described by it no longer
+// exists, delete it.
+function cleanOrphanTooltips() {
+  return new Promise((res, rej) => {
+    for (const tooltip of currentTooltips) {
+      if (document.querySelector('[aria-describedby="' + tooltip.id + '"]') ===
+          null) {
+        currentTooltips.delete(tooltip);
+        tooltip.remove();
+      }
+    }
+    res();
+  });
+}
+
+export function createPlainTooltip(srcElement, label, initTooltip = true) {
+  if (srcElement.hasAttribute('aria-describedby')) {
+    let tooltip =
+        document.getElementById(srcElement.getAttribute('aria-describedby'));
+    if (tooltip !== null) tooltip.remove();
+  }
+
+  let tooltip = document.createElement('div');
+  let tooltipId;
+  do {
+    // Idea from: https://stackoverflow.com/a/44078785
+    let randomId =
+        Date.now().toString(36) + Math.random().toString(36).substring(2);
+    tooltipId = 'TWPT_tooltip_' + randomId;
+  } while (document.getElementById(tooltipId) !== null);
+  tooltip.id = tooltipId;
+  tooltip.classList.add('mdc-tooltip');
+  tooltip.setAttribute('role', 'tooltip');
+  tooltip.setAttribute('aria-hidden', 'true');
+
+  let surface = document.createElement('div');
+  surface.classList.add(
+      'mdc-tooltip__surface', 'mdc-tooltip__surface-animation');
+  surface.textContent = label;
+
+  tooltip.append(surface);
+
+  // In the Community Console we inject the tooltip into
+  // #default-acx-overlay-container, and in TW directly into the body.
+  var tooltipParent =
+      document.getElementById('default-acx-overlay-container') ?? document.body;
+  tooltipParent.append(tooltip);
+  currentTooltips.add(tooltip);
+
+  srcElement.setAttribute('aria-describedby', tooltipId);
+
+  if (Math.random() < probCleanOrphanTooltips) cleanOrphanTooltips();
+
+  if (initTooltip) return new MDCTooltip(tooltip);
+  return tooltip;
+}
diff --git a/src/contentScripts/communityConsole/autoRefresh.js b/src/contentScripts/communityConsole/autoRefresh.js
index a35a6f0..e5dc8c8 100644
--- a/src/contentScripts/communityConsole/autoRefresh.js
+++ b/src/contentScripts/communityConsole/autoRefresh.js
@@ -1,6 +1,9 @@
+import {MDCTooltip} from '@material/tooltip';
+
 import {CCApi} from '../../common/api.js';
 import {getAuthUser} from '../../common/communityConsoleUtils.js';
 import {isOptionEnabled} from '../../common/optionsUtils.js';
+import {createPlainTooltip} from '../../common/tooltip.js';
 
 import {createExtBadge} from './utils/common.js';
 
@@ -117,7 +120,8 @@
     var content = document.createElement('div');
     content.classList.add('TWPT-focus-content-wrapper');
 
-    var badge = createExtBadge();
+    let badge, badgeTooltip;
+    [badge, badgeTooltip] = createExtBadge();
 
     var message = document.createElement('div');
     message.classList.add('TWPT-message');
@@ -141,6 +145,7 @@
     snackbar.append(ac);
     pane.append(snackbar);
     document.getElementById('default-acx-overlay-container').append(pane);
+    new MDCTooltip(badgeTooltip);
     this.snackbar = snackbar;
   }
 
@@ -148,10 +153,6 @@
   createStatusIndicator(isSetUp) {
     var container = document.createElement('div');
     container.classList.add('TWPT-autorefresh-status-indicator-container');
-    var title = chrome.i18n.getMessage(
-        isSetUp ? 'inject_autorefresh_list_status_indicator_label_active' :
-                  'inject_autorefresh_list_status_indicator_label_disabled');
-    container.setAttribute('title', title);
 
     var indicator = document.createElement('div');
     indicator.classList.add(
@@ -160,19 +161,27 @@
                   'TWPT-autorefresh-status-indicator--disabled');
     indicator.textContent =
         isSetUp ? 'notifications_active' : 'notifications_off';
+    let label = chrome.i18n.getMessage(
+        isSetUp ? 'inject_autorefresh_list_status_indicator_label_active' :
+                  'inject_autorefresh_list_status_indicator_label_disabled');
+    let statusTooltip = createPlainTooltip(indicator, label, false);
 
-    var badge = createExtBadge();
+    let badge, badgeTooltip;
+    [badge, badgeTooltip] = createExtBadge();
 
     container.append(indicator, badge);
-    return container;
+    return [container, badgeTooltip, statusTooltip];
   }
 
   injectStatusIndicator(isSetUp) {
-    this.statusIndicator = this.createStatusIndicator(isSetUp);
+    let badgeTooltip, statusTooltip;
+    [this.statusIndicator, badgeTooltip, statusTooltip] = this.createStatusIndicator(isSetUp);
 
     var sortOptionsDiv = document.querySelector('ec-thread-list .sort-options');
     if (sortOptionsDiv) {
       sortOptionsDiv.prepend(this.statusIndicator);
+      new MDCTooltip(badgeTooltip);
+      new MDCTooltip(statusTooltip);
       return;
     }
 
diff --git a/src/contentScripts/communityConsole/avatars.js b/src/contentScripts/communityConsole/avatars.js
index b125a17..03d6bcb 100644
--- a/src/contentScripts/communityConsole/avatars.js
+++ b/src/contentScripts/communityConsole/avatars.js
@@ -3,6 +3,7 @@
 import {CCApi} from '../../common/api.js';
 import {parseUrl} from '../../common/commonUtils.js';
 import {isOptionEnabled} from '../../common/optionsUtils.js';
+import {createPlainTooltip} from '../../common/tooltip.js';
 
 import AvatarsDB from './utils/AvatarsDB.js'
 
@@ -322,14 +323,12 @@
 
           var avatarUrls = res.avatars;
 
+          let singleAvatar;
           if (res.state == 'private') {
-            var avatar = document.createElement('div');
-            avatar.classList.add('TWPT-avatar-private-placeholder');
-            avatar.textContent = 'person_off';
-            var label = chrome.i18n.getMessage(
-                'inject_threadlistavatars_private_thread_indicator_label');
-            avatar.setAttribute('title', label);
-            avatarsContainer.appendChild(avatar);
+            singleAvatar = document.createElement('div');
+            singleAvatar.classList.add('TWPT-avatar-private-placeholder');
+            singleAvatar.textContent = 'person_off';
+            avatarsContainer.appendChild(singleAvatar);
           } else {
             for (var i = 0; i < avatarUrls.length; ++i) {
               var avatar = document.createElement('div');
@@ -340,6 +339,12 @@
           }
 
           header.appendChild(avatarsContainer);
+
+          if (res.state == 'private') {
+            var label = chrome.i18n.getMessage(
+                'inject_threadlistavatars_private_thread_indicator_label');
+            createPlainTooltip(singleAvatar, label);
+          }
         })
         .catch(err => {
           console.error(
diff --git a/src/contentScripts/communityConsole/batchLock.js b/src/contentScripts/communityConsole/batchLock.js
index 20af6df..11b133a 100644
--- a/src/contentScripts/communityConsole/batchLock.js
+++ b/src/contentScripts/communityConsole/batchLock.js
@@ -1,4 +1,7 @@
+import {MDCTooltip} from '@material/tooltip';
+
 import {isOptionEnabled} from '../../common/optionsUtils.js';
+import {createPlainTooltip} from '../../common/tooltip.js';
 
 import {createExtBadge, removeChildNodes} from './utils/common.js';
 
@@ -118,11 +121,11 @@
     var clone = readToggle.cloneNode(true);
     clone.setAttribute('debugid', 'batchlock');
     clone.classList.add('TWPT-btn--with-badge');
-    clone.setAttribute('title', chrome.i18n.getMessage('inject_lockbtn'));
     clone.querySelector('material-icon').setAttribute('icon', 'lock');
     clone.querySelector('i.material-icon-i').textContent = 'lock';
 
-    var badge = createExtBadge();
+    let badge, badgeTooltip;
+    [badge, badgeTooltip] = createExtBadge();
     clone.append(badge);
 
     clone.addEventListener('click', () => {
@@ -137,6 +140,9 @@
     else
       readToggle.parentNode.insertBefore(
           clone, (readToggle.nextSibling || readToggle));
+
+    createPlainTooltip(clone, chrome.i18n.getMessage('inject_lockbtn'));
+    new MDCTooltip(badgeTooltip);
   },
   addButtonIfEnabled(readToggle) {
     isOptionEnabled('batchlock').then(isEnabled => {
diff --git a/src/contentScripts/communityConsole/darkMode.js b/src/contentScripts/communityConsole/darkMode.js
index d357bad..1b6c751 100644
--- a/src/contentScripts/communityConsole/darkMode.js
+++ b/src/contentScripts/communityConsole/darkMode.js
@@ -1,11 +1,13 @@
+import {MDCTooltip} from '@material/tooltip';
+
+import {createPlainTooltip} from '../../common/tooltip.js';
+
 import {createExtBadge} from './utils/common.js';
 
 export function injectDarkModeButton(rightControl, previousDarkModeOption) {
   var darkThemeSwitch = document.createElement('material-button');
   darkThemeSwitch.classList.add('TWPT-dark-theme', 'TWPT-btn--with-badge');
   darkThemeSwitch.setAttribute('button', '');
-  darkThemeSwitch.setAttribute(
-      'title', chrome.i18n.getMessage('inject_ccdarktheme_helper'));
 
   darkThemeSwitch.addEventListener('click', e => {
     chrome.storage.sync.get(null, currentOptions => {
@@ -29,13 +31,18 @@
   switchContent.appendChild(icon);
   darkThemeSwitch.appendChild(switchContent);
 
-  var badgeContent = createExtBadge();
+  let badgeContent, badgeTooltip;
+  [badgeContent, badgeTooltip] = createExtBadge();
 
   darkThemeSwitch.appendChild(badgeContent);
 
   rightControl.style.width =
       (parseInt(window.getComputedStyle(rightControl).width) + 58) + 'px';
   rightControl.insertAdjacentElement('afterbegin', darkThemeSwitch);
+
+  createPlainTooltip(
+      switchContent, chrome.i18n.getMessage('inject_ccdarktheme_helper'));
+  new MDCTooltip(badgeTooltip);
 }
 
 export function isDarkThemeOn(options) {
diff --git a/src/contentScripts/communityConsole/profileHistoryLink.js b/src/contentScripts/communityConsole/profileHistoryLink.js
index 7f2dbd7..7f06b3e 100644
--- a/src/contentScripts/communityConsole/profileHistoryLink.js
+++ b/src/contentScripts/communityConsole/profileHistoryLink.js
@@ -46,7 +46,8 @@
   var container = document.createElement('div');
   container.classList.add('TWPT-previous-posts');
 
-  var badge = createExtBadge();
+  let badge, badgeTooltip;
+  [badge, badgeTooltip] = createExtBadge();
   container.appendChild(badge);
 
   var linkContainer = document.createElement('div');
@@ -58,6 +59,7 @@
   container.appendChild(linkContainer);
 
   mainCardContent.appendChild(container);
+  new MDCTooltip(badgeTooltip);
 }
 
 export function injectPreviousPostsLinksIfEnabled(nameElement) {
diff --git a/src/contentScripts/communityConsole/utils/common.js b/src/contentScripts/communityConsole/utils/common.js
index ca452b3..67a8768 100644
--- a/src/contentScripts/communityConsole/utils/common.js
+++ b/src/contentScripts/communityConsole/utils/common.js
@@ -1,3 +1,5 @@
+import {createPlainTooltip} from '../../../common/tooltip.js';
+
 export function removeChildNodes(node) {
   while (node.firstChild) {
     node.removeChild(node.firstChild);
@@ -11,17 +13,18 @@
 }
 
 export function createExtBadge() {
-  var badge = document.createElement('div');
+  let badge = document.createElement('div');
   badge.classList.add('TWPT-badge');
-  badge.setAttribute(
-      'title', chrome.i18n.getMessage('inject_extension_badge_helper', [
-        chrome.i18n.getMessage('appName')
-      ]));
+  let badgeTooltip = createPlainTooltip(
+      badge,
+      chrome.i18n.getMessage(
+          'inject_extension_badge_helper', [chrome.i18n.getMessage('appName')]),
+      false);
 
-  var badgeI = document.createElement('i');
+  let badgeI = document.createElement('i');
   badgeI.classList.add('material-icon-i', 'material-icons-extended');
   badgeI.textContent = 'repeat';
 
   badge.append(badgeI);
-  return badge;
+  return [badge, badgeTooltip];
 }
diff --git a/src/contentScripts/profile.js b/src/contentScripts/profile.js
index 7bd6916..0bb1a7d 100644
--- a/src/contentScripts/profile.js
+++ b/src/contentScripts/profile.js
@@ -1,5 +1,6 @@
 import {escapeUsername} from '../common/communityConsoleUtils.js';
 import {getOptions} from '../common/optionsUtils.js';
+import {createPlainTooltip} from '../common/tooltip.js';
 
 import {getSearchUrl, injectPreviousPostsLinksUnifiedProfile} from './utilsCommon/unifiedProfiles.js';
 
@@ -51,10 +52,6 @@
 
       var badge = document.createElement('span');
       badge.classList.add('TWPT-badge');
-      badge.setAttribute(
-          'title', chrome.i18n.getMessage('inject_extension_badge_helper', [
-            chrome.i18n.getMessage('appName')
-          ]));
 
       var badgeImg = document.createElement('img');
       badgeImg.src =
@@ -73,6 +70,11 @@
 
       document.querySelector('.user-profile__user-details-container')
           .appendChild(links);
+
+      createPlainTooltip(
+          badge, chrome.i18n.getMessage('inject_extension_badge_helper', [
+            chrome.i18n.getMessage('appName')
+          ]));
     } else {
       console.error('[previousposts] Can\'t find username.');
     }
diff --git a/src/contentScripts/utilsCommon/unifiedProfiles.js b/src/contentScripts/utilsCommon/unifiedProfiles.js
index d3995ea..82bc9c3 100644
--- a/src/contentScripts/utilsCommon/unifiedProfiles.js
+++ b/src/contentScripts/utilsCommon/unifiedProfiles.js
@@ -1,5 +1,8 @@
+import {MDCTooltip} from '@material/tooltip';
+
 import {escapeUsername} from '../../common/communityConsoleUtils.js';
 import {isOptionEnabled} from '../../common/optionsUtils.js';
+import {createPlainTooltip} from '../../common/tooltip.js';
 import {createExtBadge} from '../communityConsole/utils/common.js';
 
 var authuser = (new URL(location.href)).searchParams.get('authuser') || '0';
@@ -34,16 +37,12 @@
   a.setAttribute(
       'data-stats-id', 'user-posts-link--tw-power-tools-by-avm99963');
 
-  let badge;
+  let badge, badgeTooltip;
   if (isCommunityConsole) {
-    badge = createExtBadge();
+    [badge, badgeTooltip] = createExtBadge();
   } else {
     badge = document.createElement('span');
     badge.classList.add('TWPT-badge');
-    badge.setAttribute(
-        'title', chrome.i18n.getMessage('inject_extension_badge_helper', [
-          chrome.i18n.getMessage('appName')
-        ]));
 
     var badgeImg = document.createElement('img');
     badgeImg.src =
@@ -68,9 +67,18 @@
   }
 
   userDetailsNode.parentNode.insertBefore(links, userDetailsNode.nextSibling);
+
+  if (isCommunityConsole)
+    new MDCTooltip(badgeTooltip);
+  else
+    createPlainTooltip(
+        badge, chrome.i18n.getMessage('inject_extension_badge_helper', [
+          chrome.i18n.getMessage('appName')
+        ]));
 }
 
-export function injectPreviousPostsLinksUnifiedProfileIfEnabled(isCommunityConsole) {
+export function injectPreviousPostsLinksUnifiedProfileIfEnabled(
+    isCommunityConsole) {
   isOptionEnabled('history').then(isEnabled => {
     if (isEnabled) injectPreviousPostsLinksUnifiedProfile(isCommunityConsole);
   });
diff --git a/src/injections/profileIndicator.js b/src/injections/profileIndicator.js
index 3b07089..94ce943 100644
--- a/src/injections/profileIndicator.js
+++ b/src/injections/profileIndicator.js
@@ -1,5 +1,6 @@
 import {CCApi} from '../common/api.js';
 import {escapeUsername} from '../common/communityConsoleUtils.js';
+import {createPlainTooltip} from '../common/tooltip.js';
 
 var CCProfileRegex =
     /^(?:https:\/\/support\.google\.com)?\/s\/community(?:\/forum\/[0-9]*)?\/user\/(?:[0-9]+)$/;
@@ -108,12 +109,6 @@
   if (options.numPosts) return document.querySelector('.num-posts-indicator');
   var dotContainer = document.createElement('div');
   dotContainer.classList.add('profile-indicator', 'profile-indicator--loading');
-  contentScriptRequest
-      .sendRequest({
-        'action': 'geti18nMessage',
-        'msg': 'inject_profileindicator_loading'
-      })
-      .then(string => dotContainer.setAttribute('title', string));
 
   var dotLink = document.createElement('a');
   dotLink.href = searchURL;
@@ -122,6 +117,13 @@
   dotContainer.appendChild(dotLink);
   sourceNode.parentNode.appendChild(dotContainer);
 
+  contentScriptRequest
+      .sendRequest({
+        'action': 'geti18nMessage',
+        'msg': 'inject_profileindicator_loading'
+      })
+      .then(string => createPlainTooltip(dotContainer, string));
+
   return dotContainer;
 }
 
@@ -133,12 +135,6 @@
   var numPostsContainer = document.createElement('div');
   numPostsContainer.classList.add(
       'num-posts-indicator', 'num-posts-indicator--loading');
-  contentScriptRequest
-      .sendRequest({
-        'action': 'geti18nMessage',
-        'msg': 'inject_profileindicator_loading'
-      })
-      .then(string => numPostsContainer.setAttribute('title', string));
 
   var numPostsSpan = document.createElement('span');
   numPostsSpan.classList.add('num-posts-indicator--num');
@@ -146,6 +142,14 @@
   numPostsContainer.appendChild(numPostsSpan);
   link.appendChild(numPostsContainer);
   sourceNode.parentNode.appendChild(link);
+
+  contentScriptRequest
+      .sendRequest({
+        'action': 'geti18nMessage',
+        'msg': 'inject_profileindicator_loading'
+      })
+      .then(string => createPlainTooltip(numPostsContainer, string));
+
   return numPostsContainer;
 }
 
@@ -235,7 +239,7 @@
                       })
                       .then(
                           string =>
-                              numPostsContainer.setAttribute('title', string));
+                              createPlainTooltip(numPostsContainer, string));
 
                 var numPosts = 0;
 
@@ -319,7 +323,7 @@
                 'action': 'geti18nMessage',
                 'msg': 'inject_profileindicator_' + OPi18n[OPStatus]
               })
-              .then(string => dotContainer.setAttribute('title', string));
+              .then(string => createPlainTooltip(dotContainer, string));
         })
         .catch(
             err => console.error(
diff --git a/src/mdc/index.js b/src/mdc/index.js
new file mode 100644
index 0000000..e39aaa1
--- /dev/null
+++ b/src/mdc/index.js
@@ -0,0 +1 @@
+import './styles.scss';
diff --git a/src/mdc/styles.scss b/src/mdc/styles.scss
new file mode 100644
index 0000000..22ecb8d
--- /dev/null
+++ b/src/mdc/styles.scss
@@ -0,0 +1 @@
+@use "@material/tooltip/styles";
diff --git a/src/static/css/autorefresh_list.css b/src/static/css/autorefresh_list.css
index a12540b..3dcf0c6 100644
--- a/src/static/css/autorefresh_list.css
+++ b/src/static/css/autorefresh_list.css
@@ -87,7 +87,6 @@
   display: flex;
   flex-direction: row;
   align-items: center;
-  cursor: help;
 }
 
 .TWPT-autorefresh-status-indicator {
@@ -101,6 +100,7 @@
   font-size: 17px;
   line-height: 24px;
   user-select: none;
+  cursor: help;
 }
 
 .TWPT-autorefresh-status-indicator--active {