Improve test freshness page interaction experience.
https://bugs.webkit.org/show_bug.cgi?id=202684

Reviewed by Ryosuke Niwa.

Change test freshness page show tooltip on click instead of popuping on mouse hover.
And clicking anywhere in 'page-with-heading' section except the tooltip can dismiss tooltip.
Add keyboard support to move focus around including 'Tab' key support.
Add support to use 'Enter' key to show or dismiss tooltip.
Add support to use 'Escape' key to dismiss tooltip.

* public/shared/common-component-base.js: Added support for link to specify 'tabindex'.
(CommonComponentBase.prototype.createLink):
(CommonComponentBase.createLink):
(CommonComponentBase):
* public/v3/components/base.js: Added support for customizing whether or not prevent default and stop propagation
while creating event handler.
(ComponentBase.prototype.createEventHandler):
(ComponentBase.createEventHandler):
(ComponentBase):
* public/v3/components/freshness-indicator.js:
(FreshnessIndicator): Removed 'url' property and removed customization for mouse event.
(FreshnessIndicator.prototype.update):
(FreshnessIndicator.prototype.didConstructShadowTree): Deleted.
* public/v3/pages/test-freshness-page.js:
(TestFreshnessPage): Changed to show tooltip on click and added key board event.
(TestFreshnessPage.prototype.didConstructShadowTree): Added key event support.
(TestFreshnessPage.prototype._findClosestIndicatorAnchorForCoordinate):
(TestFreshnessPage.prototype.render):
(TestFreshnessPage.prototype._renderTooltip):
(TestFreshnessPage.prototype._constructTableCell): Added tabIndex for each cell that contains freshness indicator.
(TestFreshnessPage.prototype._configureAnchorForIndicator):
(TestFreshnessPage.prototype._clearIndicatorState): Changed the color of links in tooltip to a more readable color.
Added styles when anchor for status cell and links on tooltip are focused.
(TestFreshnessPage.cssTemplate):

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@251028 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Websites/perf.webkit.org/ChangeLog b/Websites/perf.webkit.org/ChangeLog
index 3b19641..0218e65 100644
--- a/Websites/perf.webkit.org/ChangeLog
+++ b/Websites/perf.webkit.org/ChangeLog
@@ -1,3 +1,41 @@
+2019-10-11  Dewei Zhu  <dewei_zhu@apple.com>
+
+        Improve test freshness page interaction experience.
+        https://bugs.webkit.org/show_bug.cgi?id=202684
+
+        Reviewed by Ryosuke Niwa.
+
+        Change test freshness page show tooltip on click instead of popuping on mouse hover.
+        And clicking anywhere in 'page-with-heading' section except the tooltip can dismiss tooltip.
+        Add keyboard support to move focus around including 'Tab' key support.
+        Add support to use 'Enter' key to show or dismiss tooltip.
+        Add support to use 'Escape' key to dismiss tooltip.
+
+        * public/shared/common-component-base.js: Added support for link to specify 'tabindex'.
+        (CommonComponentBase.prototype.createLink):
+        (CommonComponentBase.createLink):
+        (CommonComponentBase):
+        * public/v3/components/base.js: Added support for customizing whether or not prevent default and stop propagation
+        while creating event handler.
+        (ComponentBase.prototype.createEventHandler):
+        (ComponentBase.createEventHandler):
+        (ComponentBase):
+        * public/v3/components/freshness-indicator.js:
+        (FreshnessIndicator): Removed 'url' property and removed customization for mouse event.
+        (FreshnessIndicator.prototype.update):
+        (FreshnessIndicator.prototype.didConstructShadowTree): Deleted.
+        * public/v3/pages/test-freshness-page.js:
+        (TestFreshnessPage): Changed to show tooltip on click and added key board event.
+        (TestFreshnessPage.prototype.didConstructShadowTree): Added key event support.
+        (TestFreshnessPage.prototype._findClosestIndicatorAnchorForCoordinate):
+        (TestFreshnessPage.prototype.render):
+        (TestFreshnessPage.prototype._renderTooltip):
+        (TestFreshnessPage.prototype._constructTableCell): Added tabIndex for each cell that contains freshness indicator.
+        (TestFreshnessPage.prototype._configureAnchorForIndicator):
+        (TestFreshnessPage.prototype._clearIndicatorState): Changed the color of links in tooltip to a more readable color.
+        Added styles when anchor for status cell and links on tooltip are focused.
+        (TestFreshnessPage.cssTemplate):
+
 2019-10-04  Zhifei Fang  <zhifei_fang@apple.com>
 
         [perf dashboard] Test fressness popover sometimes point to wrong place
