Changeset 251028 in webkit
- Timestamp:
- Oct 11, 2019 4:17:52 PM (4 years ago)
- Location:
- trunk/Websites/perf.webkit.org
- Files:
-
- 5 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/Websites/perf.webkit.org/ChangeLog
r250760 r251028 1 2019-10-11 Dewei Zhu <dewei_zhu@apple.com> 2 3 Improve test freshness page interaction experience. 4 https://bugs.webkit.org/show_bug.cgi?id=202684 5 6 Reviewed by Ryosuke Niwa. 7 8 Change test freshness page show tooltip on click instead of popuping on mouse hover. 9 And clicking anywhere in 'page-with-heading' section except the tooltip can dismiss tooltip. 10 Add keyboard support to move focus around including 'Tab' key support. 11 Add support to use 'Enter' key to show or dismiss tooltip. 12 Add support to use 'Escape' key to dismiss tooltip. 13 14 * public/shared/common-component-base.js: Added support for link to specify 'tabindex'. 15 (CommonComponentBase.prototype.createLink): 16 (CommonComponentBase.createLink): 17 (CommonComponentBase): 18 * public/v3/components/base.js: Added support for customizing whether or not prevent default and stop propagation 19 while creating event handler. 20 (ComponentBase.prototype.createEventHandler): 21 (ComponentBase.createEventHandler): 22 (ComponentBase): 23 * public/v3/components/freshness-indicator.js: 24 (FreshnessIndicator): Removed 'url' property and removed customization for mouse event. 25 (FreshnessIndicator.prototype.update): 26 (FreshnessIndicator.prototype.didConstructShadowTree): Deleted. 27 * public/v3/pages/test-freshness-page.js: 28 (TestFreshnessPage): Changed to show tooltip on click and added key board event. 29 (TestFreshnessPage.prototype.didConstructShadowTree): Added key event support. 30 (TestFreshnessPage.prototype._findClosestIndicatorAnchorForCoordinate): 31 (TestFreshnessPage.prototype.render): 32 (TestFreshnessPage.prototype._renderTooltip): 33 (TestFreshnessPage.prototype._constructTableCell): Added tabIndex for each cell that contains freshness indicator. 34 (TestFreshnessPage.prototype._configureAnchorForIndicator): 35 (TestFreshnessPage.prototype._clearIndicatorState): Changed the color of links in tooltip to a more readable color. 36 Added styles when anchor for status cell and links on tooltip are focused. 37 (TestFreshnessPage.cssTemplate): 38 1 39 2019-10-04 Zhifei Fang <zhifei_fang@apple.com> 2 40 -
trunk/Websites/perf.webkit.org/public/shared/common-component-base.js
r232614 r251028 136 136 } 137 137 138 createLink(content, titleOrCallback, callback, isExternal )138 createLink(content, titleOrCallback, callback, isExternal, tabIndex=null) 139 139 { 140 return CommonComponentBase.createLink(content, titleOrCallback, callback, isExternal );140 return CommonComponentBase.createLink(content, titleOrCallback, callback, isExternal, tabIndex); 141 141 } 142 142 143 static createLink(content, titleOrCallback, callback, isExternal )143 static createLink(content, titleOrCallback, callback, isExternal, tabIndex=null) 144 144 { 145 145 var title = titleOrCallback; … … 153 153 title: title, 154 154 }; 155 156 if (tabIndex) 157 attributes['tabindex'] = tabIndex; 155 158 156 159 if (typeof(callback) === 'string') -
trunk/Websites/perf.webkit.org/public/v3/components/base.js
r233333 r251028 250 250 } 251 251 252 createEventHandler(callback ) { return ComponentBase.createEventHandler(callback); }253 static createEventHandler(callback )252 createEventHandler(callback, options={}) { return ComponentBase.createEventHandler(callback, options); } 253 static createEventHandler(callback, options={}) 254 254 { 255 255 return function (event) { 256 event.preventDefault(); 257 event.stopPropagation(); 256 if (!('preventDefault' in options) || options['preventDefault']) 257 event.preventDefault(); 258 if (!('stopPropagation' in options) || options['stopPropagation']) 259 event.stopPropagation(); 258 260 callback.call(this, event); 259 261 }; -
trunk/Websites/perf.webkit.org/public/v3/components/freshness-indicator.js
r250749 r251028 1 1 class FreshnessIndicator extends ComponentBase { 2 constructor(lastDataPointDuration, testAgeTolerance, summary , url)2 constructor(lastDataPointDuration, testAgeTolerance, summary) 3 3 { 4 4 super('freshness-indicator'); 5 5 this._lastDataPointDuration = lastDataPointDuration; 6 6 this._testAgeTolerance = testAgeTolerance; 7 this._url = url;8 7 this._highlighted = false; 9 8 … … 11 10 } 12 11 13 update(lastDataPointDuration, testAgeTolerance, url,highlighted)12 update(lastDataPointDuration, testAgeTolerance, highlighted) 14 13 { 15 14 this._lastDataPointDuration = lastDataPointDuration; 16 15 this._testAgeTolerance = testAgeTolerance; 17 this._url = url;18 16 this._highlighted = highlighted; 19 17 this.enqueueToRender(); 20 }21 22 didConstructShadowTree()23 {24 const container = this.content('container');25 container.addEventListener('mouseenter', () => this.dispatchAction('select', this));26 container.addEventListener('mouseleave', () => this.dispatchAction('unselect'));27 18 } 28 19 … … 47 38 const hue = Math.round(120 * rating); 48 39 const brightness = Math.round(30 + 50 * rating); 49 const indicator = element('a', {id: 'cell', href: url, class: highlighted ? 'highlight' : ''});40 const indicator = element('a', {id: 'cell', class: `${highlighted ? 'highlight' : ''}`}); 50 41 51 42 indicator.style.backgroundColor = `hsl(${hue}, 100%, ${brightness}%)`; -
trunk/Websites/perf.webkit.org/public/v3/pages/test-freshness-page.js
r250760 r251028 10 10 this._indicatorByConfiguration = null; 11 11 this._renderTableLazily = new LazilyEvaluatedFunction(this._renderTable.bind(this)); 12 this._currentlyHighlightedIndicator = null; 13 this._hoveringTooltip = false; 12 this._hoveringIndicator = null; 13 this._indicatorForTooltip = null; 14 this._firstIndicatorAnchor = null; 15 this._showTooltip = false; 14 16 this._builderByIndicator = null; 17 this._tabIndexForIndicator = null; 18 this._coordinateForIndicator = null; 19 this._indicatorAnchorGrid = null; 20 this._skipNextClick = false; 21 this._skipNextStateCleanOnScroll = false; 22 this._lastFocusedCell = null; 15 23 this._renderTooltipLazily = new LazilyEvaluatedFunction(this._renderTooltip.bind(this)); 16 24 17 25 this._loadConfig(summaryPageConfiguration); 18 }19 20 didConstructShadowTree()21 {22 const tooltipTable = this.content('tooltip-table');23 tooltipTable.addEventListener('mouseenter', () => {24 this._hoveringTooltip = true;25 this.enqueueToRender();26 });27 tooltipTable.addEventListener('mouseleave', () => {28 this._hoveringTooltip = false;29 this.enqueueToRender();30 });31 26 } 32 27 … … 67 62 this._fetchTestResults(); 68 63 super.open(state); 64 } 65 66 didConstructShadowTree() 67 { 68 super.didConstructShadowTree(); 69 70 const tooltipTable = this.content('tooltip-table'); 71 this.content().addEventListener('click', (event) => { 72 if (!tooltipTable.contains(event.target)) 73 this._clearIndicatorState(false); 74 }); 75 76 tooltipTable.onkeydown = this.createEventHandler((event) => { 77 if (event.code == 'Escape') { 78 event.preventDefault(); 79 event.stopPropagation(); 80 this._lastFocusedCell.focus({preventScroll: true}); 81 } 82 }, {preventDefault: false, stopPropagation: false}); 83 84 window.addEventListener('keydown', (event) => { 85 if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.code)) 86 return; 87 88 event.preventDefault(); 89 if (!this._indicatorForTooltip && !this._hoveringIndicator) { 90 if (this._firstIndicatorAnchor) 91 this._firstIndicatorAnchor.focus({preventScroll: true}); 92 return; 93 } 94 95 let [row, column] = this._coordinateForIndicator.get(this._indicatorForTooltip || this._hoveringIndicator); 96 let direction = null; 97 98 switch (event.code) { 99 case 'ArrowUp': 100 row -= 1; 101 break; 102 case 'ArrowDown': 103 row += 1; 104 break; 105 case 'ArrowLeft': 106 column -= 1; 107 direction = 'leftOnly' 108 break; 109 case 'ArrowRight': 110 column += 1; 111 direction = 'rightOnly' 112 } 113 114 const closestIndicatorAnchor = this._findClosestIndicatorAnchorForCoordinate(column, row, this._indicatorAnchorGrid, direction); 115 if (closestIndicatorAnchor) 116 closestIndicatorAnchor.focus({preventScroll: true}); 117 }); 118 } 119 120 _findClosestIndicatorAnchorForCoordinate(columnIndex, rowIndex, grid, direction) 121 { 122 rowIndex = Math.min(Math.max(rowIndex, 0), grid.length - 1); 123 const row = grid[rowIndex]; 124 if (!row.length) 125 return null; 126 127 const start = Math.min(Math.max(columnIndex, 0), row.length - 1); 128 if (row[start]) 129 return row[start]; 130 131 let offset = 1; 132 while (true) { 133 const leftIndex = start - offset; 134 if (leftIndex >= 0 && row[leftIndex] && direction != 'rightOnly') 135 return row[leftIndex]; 136 const rightIndex = start + offset; 137 if (rightIndex < row.length && row[rightIndex] && direction != 'leftOnly') 138 return row[rightIndex]; 139 if (leftIndex < 0 && rightIndex >= row.length) 140 break; 141 offset += 1; 142 } 143 return null; 69 144 } 70 145 … … 124 199 this._renderTableLazily.evaluate(this._platforms, this._metrics); 125 200 126 let buildSummaryForCurrentlyHighlightedIndicator = null; 127 let buildForCurrentlyHighlightedIndicator = null; 128 let commitSetForCurrentHighlightedIndicator = null; 129 const builderForCurrentlyHighlightedIndicator = this._currentlyHighlightedIndicator ? this._builderByIndicator.get(this._currentlyHighlightedIndicator) : null; 201 let buildSummaryForFocusingIndicator = null; 202 let buildForFocusingIndicator = null; 203 let commitSetForFocusingdIndicator = null; 204 let chartURLForFocusingIndicator = null; 205 let platformForFocusingIndicator = null; 206 let metricForFocusingIndicator = null; 207 const builderForFocusingIndicator = this._indicatorForTooltip ? this._builderByIndicator.get(this._indicatorForTooltip) : null; 208 const builderForHoveringIndicator = this._hoveringIndicator ? this._builderByIndicator.get(this._hoveringIndicator) : null; 130 209 for (const [platform, lastDataPointByMetric] of this._lastDataPointByConfiguration.entries()) { 131 210 for (const [metric, lastDataPoint] of lastDataPointByMetric.entries()) { … … 134 213 const timeDurationSummary = BuildRequest.formatTimeInterval(timeDuration); 135 214 const summary = `${timeDurationSummaryPrefix}${timeDurationSummary} since latest data point.`; 136 const url = this._router.url('charts', ChartsPage.createStateForDashboardItem(platform.id(), metric.id(),137 this._measurementSetFetchTime - this._timeDuration));138 215 139 216 const indicator = this._indicatorByConfiguration.get(platform).get(metric); 140 if (this._currentlyHighlightedIndicator && this._currentlyHighlightedIndicator === indicator) { 141 buildSummaryForCurrentlyHighlightedIndicator = summary; 142 buildForCurrentlyHighlightedIndicator = lastDataPoint.lastBuild; 143 commitSetForCurrentHighlightedIndicator = lastDataPoint.commitSetOfLastPoint; 217 if (this._indicatorForTooltip && this._indicatorForTooltip === indicator) { 218 buildSummaryForFocusingIndicator = summary; 219 buildForFocusingIndicator = lastDataPoint.lastBuild; 220 commitSetForFocusingdIndicator = lastDataPoint.commitSetOfLastPoint; 221 chartURLForFocusingIndicator = this._router.url('charts', ChartsPage.createStateForDashboardItem(platform.id(), metric.id(), 222 this._measurementSetFetchTime - this._timeDuration)); 223 platformForFocusingIndicator = platform; 224 metricForFocusingIndicator = metric; 144 225 } 145 226 this._builderByIndicator.set(indicator, lastDataPoint.builder); 146 indicator.update(timeDuration, this._testAgeTolerance, url, builderForCurrentlyHighlightedIndicator && builderForCurrentlyHighlightedIndicator === lastDataPoint.builder); 147 } 148 } 149 this._renderTooltipLazily.evaluate(this._currentlyHighlightedIndicator, this._hoveringTooltip, buildSummaryForCurrentlyHighlightedIndicator, buildForCurrentlyHighlightedIndicator, commitSetForCurrentHighlightedIndicator); 150 } 151 152 _renderTooltip(indicator, hoveringTooltip, buildSummary, build, commitSet) 153 { 154 if (!indicator || !buildSummary) { 155 this.content('tooltip-anchor').style.display = hoveringTooltip ? null : 'none'; 227 const highlighted = builderForFocusingIndicator && builderForFocusingIndicator == lastDataPoint.builder 228 || builderForHoveringIndicator && builderForHoveringIndicator === lastDataPoint.builder; 229 indicator.update(timeDuration, this._testAgeTolerance, highlighted); 230 } 231 } 232 this._renderTooltipLazily.evaluate(this._indicatorForTooltip, this._showTooltip, buildSummaryForFocusingIndicator, buildForFocusingIndicator, 233 commitSetForFocusingdIndicator, chartURLForFocusingIndicator, platformForFocusingIndicator, metricForFocusingIndicator, this._tabIndexForIndicator.get(this._indicatorForTooltip)); 234 } 235 236 _renderTooltip(indicator, showTooltip, buildSummary, build, commitSet, chartURL, platform, metric, tabIndex) 237 { 238 if (!indicator || !buildSummary || !showTooltip) { 239 this.content('tooltip-anchor').style.display = showTooltip ? null : 'none'; 156 240 return; 157 241 } … … 167 251 let tableContent = [element('tr', element('td', {colspan: 2}, buildSummary))]; 168 252 253 if (chartURL) { 254 const linkDescription = `${metric.test().name()} on ${platform.name()}`; 255 tableContent.push(element('tr', [ 256 element('td', 'Chart'), 257 element('td', {colspan: 2}, link(linkDescription, linkDescription, chartURL, true, tabIndex)) 258 ])); 259 } 260 169 261 if (commitSet) { 170 262 if (commitSet.repositories().length) … … 175 267 return element('tr', [ 176 268 element('td', repository.name()), 177 element('td', commit.url() ? link(commit.label(), commit.label(), commit.url(), true ) : commit.label())269 element('td', commit.url() ? link(commit.label(), commit.label(), commit.url(), true, tabIndex) : commit.label()) 178 270 ]); 179 271 })); … … 186 278 element('td', 'Build'), 187 279 element('td', {colspan: 2}, [ 188 url ? link(buildNumber, build.label(), url, true ) : buildNumber280 url ? link(buildNumber, build.label(), url, true, tabIndex) : buildNumber 189 281 ]), 190 282 ])); … … 197 289 { 198 290 const element = ComponentBase.createElement; 199 const tableBodyElement = [];200 291 const tableHeadElements = [element('th', {class: 'table-corner row-head'}, 'Platform \\ Test')]; 201 292 … … 204 295 205 296 this._indicatorByConfiguration = new Map; 206 for (const platform of platforms) { 297 this._coordinateForIndicator = new Map; 298 this._tabIndexForIndicator = new Map; 299 this._indicatorAnchorGrid = []; 300 this._firstIndicatorAnchor = null; 301 let currentTabIndex = 1; 302 303 const tableBodyElement = platforms.map((platform, rowIndex) => { 207 304 const indicatorByMetric = new Map; 208 305 this._indicatorByConfiguration.set(platform, indicatorByMetric); 209 tableBodyElement.push(element('tr', 210 [element('th', {class: 'row-head'}, platform.label()), ...metrics.map((metric) => this._constructTableCell(platform, metric, indicatorByMetric))])); 211 } 212 213 this.renderReplace(this.content('test-health'), [element('thead', tableHeadElements), element('tbody', tableBodyElement)]); 306 307 let indicatorAnchorsInCurrentRow = []; 308 309 const cells = metrics.map((metric, columnIndex) => { 310 const [cell, anchor, indicator] = this._constructTableCell(platform, metric, currentTabIndex); 311 312 indicatorAnchorsInCurrentRow.push(anchor); 313 if (!indicator) 314 return cell; 315 316 indicatorByMetric.set(metric, indicator); 317 this._tabIndexForIndicator.set(indicator, currentTabIndex); 318 this._coordinateForIndicator.set(indicator, [rowIndex, columnIndex]); 319 320 ++currentTabIndex; 321 if (!this._firstIndicatorAnchor) 322 this._firstIndicatorAnchor = anchor; 323 return cell; 324 }); 325 this._indicatorAnchorGrid.push(indicatorAnchorsInCurrentRow); 326 327 const row = element('tr', [element('th', {class: 'row-head'}, platform.label()), ...cells]); 328 return row; 329 }); 330 331 const tableBody = element('tbody', tableBodyElement); 332 tableBody.onscroll = this.createEventHandler(() => this._clearIndicatorState(true)); 333 334 this.renderReplace(this.content('test-health'), [element('thead', tableHeadElements), tableBody]); 214 335 } 215 336 … … 221 342 } 222 343 223 _constructTableCell(platform, metric, indicatorByMetric)344 _constructTableCell(platform, metric, tabIndex) 224 345 { 225 346 const element = ComponentBase.createElement; 226 347 const link = ComponentBase.createLink; 227 348 if (!this._isValidPlatformMetricCombination(platform, metric)) 228 return element('td', {class: 'blank-cell'}, element('div'));349 return [element('td', {class: 'blank-cell'}, element('div')), null, null]; 229 350 230 351 const indicator = new FreshnessIndicator; 231 indicator.listenToAction('select', (originator) => { 232 this._currentlyHighlightedIndicator = originator; 352 const anchor = link(indicator, '', () => { 353 if (this._skipNextClick) { 354 this._skipNextClick = false; 355 return; 356 } 357 this._showTooltip = !this._showTooltip; 358 this.enqueueToRender(); 359 }, false, tabIndex); 360 361 const cell = element('td', {class: 'status-cell'}, anchor); 362 this._configureAnchorForIndicator(anchor, indicator); 363 return [cell, anchor, indicator]; 364 } 365 366 _configureAnchorForIndicator(anchor, indicator) 367 { 368 anchor.onmouseover = this.createEventHandler(() => { 369 this._hoveringIndicator = indicator; 233 370 this.enqueueToRender(); 234 371 }); 235 indicator.listenToAction('unselect', () => { 236 this._currentlyHighlightedIndicator = null; 372 anchor.onmousedown = this.createEventHandler(() => 373 this._skipNextClick = this._indicatorForTooltip != indicator, {preventDefault: false, stopPropagation: false}); 374 anchor.onfocus = this.createEventHandler(() => { 375 this._showTooltip = this._indicatorForTooltip != indicator; 376 this._hoveringIndicator = indicator; 377 this._indicatorForTooltip = indicator; 378 this._lastFocusedCell = anchor; 379 this._skipNextStateCleanOnScroll = true; 237 380 this.enqueueToRender(); 238 381 }); 239 indicatorByMetric.set(metric, indicator); 240 return element('td', {class: 'status-cell'}, indicator); 382 anchor.onkeydown = this.createEventHandler((event) => { 383 if (event.code == 'Escape') { 384 event.preventDefault(); 385 event.stopPropagation(); 386 this._showTooltip = event.code == 'Enter' ? !this._showTooltip : false; 387 this.enqueueToRender(); 388 } 389 }, {preventDefault: false, stopPropagation: false}); 390 } 391 392 _clearIndicatorState(dueToScroll) 393 { 394 if (this._skipNextStateCleanOnScroll) { 395 this._skipNextStateCleanOnScroll = false; 396 if (dueToScroll) 397 return; 398 } 399 this._showTooltip = false; 400 this._indicatorForTooltip = null; 401 this._hoveringIndicator = null; 402 this.enqueueToRender(); 241 403 } 242 404 … … 244 406 { 245 407 return `<section class="page-with-heading"> 408 <table id="test-health"></table> 246 409 <div id="tooltip-anchor"> 247 410 <table id="tooltip-table"></table> 248 411 </div> 249 <table id="test-health"></table>250 412 </section>`; 251 413 } … … 296 458 } 297 459 #test-health td.status-cell { 460 position: relative; 298 461 margin: 0; 299 462 padding: 0; … … 302 465 min-width: 2.2rem; 303 466 min-height: 2.2rem; 467 } 468 #test-health td.status-cell>a { 469 display: block; 470 } 471 #test-health td.status-cell>a:focus { 472 outline: none; 473 } 474 #test-health td.status-cell>a:focus::after { 475 position: absolute; 476 content: ""; 477 bottom: -0.1rem; 478 left: 50%; 479 margin-left: -0.2rem; 480 height: 0rem; 481 border-width: 0.2rem; 482 border-style: solid; 483 border-color: transparent transparent red transparent; 484 outline: none; 304 485 } 305 486 #test-health td.blank-cell { … … 350 531 position: absolute; 351 532 width: 22rem; 352 background-color: # 34495E;353 opacity: 0.9 ;533 background-color: #696969; 534 opacity: 0.96; 354 535 margin: 0.3rem; 355 536 padding: 0.3rem; … … 375 556 border-width: 0.3rem; 376 557 border-style: solid; 377 border-color: # 34495Etransparent transparent transparent;558 border-color: #696969 transparent transparent transparent; 378 559 } 379 560 #tooltip-table a { 380 color: #B03A2E;561 color: white; 381 562 font-weight: bold; 563 } 564 #tooltip-table a:focus { 565 background-color: #AAB7B8; 566 outline: none; 382 567 } 383 568 `;
Note: See TracChangeset
for help on using the changeset viewer.