Improve test freshness page interaction experience.
authordewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 11 Oct 2019 23:17:52 +0000 (23:17 +0000)
committerdewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 11 Oct 2019 23:17:52 +0000 (23:17 +0000)
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

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/public/shared/common-component-base.js
Websites/perf.webkit.org/public/v3/components/base.js
Websites/perf.webkit.org/public/v3/components/freshness-indicator.js
Websites/perf.webkit.org/public/v3/pages/test-freshness-page.js

index 3b19641..0218e65 100644 (file)
@@ -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
index 799113f..c56e2b0 100644 (file)
@@ -135,12 +135,12 @@ class CommonComponentBase {
             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 @@ class CommonComponentBase {
             title: title,
         };
 
+        if (tabIndex)
+            attributes['tabindex'] = tabIndex;
+
         if (typeof(callback) === 'string')
             attributes['href'] = callback;
         else
index f3a5e65..1fd79e0 100644 (file)
@@ -249,12 +249,14 @@ class ComponentBase extends CommonComponentBase {
         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);
         };
     }
index 4fbb920..a04784b 100644 (file)
@@ -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 @@ class FreshnessIndicator extends ComponentBase {
         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);
index f092ddb..72c83d6 100644 (file)
@@ -9,27 +9,22 @@ class TestFreshnessPage extends PageWithHeading {
         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 @@ class TestFreshnessPage extends PageWithHeading {
         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 @@ class TestFreshnessPage extends PageWithHeading {
 
         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 @@ class TestFreshnessPage extends PageWithHeading {
 
         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 @@ class TestFreshnessPage extends PageWithHeading {
                 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 @@ class TestFreshnessPage extends PageWithHeading {
             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 @@ class TestFreshnessPage extends PageWithHeading {
     _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 @@ class TestFreshnessPage extends PageWithHeading {
             && 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 @@ class TestFreshnessPage extends PageWithHeading {
                 height: calc(100vh - 24rem);
             }
             #test-health td.status-cell {
+                position: relative;
                 margin: 0;
                 padding: 0;
                 max-width: 2.2rem;
@@ -302,6 +465,24 @@ class TestFreshnessPage extends PageWithHeading {
                 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 @@ class TestFreshnessPage extends PageWithHeading {
             #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 @@ class TestFreshnessPage extends PageWithHeading {
                 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