diff --git a/Websites/perf.webkit.org/public/shared/common-component-base.js b/Websites/perf.webkit.org/public/shared/common-component-base.js
index 799113f..c56e2b0 100644
--- a/Websites/perf.webkit.org/public/shared/common-component-base.js
+++ b/Websites/perf.webkit.org/public/shared/common-component-base.js
@@ -135,12 +135,12 @@
             element.appendChild(CommonComponentBase._context.createTextNode(content));
     }
 
-    createLink(content, titleOrCallback, callback, isExternal)
+    createLink(content, titleOrCallback, callback, isExternal, tabIndex=null)
     {
-        return CommonComponentBase.createLink(content, titleOrCallback, callback, isExternal);
+        return CommonComponentBase.createLink(content, titleOrCallback, callback, isExternal, tabIndex);
     }
 
-    static createLink(content, titleOrCallback, callback, isExternal)
+    static createLink(content, titleOrCallback, callback, isExternal, tabIndex=null)
     {
         var title = titleOrCallback;
         if (callback === undefined) {
@@ -153,6 +153,9 @@
             title: title,
         };
 
+        if (tabIndex)
+            attributes['tabindex'] = tabIndex;
+
         if (typeof(callback) === 'string')
             attributes['href'] = callback;
         else
diff --git a/Websites/perf.webkit.org/public/v3/components/base.js b/Websites/perf.webkit.org/public/v3/components/base.js
index f3a5e65..1fd79e0 100644
--- a/Websites/perf.webkit.org/public/v3/components/base.js
+++ b/Websites/perf.webkit.org/public/v3/components/base.js
@@ -249,12 +249,14 @@
         customElements.define(name, elementClass);
     }
 
-    createEventHandler(callback) { return ComponentBase.createEventHandler(callback); }
-    static createEventHandler(callback)
+    createEventHandler(callback, options={}) { return ComponentBase.createEventHandler(callback, options); }
+    static createEventHandler(callback, options={})
     {
         return function (event) {
-            event.preventDefault();
-            event.stopPropagation();
+            if (!('preventDefault' in options) || options['preventDefault'])
+                event.preventDefault();
+            if (!('stopPropagation' in options) || options['stopPropagation'])
+                event.stopPropagation();
             callback.call(this, event);
         };
     }
diff --git a/Websites/perf.webkit.org/public/v3/components/freshness-indicator.js b/Websites/perf.webkit.org/public/v3/components/freshness-indicator.js
index 4fbb920..a04784b 100644
--- a/Websites/perf.webkit.org/public/v3/components/freshness-indicator.js
+++ b/Websites/perf.webkit.org/public/v3/components/freshness-indicator.js
@@ -1,31 +1,22 @@
 class FreshnessIndicator extends ComponentBase {
-    constructor(lastDataPointDuration, testAgeTolerance, summary, url)
+    constructor(lastDataPointDuration, testAgeTolerance, summary)
     {
         super('freshness-indicator');
         this._lastDataPointDuration = lastDataPointDuration;
         this._testAgeTolerance = testAgeTolerance;
-        this._url = url;
         this._highlighted = false;
 
         this._renderIndicatorLazily = new LazilyEvaluatedFunction(this._renderIndicator.bind(this));
     }
 
-    update(lastDataPointDuration, testAgeTolerance, url, highlighted)
+    update(lastDataPointDuration, testAgeTolerance, highlighted)
     {
         this._lastDataPointDuration = lastDataPointDuration;
         this._testAgeTolerance = testAgeTolerance;
-        this._url = url;
         this._highlighted = highlighted;
         this.enqueueToRender();
     }
 
-    didConstructShadowTree()
-    {
-        const container = this.content('container');
-        container.addEventListener('mouseenter', () => this.dispatchAction('select', this));
-        container.addEventListener('mouseleave', () => this.dispatchAction('unselect'));
-    }
-
     render()
     {
         super.render();
@@ -46,7 +37,7 @@
         const rating = 1 / (1 + Math.exp(Math.log(1.2) * (hoursSinceLastDataPoint - testAgeToleranceInHours)));
         const hue = Math.round(120 * rating);
         const brightness = Math.round(30 + 50 * rating);
-        const indicator = element('a', {id: 'cell', href: url, class: highlighted ? 'highlight' : ''});
+        const indicator = element('a', {id: 'cell', class: `${highlighted ? 'highlight' : ''}`});
 
         indicator.style.backgroundColor = `hsl(${hue}, 100%, ${brightness}%)`;
         this.renderReplace(this.content('container'), indicator);
diff --git a/Websites/perf.webkit.org/public/v3/pages/test-freshness-page.js b/Websites/perf.webkit.org/public/v3/pages/test-freshness-page.js
index f092ddb..72c83d6 100644
--- a/Websites/perf.webkit.org/public/v3/pages/test-freshness-page.js
+++ b/Websites/perf.webkit.org/public/v3/pages/test-freshness-page.js
@@ -9,27 +9,22 @@
         this._lastDataPointByConfiguration = null;
         this._indicatorByConfiguration = null;
         this._renderTableLazily = new LazilyEvaluatedFunction(this._renderTable.bind(this));
-        this._currentlyHighlightedIndicator = null;
-        this._hoveringTooltip = false;
+        this._hoveringIndicator = null;
+        this._indicatorForTooltip = null;
+        this._firstIndicatorAnchor = null;
+        this._showTooltip = false;
         this._builderByIndicator = null;
+        this._tabIndexForIndicator = null;
+        this._coordinateForIndicator = null;
+        this._indicatorAnchorGrid = null;
+        this._skipNextClick = false;
+        this._skipNextStateCleanOnScroll = false;
+        this._lastFocusedCell = null;
         this._renderTooltipLazily = new LazilyEvaluatedFunction(this._renderTooltip.bind(this));
 
         this._loadConfig(summaryPageConfiguration);
     }
 
-    didConstructShadowTree()
-    {
-        const tooltipTable = this.content('tooltip-table');
-        tooltipTable.addEventListener('mouseenter', () => {
-            this._hoveringTooltip = true;
-            this.enqueueToRender();
-        });
-        tooltipTable.addEventListener('mouseleave', () => {
-            this._hoveringTooltip = false;
-            this.enqueueToRender();
-        });
-    }
-
     name() { return 'Test-Freshness'; }
 
     _loadConfig(summaryPageConfiguration)
@@ -68,6 +63,86 @@
         super.open(state);
     }
 
+    didConstructShadowTree()
+    {
+        super.didConstructShadowTree();
+
+        const tooltipTable = this.content('tooltip-table');
+        this.content().addEventListener('click', (event) => {
+            if (!tooltipTable.contains(event.target))
+                this._clearIndicatorState(false);
+        });
+
+        tooltipTable.onkeydown = this.createEventHandler((event) => {
+            if (event.code == 'Escape') {
+                event.preventDefault();
+                event.stopPropagation();
+                this._lastFocusedCell.focus({preventScroll: true});
+            }
+        }, {preventDefault: false, stopPropagation: false});
+
+        window.addEventListener('keydown', (event) => {
+            if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.code))
+                return;
+
+            event.preventDefault();
+            if (!this._indicatorForTooltip && !this._hoveringIndicator) {
+                if (this._firstIndicatorAnchor)
+                    this._firstIndicatorAnchor.focus({preventScroll: true});
+                return;
+            }
+
+            let [row, column] = this._coordinateForIndicator.get(this._indicatorForTooltip || this._hoveringIndicator);
+            let direction = null;
+
+            switch (event.code) {
+                case 'ArrowUp':
+                    row -= 1;
+                    break;
+                case 'ArrowDown':
+                    row += 1;
+                    break;
+                case 'ArrowLeft':
+                    column -= 1;
+                    direction = 'leftOnly'
+                    break;
+                case 'ArrowRight':
+                    column += 1;
+                    direction = 'rightOnly'
+            }
+
+            const closestIndicatorAnchor = this._findClosestIndicatorAnchorForCoordinate(column, row, this._indicatorAnchorGrid, direction);
+            if (closestIndicatorAnchor)
+                closestIndicatorAnchor.focus({preventScroll: true});
+        });
+    }
+
+    _findClosestIndicatorAnchorForCoordinate(columnIndex, rowIndex, grid, direction)
+    {
+        rowIndex = Math.min(Math.max(rowIndex, 0), grid.length - 1);
+        const row = grid[rowIndex];
+        if (!row.length)
+            return null;
+
+        const start = Math.min(Math.max(columnIndex, 0), row.length - 1);
+        if (row[start])
+            return row[start];
+
+        let offset = 1;
+        while (true) {
+            const leftIndex = start - offset;
+            if (leftIndex >= 0 && row[leftIndex] && direction != 'rightOnly')
+                return row[leftIndex];
+            const rightIndex = start + offset;
+            if (rightIndex < row.length && row[rightIndex] && direction != 'leftOnly')
+                return row[rightIndex];
+            if (leftIndex < 0 && rightIndex >= row.length)
+                break;
+            offset += 1;
+        }
+        return null;
+    }
+
     _fetchTestResults()
     {
         this._measurementSetFetchTime = Date.now();
@@ -123,36 +198,45 @@
 
         this._renderTableLazily.evaluate(this._platforms, this._metrics);
 
-        let buildSummaryForCurrentlyHighlightedIndicator = null;
-        let buildForCurrentlyHighlightedIndicator = null;
-        let commitSetForCurrentHighlightedIndicator = null;
-        const builderForCurrentlyHighlightedIndicator = this._currentlyHighlightedIndicator ? this._builderByIndicator.get(this._currentlyHighlightedIndicator) : null;
+        let buildSummaryForFocusingIndicator = null;
+        let buildForFocusingIndicator = null;
+        let commitSetForFocusingdIndicator = null;
+        let chartURLForFocusingIndicator = null;
+        let platformForFocusingIndicator = null;
+        let metricForFocusingIndicator = null;
+        const builderForFocusingIndicator = this._indicatorForTooltip ? this._builderByIndicator.get(this._indicatorForTooltip) : null;
+        const builderForHoveringIndicator = this._hoveringIndicator ? this._builderByIndicator.get(this._hoveringIndicator) : null;
         for (const [platform, lastDataPointByMetric] of this._lastDataPointByConfiguration.entries()) {
             for (const [metric, lastDataPoint] of lastDataPointByMetric.entries()) {
                 const timeDuration = this._measurementSetFetchTime - lastDataPoint.time;
                 const timeDurationSummaryPrefix = lastDataPoint.hasCurrentDataPoint ? '' : 'More than ';
                 const timeDurationSummary = BuildRequest.formatTimeInterval(timeDuration);
                 const summary = `${timeDurationSummaryPrefix}${timeDurationSummary} since latest data point.`;
-                const url = this._router.url('charts', ChartsPage.createStateForDashboardItem(platform.id(), metric.id(),
-                    this._measurementSetFetchTime - this._timeDuration));
 
                 const indicator = this._indicatorByConfiguration.get(platform).get(metric);
-                if (this._currentlyHighlightedIndicator && this._currentlyHighlightedIndicator === indicator) {
-                    buildSummaryForCurrentlyHighlightedIndicator = summary;
-                    buildForCurrentlyHighlightedIndicator = lastDataPoint.lastBuild;
-                    commitSetForCurrentHighlightedIndicator = lastDataPoint.commitSetOfLastPoint;
+                if (this._indicatorForTooltip && this._indicatorForTooltip === indicator) {
+                    buildSummaryForFocusingIndicator = summary;
+                    buildForFocusingIndicator = lastDataPoint.lastBuild;
+                    commitSetForFocusingdIndicator = lastDataPoint.commitSetOfLastPoint;
+                    chartURLForFocusingIndicator =  this._router.url('charts', ChartsPage.createStateForDashboardItem(platform.id(), metric.id(),
+                        this._measurementSetFetchTime - this._timeDuration));
+                    platformForFocusingIndicator = platform;
+                    metricForFocusingIndicator = metric;
                 }
                 this._builderByIndicator.set(indicator, lastDataPoint.builder);
-                indicator.update(timeDuration, this._testAgeTolerance, url, builderForCurrentlyHighlightedIndicator && builderForCurrentlyHighlightedIndicator === lastDataPoint.builder);
+                const highlighted = builderForFocusingIndicator && builderForFocusingIndicator == lastDataPoint.builder
+                    || builderForHoveringIndicator && builderForHoveringIndicator === lastDataPoint.builder;
+                indicator.update(timeDuration, this._testAgeTolerance, highlighted);
             }
         }
-        this._renderTooltipLazily.evaluate(this._currentlyHighlightedIndicator, this._hoveringTooltip, buildSummaryForCurrentlyHighlightedIndicator, buildForCurrentlyHighlightedIndicator, commitSetForCurrentHighlightedIndicator);
+        this._renderTooltipLazily.evaluate(this._indicatorForTooltip, this._showTooltip, buildSummaryForFocusingIndicator, buildForFocusingIndicator,
+            commitSetForFocusingdIndicator, chartURLForFocusingIndicator, platformForFocusingIndicator, metricForFocusingIndicator, this._tabIndexForIndicator.get(this._indicatorForTooltip));
     }
 
-    _renderTooltip(indicator, hoveringTooltip, buildSummary, build, commitSet)
+    _renderTooltip(indicator, showTooltip, buildSummary, build, commitSet, chartURL, platform, metric, tabIndex)
     {
-        if (!indicator || !buildSummary) {
-            this.content('tooltip-anchor').style.display = hoveringTooltip ? null : 'none';
+        if (!indicator || !buildSummary || !showTooltip) {
+            this.content('tooltip-anchor').style.display =  showTooltip ? null : 'none';
             return;
         }
         const element = ComponentBase.createElement;
@@ -166,6 +250,14 @@
 
         let tableContent = [element('tr', element('td', {colspan: 2}, buildSummary))];
 
+        if (chartURL) {
+            const linkDescription = `${metric.test().name()} on ${platform.name()}`;
+            tableContent.push(element('tr', [
+                element('td', 'Chart'),
+                element('td', {colspan: 2}, link(linkDescription, linkDescription, chartURL, true, tabIndex))
+            ]));
+        }
+
         if (commitSet) {
             if (commitSet.repositories().length)
                 tableContent.push(element('tr', element('th', {colspan: 2}, 'Latest build information')));
@@ -174,7 +266,7 @@
                 const commit = commitSet.commitForRepository(repository);
                 return element('tr', [
                     element('td', repository.name()),
-                    element('td', commit.url() ? link(commit.label(), commit.label(), commit.url(), true) : commit.label())
+                    element('td', commit.url() ? link(commit.label(), commit.label(), commit.url(), true, tabIndex) : commit.label())
                 ]);
             }));
         }
@@ -185,7 +277,7 @@
             tableContent.push(element('tr', [
                 element('td', 'Build'),
                 element('td', {colspan: 2}, [
-                    url ? link(buildNumber, build.label(), url, true) : buildNumber
+                    url ? link(buildNumber, build.label(), url, true, tabIndex) : buildNumber
                 ]),
             ]));
         }
@@ -196,21 +288,50 @@
     _renderTable(platforms, metrics)
     {
         const element = ComponentBase.createElement;
-        const tableBodyElement = [];
         const tableHeadElements = [element('th',  {class: 'table-corner row-head'}, 'Platform \\ Test')];
 
         for (const metric of metrics)
             tableHeadElements.push(element('th', {class: 'diagonal-head'}, element('div', metric.test().fullName())));
 
         this._indicatorByConfiguration = new Map;
-        for (const platform of platforms) {
+        this._coordinateForIndicator = new Map;
+        this._tabIndexForIndicator = new Map;
+        this._indicatorAnchorGrid = [];
+        this._firstIndicatorAnchor = null;
+        let currentTabIndex = 1;
+
+        const tableBodyElement = platforms.map((platform, rowIndex) =>  {
             const indicatorByMetric = new Map;
             this._indicatorByConfiguration.set(platform, indicatorByMetric);
-            tableBodyElement.push(element('tr',
-                [element('th', {class: 'row-head'}, platform.label()), ...metrics.map((metric) => this._constructTableCell(platform, metric, indicatorByMetric))]));
-        }
 
-        this.renderReplace(this.content('test-health'), [element('thead', tableHeadElements), element('tbody', tableBodyElement)]);
+            let indicatorAnchorsInCurrentRow = [];
+
+            const cells = metrics.map((metric, columnIndex) => {
+                const [cell, anchor, indicator] = this._constructTableCell(platform, metric, currentTabIndex);
+
+                indicatorAnchorsInCurrentRow.push(anchor);
+                if (!indicator)
+                    return cell;
+
+                indicatorByMetric.set(metric, indicator);
+                this._tabIndexForIndicator.set(indicator, currentTabIndex);
+                this._coordinateForIndicator.set(indicator, [rowIndex, columnIndex]);
+
+                ++currentTabIndex;
+                if (!this._firstIndicatorAnchor)
+                    this._firstIndicatorAnchor = anchor;
+                return cell;
+            });
+            this._indicatorAnchorGrid.push(indicatorAnchorsInCurrentRow);
+
+            const row = element('tr', [element('th', {class: 'row-head'}, platform.label()), ...cells]);
+            return row;
+        });
+
+        const tableBody = element('tbody', tableBodyElement);
+        tableBody.onscroll = this.createEventHandler(() => this._clearIndicatorState(true));
+
+        this.renderReplace(this.content('test-health'), [element('thead', tableHeadElements), tableBody]);
     }
 
     _isValidPlatformMetricCombination(platform, metric)
@@ -220,33 +341,74 @@
             && platform.hasMetric(metric);
     }
 
-    _constructTableCell(platform, metric, indicatorByMetric)
+    _constructTableCell(platform, metric, tabIndex)
     {
         const element = ComponentBase.createElement;
-
+        const link = ComponentBase.createLink;
         if (!this._isValidPlatformMetricCombination(platform, metric))
-            return element('td', {class: 'blank-cell'}, element('div'));
+            return [element('td', {class: 'blank-cell'}, element('div')), null, null];
 
         const indicator = new FreshnessIndicator;
-        indicator.listenToAction('select', (originator) => {
-            this._currentlyHighlightedIndicator = originator;
+        const anchor = link(indicator, '', () => {
+            if (this._skipNextClick) {
+                this._skipNextClick = false;
+                return;
+            }
+            this._showTooltip = !this._showTooltip;
+            this.enqueueToRender();
+        }, false, tabIndex);
+
+        const cell = element('td', {class: 'status-cell'}, anchor);
+        this._configureAnchorForIndicator(anchor, indicator);
+        return [cell, anchor, indicator];
+    }
+
+    _configureAnchorForIndicator(anchor, indicator)
+    {
+        anchor.onmouseover = this.createEventHandler(() => {
+            this._hoveringIndicator = indicator;
             this.enqueueToRender();
         });
-        indicator.listenToAction('unselect', () => {
-            this._currentlyHighlightedIndicator = null;
+        anchor.onmousedown = this.createEventHandler(() =>
+            this._skipNextClick = this._indicatorForTooltip != indicator, {preventDefault: false, stopPropagation: false});
+        anchor.onfocus = this.createEventHandler(() => {
+            this._showTooltip = this._indicatorForTooltip != indicator;
+            this._hoveringIndicator = indicator;
+            this._indicatorForTooltip = indicator;
+            this._lastFocusedCell = anchor;
+            this._skipNextStateCleanOnScroll = true;
             this.enqueueToRender();
         });
-        indicatorByMetric.set(metric, indicator);
-        return element('td', {class: 'status-cell'}, indicator);
+        anchor.onkeydown = this.createEventHandler((event) => {
+            if (event.code == 'Escape') {
+                event.preventDefault();
+                event.stopPropagation();
+                this._showTooltip = event.code == 'Enter' ? !this._showTooltip : false;
+                this.enqueueToRender();
+            }
+        }, {preventDefault: false, stopPropagation: false});
+    }
+
+    _clearIndicatorState(dueToScroll)
+    {
+        if (this._skipNextStateCleanOnScroll) {
+            this._skipNextStateCleanOnScroll = false;
+            if (dueToScroll)
+                return;
+        }
+        this._showTooltip = false;
+        this._indicatorForTooltip = null;
+        this._hoveringIndicator = null;
+        this.enqueueToRender();
     }
 
     static htmlTemplate()
     {
         return `<section class="page-with-heading">
+            <table id="test-health"></table>
             <div id="tooltip-anchor">
                 <table id="tooltip-table"></table>
             </div>
-            <table id="test-health"></table>
         </section>`;
     }
 
@@ -295,6 +457,7 @@
                 height: calc(100vh - 24rem);
             }
             #test-health td.status-cell {
+                position: relative;
                 margin: 0;
                 padding: 0;
                 max-width: 2.2rem;
@@ -302,6 +465,24 @@
                 min-width: 2.2rem;
                 min-height: 2.2rem;
             }
+            #test-health td.status-cell>a {
+                display: block;
+            }
+            #test-health td.status-cell>a:focus {
+                outline: none;
+            }
+            #test-health td.status-cell>a:focus::after {
+                position: absolute;
+                content: "";
+                bottom: -0.1rem;
+                left: 50%;
+                margin-left: -0.2rem;
+                height: 0rem;
+                border-width: 0.2rem;
+                border-style: solid;
+                border-color: transparent transparent red transparent;
+                outline: none;
+            }
             #test-health td.blank-cell {
                 margin: 0;
                 padding: 0;
@@ -349,8 +530,8 @@
             #tooltip-table {
                 position: absolute;
                 width: 22rem;
-                background-color: #34495E;
-                opacity: 0.9;
+                background-color: #696969;
+                opacity: 0.96;
                 margin: 0.3rem;
                 padding: 0.3rem;
                 border-radius: 0.4rem;
@@ -374,14 +555,18 @@
                 margin-left: -0.3rem;
                 border-width: 0.3rem;
                 border-style: solid;
-                border-color: #34495E transparent transparent transparent;
+                border-color: #696969 transparent transparent transparent;
             }
             #tooltip-table a {
-                color: #B03A2E;
+                color: white;
                 font-weight: bold;
             }
+            #tooltip-table a:focus {
+                background-color: #AAB7B8;
+                outline: none;
+            }
         `;
     }
 
     routeName() { return 'test-freshness'; }
-}
+}
\ No newline at end of file