chrome-devtools-frontend 1.0.1667564 → 1.0.1668390

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.agents/skills/devtools-setting-migration/SKILL.md +290 -0
  2. package/.agents/skills/ui-eng-vision-widget-promoter/SKILL.md +11 -4
  3. package/.agents/skills/ui-widgets/SKILL.md +25 -1
  4. package/docs/ui_engineering.md +44 -1
  5. package/front_end/core/common/Settings.ts +11 -4
  6. package/front_end/core/root/Runtime.ts +5 -0
  7. package/front_end/core/sdk/CSSModel.test.api.ts +102 -0
  8. package/front_end/core/sdk/CSSModel.ts +3 -2
  9. package/front_end/core/sdk/DebuggerModel.ts +3 -2
  10. package/front_end/core/sdk/SDKSettings.ts +19 -0
  11. package/front_end/core/sdk/sdk-meta.ts +0 -62
  12. package/front_end/core/sdk/sdk.ts +2 -0
  13. package/front_end/entrypoints/main/MainImpl.ts +10 -3
  14. package/front_end/foundation/Universe.ts +7 -0
  15. package/front_end/models/ai_assistance/AiAgent2.ts +1 -1
  16. package/front_end/models/ai_assistance/BuiltInAi.ts +11 -6
  17. package/front_end/models/ai_assistance/ChangeManager.ts +17 -1
  18. package/front_end/models/javascript_metadata/NativeFunctions.js +22 -9
  19. package/front_end/models/source_map_scopes/NamesResolver.ts +23 -17
  20. package/front_end/panels/application/ServiceWorkerUpdateCycleView.ts +78 -118
  21. package/front_end/panels/application/preloading/components/preloadingDetailsReportView.css +13 -13
  22. package/front_end/panels/console/ConsoleInsightTeaser.ts +3 -2
  23. package/front_end/panels/emulation/DeviceModeToolbar.ts +2 -2
  24. package/front_end/panels/emulation/DeviceModeView.ts +63 -104
  25. package/front_end/panels/sensors/LocationsSettingsTab.ts +244 -34
  26. package/front_end/panels/sensors/locationsSettingsTab.css +74 -0
  27. package/front_end/panels/sources/DebuggerPlugin.ts +2 -2
  28. package/front_end/panels/sources/sources-meta.ts +55 -0
  29. package/front_end/third_party/chromium/README.chromium +1 -1
  30. package/front_end/ui/legacy/Dialog.ts +93 -4
  31. package/front_end/ui/legacy/InspectorView.ts +1 -5
  32. package/front_end/ui/legacy/PopoverHelper.ts +5 -5
  33. package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
  34. package/package.json +1 -1
@@ -452,30 +452,6 @@ const UIStrings = {
452
452
  */
453
453
  networkCacheExplanation:
454
454
  'Disabling the network cache will simulate a network experience similar to a first time visitor.',
455
- /**
456
- * @description Setting under the Sources category to toggle usage of JavaScript source maps.
457
- */
458
- javaScriptSourceMaps: 'JavaScript source maps',
459
- /**
460
- * @description Title of an option under the Sources category that can be invoked through the Command Menu.
461
- */
462
- enableJavaScriptSourceMaps: 'Enable JavaScript source maps',
463
- /**
464
- * @description Title of an option under the Sources category that can be invoked through the Command Menu.
465
- */
466
- disableJavaScriptSourceMaps: 'Disable JavaScript source maps',
467
- /**
468
- * @description Title of a setting under the Sources category.
469
- */
470
- cssSourceMaps: 'CSS source maps',
471
- /**
472
- * @description Title of an option under the Sources category that can be invoked through the Command Menu.
473
- */
474
- enableCssSourceMaps: 'Enable CSS source maps',
475
- /**
476
- * @description Title of an option under the Sources category that can be invoked through the Command Menu.
477
- */
478
- disableCssSourceMaps: 'Disable CSS source maps',
479
455
  /**
480
456
  * @description Title of a setting under the Console category in Settings.
481
457
  */
@@ -1391,44 +1367,6 @@ Common.Settings.registerSettingExtension({
1391
1367
  },
1392
1368
  });
1393
1369
 
1394
- Common.Settings.registerSettingExtension({
1395
- category: Common.Settings.SettingCategory.SOURCES,
1396
- storageType: Common.Settings.SettingStorageType.SYNCED,
1397
- title: i18nLazyString(UIStrings.javaScriptSourceMaps),
1398
- settingName: 'js-source-maps-enabled',
1399
- settingType: Common.Settings.SettingType.BOOLEAN,
1400
- defaultValue: true,
1401
- options: [
1402
- {
1403
- value: true,
1404
- title: i18nLazyString(UIStrings.enableJavaScriptSourceMaps),
1405
- },
1406
- {
1407
- value: false,
1408
- title: i18nLazyString(UIStrings.disableJavaScriptSourceMaps),
1409
- },
1410
- ],
1411
- });
1412
-
1413
- Common.Settings.registerSettingExtension({
1414
- category: Common.Settings.SettingCategory.SOURCES,
1415
- storageType: Common.Settings.SettingStorageType.SYNCED,
1416
- title: i18nLazyString(UIStrings.cssSourceMaps),
1417
- settingName: 'css-source-maps-enabled',
1418
- settingType: Common.Settings.SettingType.BOOLEAN,
1419
- defaultValue: true,
1420
- options: [
1421
- {
1422
- value: true,
1423
- title: i18nLazyString(UIStrings.enableCssSourceMaps),
1424
- },
1425
- {
1426
- value: false,
1427
- title: i18nLazyString(UIStrings.disableCssSourceMaps),
1428
- },
1429
- ],
1430
- });
1431
-
1432
1370
  Common.Settings.registerSettingExtension({
1433
1371
  category: Common.Settings.SettingCategory.CONSOLE,
1434
1372
  storageType: Common.Settings.SettingStorageType.SYNCED,
@@ -75,6 +75,7 @@ import * as ScopeTreeCache from './ScopeTreeCache.js';
75
75
  import * as ScreenCaptureModel from './ScreenCaptureModel.js';
76
76
  import * as Script from './Script.js';
77
77
  import * as SDKModel from './SDKModel.js';
78
+ import * as SDKSettings from './SDKSettings.js';
78
79
  import * as SecurityOriginManager from './SecurityOriginManager.js';
79
80
  import * as ServerSentEventProtocol from './ServerSentEventsProtocol.js';
80
81
  import * as ServerTiming from './ServerTiming.js';
@@ -160,6 +161,7 @@ export {
160
161
  ScreenCaptureModel,
161
162
  Script,
162
163
  SDKModel,
164
+ SDKSettings,
163
165
  SecurityOriginManager,
164
166
  ServerSentEventProtocol,
165
167
  ServerTiming,
@@ -373,8 +373,15 @@ export class MainImpl {
373
373
  Root.ExperimentNames.ExperimentName.PROTOCOL_MONITOR, protocolMonitorExperiment);
374
374
 
375
375
  // Debugging
376
- Root.Runtime.experiments.register(
377
- Root.ExperimentNames.ExperimentName.INSTRUMENTATION_BREAKPOINTS, 'Instrumentation breakpoints');
376
+ const instrumentationBreakpointsExperiment = Root.Runtime.experiments.registerHostExperiment({
377
+ name: Root.ExperimentNames.ExperimentName.INSTRUMENTATION_BREAKPOINTS,
378
+ title: 'Instrumentation breakpoints',
379
+ aboutFlag: 'devtools-instrumentation-breakpoints',
380
+ isEnabled: Root.Runtime.hostConfig.devToolsInstrumentationBreakpoints?.enabled ?? false,
381
+ requiresChromeRestart: false,
382
+ });
383
+ this.#migrateValueFromLegacyToHostExperiment(Root.ExperimentNames.ExperimentName.INSTRUMENTATION_BREAKPOINTS,
384
+ instrumentationBreakpointsExperiment);
378
385
 
379
386
  Root.Runtime.experiments.registerHostExperiment({
380
387
  name: Root.ExperimentNames.ExperimentName.DURABLE_MESSAGES,
@@ -471,7 +478,7 @@ export class MainImpl {
471
478
  void this.#universe.liveMetrics.enable();
472
479
  CrUXManager.CrUXManager.instance();
473
480
 
474
- const builtInAi = AiAssistanceModel.BuiltInAi.BuiltInAi.instance();
481
+ const builtInAi = this.#universe.builtInAi;
475
482
  builtInAi.addEventListener(
476
483
  AiAssistanceModel.BuiltInAi.Events.DOWNLOADED_AND_SESSION_CREATED,
477
484
  () => Snackbar.Snackbar.Snackbar.show({message: i18nString(UIStrings.aiModelDownloaded)}));
@@ -216,6 +216,9 @@ export class Universe {
216
216
  const aiHistoryStorage = new AiAssistance.AiHistoryStorage.AiHistoryStorage(settings);
217
217
  context.set(AiAssistance.AiHistoryStorage.AiHistoryStorage, aiHistoryStorage);
218
218
 
219
+ const builtInAi = new AiAssistance.BuiltInAi.BuiltInAi();
220
+ context.set(AiAssistance.BuiltInAi.BuiltInAi, builtInAi);
221
+
219
222
  this.autofillManager = new AutofillManager.AutofillManager.AutofillManager(targetManager, frameManager);
220
223
  context.set(AutofillManager.AutofillManager.AutofillManager, this.autofillManager);
221
224
  }
@@ -233,6 +236,10 @@ export class Universe {
233
236
  return this.context.get(AiAssistance.AiHistoryStorage.AiHistoryStorage);
234
237
  }
235
238
 
239
+ get builtInAi(): AiAssistance.BuiltInAi.BuiltInAi {
240
+ return this.context.get(AiAssistance.BuiltInAi.BuiltInAi);
241
+ }
242
+
236
243
  get breakpointManager(): Breakpoints.BreakpointManager.BreakpointManager {
237
244
  return this.context.get(Breakpoints.BreakpointManager.BreakpointManager);
238
245
  }
@@ -82,7 +82,7 @@ export class AiAgent2 extends AiAgent<unknown> {
82
82
 
83
83
  constructor(opts: AiAgent2Options) {
84
84
  super(opts);
85
- this.#changes = new ChangeManager(opts.targetManager);
85
+ this.#changes = opts.changeManager ?? new ChangeManager(opts.targetManager);
86
86
  this.#lighthouseRecording = opts.lighthouseRecording;
87
87
  this.#execJs = opts.execJs ?? executeJsCode;
88
88
  this.#allowedOrigin = opts.allowedOrigin;
@@ -6,8 +6,6 @@ import * as Common from '../../core/common/common.js';
6
6
  import * as Host from '../../core/host/host.js';
7
7
  import * as Root from '../../core/root/root.js';
8
8
 
9
- let builtInAiInstance: BuiltInAi|undefined;
10
-
11
9
  export interface LanguageModel {
12
10
  promptStreaming: (arg0: string, opts?: {
13
11
  signal?: AbortSignal,
@@ -33,10 +31,11 @@ export class BuiltInAi extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
33
31
  #currentlyCreatingSession = false;
34
32
 
35
33
  static instance(): BuiltInAi {
36
- if (builtInAiInstance === undefined) {
37
- builtInAiInstance = new BuiltInAi();
34
+ if (!Root.DevToolsContext.globalInstance().has(BuiltInAi)) {
35
+ Root.DevToolsContext.globalInstance().set(BuiltInAi, new BuiltInAi());
38
36
  }
39
- return builtInAiInstance;
37
+
38
+ return Root.DevToolsContext.globalInstance().get(BuiltInAi);
40
39
  }
41
40
 
42
41
  constructor() {
@@ -107,6 +106,12 @@ export class BuiltInAi extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
107
106
  }
108
107
 
109
108
  #isGpuAvailable(): boolean {
109
+ // In node-based API tests, there is no global `document` available. Since this
110
+ // class is instantiated during Universe bootstrapping, we need to guard against
111
+ // using document when not in the browser.
112
+ if (typeof document === 'undefined') {
113
+ return false;
114
+ }
110
115
  const canvas = document.createElement('canvas');
111
116
  try {
112
117
  const webgl = canvas.getContext('webgl');
@@ -197,7 +202,7 @@ Your instructions are as follows:
197
202
  }
198
203
 
199
204
  static removeInstance(): void {
200
- builtInAiInstance = undefined;
205
+ Root.DevToolsContext.globalInstance().delete(BuiltInAi);
201
206
  }
202
207
 
203
208
  async * getConsoleInsight(prompt: string, abortController: AbortController): AsyncGenerator<string> {
@@ -25,13 +25,15 @@ function formatStyles(styles: Record<string, string>, indent = 2): string {
25
25
  * primarily for stylesheet generation based on all changes.
26
26
  */
27
27
  export class ChangeManager {
28
+ readonly #targetManager: SDK.TargetManager.TargetManager;
28
29
  readonly #stylesheetMutex = new Common.Mutex.Mutex();
29
30
  readonly #cssModelToStylesheetId =
30
31
  new Map<SDK.CSSModel.CSSModel, Map<Protocol.Page.FrameId, Protocol.DOM.StyleSheetId>>();
31
32
  readonly #stylesheetChanges = new Map<Protocol.DOM.StyleSheetId, Change[]>();
32
33
 
33
34
  constructor(targetManager: SDK.TargetManager.TargetManager = SDK.TargetManager.TargetManager.instance()) {
34
- targetManager.addModelListener(
35
+ this.#targetManager = targetManager;
36
+ this.#targetManager.addModelListener(
35
37
  SDK.ResourceTreeModel.ResourceTreeModel,
36
38
  SDK.ResourceTreeModel.Events.PrimaryPageChanged,
37
39
  this.clear,
@@ -39,6 +41,20 @@ export class ChangeManager {
39
41
  );
40
42
  }
41
43
 
44
+ dispose(): void {
45
+ this.#targetManager.removeModelListener(
46
+ SDK.ResourceTreeModel.ResourceTreeModel,
47
+ SDK.ResourceTreeModel.Events.PrimaryPageChanged,
48
+ this.clear,
49
+ this,
50
+ );
51
+ for (const cssModel of this.#cssModelToStylesheetId.keys()) {
52
+ cssModel.removeEventListener(SDK.CSSModel.Events.ModelDisposed, this.#onCssModelDisposed, this);
53
+ }
54
+ this.#cssModelToStylesheetId.clear();
55
+ this.#stylesheetChanges.clear();
56
+ }
57
+
42
58
  async clear(): Promise<void> {
43
59
  const models = Array.from(this.#cssModelToStylesheetId.keys());
44
60
  const results = await Promise.allSettled(models.map(async model => {
@@ -117,11 +117,6 @@ export const NativeFunctions = [
117
117
  signatures: [["id"]],
118
118
  receivers: ["Clients","BackgroundFetchManager"]
119
119
  },
120
- {
121
- name: "get",
122
- signatures: [["entry"]],
123
- receivers: ["RouteMap"]
124
- },
125
120
  {
126
121
  name: "set",
127
122
  signatures: [["v"]],
@@ -2003,6 +1998,11 @@ export const NativeFunctions = [
2003
1998
  signatures: [["start","end"]],
2004
1999
  receivers: ["SourceBuffer"]
2005
2000
  },
2001
+ {
2002
+ name: "remove",
2003
+ signatures: [["slot"]],
2004
+ receivers: ["GPUResourceTable"]
2005
+ },
2006
2006
  {
2007
2007
  name: "remove",
2008
2008
  signatures: [["manifest_ids"]],
@@ -3302,6 +3302,11 @@ export const NativeFunctions = [
3302
3302
  signatures: [["response"]],
3303
3303
  receivers: ["MediaKeySession"]
3304
3304
  },
3305
+ {
3306
+ name: "update",
3307
+ signatures: [["slot","resource"]],
3308
+ receivers: ["GPUResourceTable"]
3309
+ },
3305
3310
  {
3306
3311
  name: "createObjectStore",
3307
3312
  signatures: [["name","?options"]]
@@ -6685,6 +6690,14 @@ export const NativeFunctions = [
6685
6690
  name: "writeTimestamp",
6686
6691
  signatures: [["querySet","queryIndex"]]
6687
6692
  },
6693
+ {
6694
+ name: "setResourceTable",
6695
+ signatures: [["table"]]
6696
+ },
6697
+ {
6698
+ name: "createResourceTable",
6699
+ signatures: [["descriptor"]]
6700
+ },
6688
6701
  {
6689
6702
  name: "GPUInternalError",
6690
6703
  signatures: [["message"]]
@@ -6713,6 +6726,10 @@ export const NativeFunctions = [
6713
6726
  name: "multiDrawIndexedIndirect",
6714
6727
  signatures: [["indirectBuffer","indirectOffset","maxDrawCount","?drawCountBuffer","?drawCountBufferOffset"]]
6715
6728
  },
6729
+ {
6730
+ name: "insert",
6731
+ signatures: [["resource"]]
6732
+ },
6716
6733
  {
6717
6734
  name: "GPUUncapturedErrorEvent",
6718
6735
  signatures: [["type","gpuUncapturedErrorEventInitDict"]]
@@ -8469,10 +8486,6 @@ export const NativeFunctions = [
8469
8486
  name: "removeProcessingInstruction",
8470
8487
  signatures: [["pi"]]
8471
8488
  },
8472
- {
8473
- name: "RouteEvent",
8474
- signatures: [["type"]]
8475
- },
8476
8489
  {
8477
8490
  name: "ResizeObserver",
8478
8491
  signatures: [["callback"]]
@@ -187,21 +187,21 @@ const enum Punctuation {
187
187
  EQUALS = 'equals',
188
188
  }
189
189
 
190
- const resolveDebuggerScope = async(
191
- scope: SDK.DebuggerModel.ScopeChainEntry,
192
- debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding):
193
- Promise<{variableMapping: Map<string, string>, thisMapping: string | null}> => {
194
- if (!scope.callFrame()
195
- .debuggerModel.target()
196
- .targetManager()
197
- .settings.moduleSetting('js-source-maps-enabled')
198
- .get()) {
199
- return {variableMapping: new Map(), thisMapping: null};
200
- }
201
- const script = scope.callFrame().script;
202
- const scopeChain = await findScopeChainForDebuggerScope(scope);
203
- return await resolveScope(script, scopeChain, debuggerWorkspaceBinding);
204
- };
190
+ const resolveDebuggerScope =
191
+ async(scope: SDK.DebuggerModel.ScopeChainEntry,
192
+ debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding):
193
+ Promise<{variableMapping: Map<string, string>, thisMapping: string | null}> => {
194
+ if (!scope.callFrame()
195
+ .debuggerModel.target()
196
+ .targetManager()
197
+ .settings.resolve(SDK.SDKSettings.jsSourceMapsEnabledSettingDescriptor)
198
+ .get()) {
199
+ return {variableMapping: new Map(), thisMapping: null};
200
+ }
201
+ const script = scope.callFrame().script;
202
+ const scopeChain = await findScopeChainForDebuggerScope(scope);
203
+ return await resolveScope(script, scopeChain, debuggerWorkspaceBinding);
204
+ };
205
205
 
206
206
  const resolveScope = async(script: SDK.Script.Script, scopeChain: Formatter.FormatterWorkerPool.ScopeTreeNode[],
207
207
  debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding):
@@ -400,7 +400,10 @@ export const allVariablesInCallFrame = async(
400
400
  callFrame: SDK.DebuggerModel.CallFrame,
401
401
  debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding):
402
402
  Promise<Map<string, string|null>> => {
403
- if (!callFrame.debuggerModel.target().targetManager().settings.moduleSetting('js-source-maps-enabled').get()) {
403
+ if (!callFrame.debuggerModel.target()
404
+ .targetManager()
405
+ .settings.resolve(SDK.SDKSettings.jsSourceMapsEnabledSettingDescriptor)
406
+ .get()) {
404
407
  return new Map<string, string|null>();
405
408
  }
406
409
  const cachedMap = cachedMapByCallFrame.get(callFrame);
@@ -443,7 +446,10 @@ export const allVariablesAtPosition =
443
446
  if (!script) {
444
447
  return reverseMapping;
445
448
  }
446
- if (!script.debuggerModel.target().targetManager().settings.moduleSetting('js-source-maps-enabled').get()) {
449
+ if (!script.debuggerModel.target()
450
+ .targetManager()
451
+ .settings.resolve(SDK.SDKSettings.jsSourceMapsEnabledSettingDescriptor)
452
+ .get()) {
447
453
  return reverseMapping;
448
454
  }
449
455
 
@@ -6,7 +6,7 @@
6
6
  import * as i18n from '../../core/i18n/i18n.js';
7
7
  import * as SDK from '../../core/sdk/sdk.js';
8
8
  import * as Protocol from '../../generated/protocol.js';
9
- import * as UI from '../../ui/legacy/legacy.js';
9
+ import {html, render} from '../../ui/lit/lit.js';
10
10
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
11
11
 
12
12
  const UIStrings = {
@@ -39,13 +39,16 @@ export class ServiceWorkerUpdateCycleView {
39
39
  private registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration;
40
40
  private rows: HTMLTableRowElement[];
41
41
  private selectedRowIndex: number;
42
+ private expandedRows = new Set<string>();
42
43
  tableElement: HTMLElement;
43
44
  constructor(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration) {
44
45
  this.registration = registration;
45
46
  this.rows = [];
46
47
  this.selectedRowIndex = -1;
47
48
  this.tableElement = document.createElement('table');
48
- this.createTimingTable();
49
+ this.tableElement.classList.add('service-worker-update-timing-table');
50
+ this.tableElement.setAttribute('jslog', `${VisualLogging.tree('update-timing-table')}`);
51
+ this.refresh();
49
52
  }
50
53
 
51
54
  calculateServiceWorkerUpdateRanges(): ServiceWorkerUpdateRange[] {
@@ -132,119 +135,87 @@ export class ServiceWorkerUpdateCycleView {
132
135
  return [];
133
136
  }
134
137
 
135
- private createTimingTable(): void {
136
- this.tableElement.classList.add('service-worker-update-timing-table');
137
- this.tableElement.setAttribute('jslog', `${VisualLogging.tree('update-timing-table')}`);
138
+ refresh(): void {
139
+ const tableHeader = html`
140
+ <tr class="service-worker-update-timing-table-header">
141
+ <td>${i18nString(UIStrings.version)}</td>
142
+ <td>${i18nString(UIStrings.updateActivity)}</td>
143
+ <td>${i18nString(UIStrings.timeline)}</td>
144
+ </tr>
145
+ `;
138
146
  const timeRanges = this.calculateServiceWorkerUpdateRanges();
139
- this.updateTimingTable(timeRanges);
140
- }
141
-
142
- private createTimingTableHead(): void {
143
- const serverHeader = this.tableElement.createChild('tr', 'service-worker-update-timing-table-header');
144
- UI.UIUtils.createTextChild(serverHeader.createChild('td'), i18nString(UIStrings.version));
145
- UI.UIUtils.createTextChild(serverHeader.createChild('td'), i18nString(UIStrings.updateActivity));
146
- UI.UIUtils.createTextChild(serverHeader.createChild('td'), i18nString(UIStrings.timeline));
147
- }
148
-
149
- private removeRows(): void {
150
- const rows = this.tableElement.getElementsByTagName('tr');
151
- while (rows[0]) {
152
- if (rows[0].parentNode) {
153
- rows[0].parentNode.removeChild(rows[0]);
154
- }
155
- }
156
- this.rows = [];
157
- }
158
-
159
- private updateTimingTable(timeRanges: ServiceWorkerUpdateRange[]): void {
160
- this.selectedRowIndex = -1;
161
- this.removeRows();
162
- this.createTimingTableHead();
163
- const timeRangeArray = timeRanges;
164
- if (timeRangeArray.length === 0) {
147
+ if (timeRanges.length === 0) {
148
+ // clang-format off
149
+ // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
150
+ render(tableHeader, this.tableElement, {host: this});
151
+ // clang-format on
152
+ this.rows = [];
165
153
  return;
166
154
  }
167
155
 
168
- const startTimes = timeRangeArray.map(r => r.start);
169
- const endTimes = timeRangeArray.map(r => r.end);
156
+ const startTimes = timeRanges.map(r => r.start);
157
+ const endTimes = timeRanges.map(r => r.end);
170
158
  const startTime = startTimes.reduce((a, b) => Math.min(a, b));
171
159
  const endTime = endTimes.reduce((a, b) => Math.max(a, b));
172
160
  const scale = 100 / (endTime - startTime);
173
161
 
174
- for (const range of timeRangeArray) {
175
- const phaseName = range.phase;
176
-
177
- const left = (scale * (range.start - startTime));
178
- const right = (scale * (endTime - range.end));
179
-
180
- const tr = this.tableElement.createChild('tr', 'service-worker-update-timeline');
181
- tr.setAttribute('jslog', `${VisualLogging.treeItem('update-timeline').track({
162
+ // clang-format off
163
+ // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
164
+ render(html`
165
+ ${tableHeader}
166
+ ${timeRanges.map(range => {
167
+ const phaseName = range.phase;
168
+ const left = (scale * (range.start - startTime));
169
+ const right = (scale * (endTime - range.end));
170
+ const key = `${range.id}-${range.phase}`;
171
+ const expanded = this.expandedRows.has(key);
172
+
173
+ return html`
174
+ <tr class="service-worker-update-timeline" jslog=${VisualLogging.treeItem('update-timeline').track({
182
175
  click: true,
183
176
  resize: true,
184
177
  keydown: 'ArrowLeft|ArrowRight|ArrowUp|ArrowDown|Enter|Space',
185
- })}`);
186
- this.rows.push(tr);
187
- const timingBarVersionElement = tr.createChild('td');
188
- UI.UIUtils.createTextChild(timingBarVersionElement, '#' + range.id);
189
- timingBarVersionElement.classList.add('service-worker-update-timing-bar-clickable');
190
- timingBarVersionElement.setAttribute('tabindex', '0');
191
- timingBarVersionElement.setAttribute('role', 'switch');
192
- timingBarVersionElement.addEventListener('focus', (event: Event) => {
193
- this.onFocus(event);
194
- });
195
- timingBarVersionElement.setAttribute('jslog', `${VisualLogging.expand('timing-info').track({click: true})}`);
196
- UI.ARIAUtils.setChecked(timingBarVersionElement, false);
197
- const timingBarTitleElement = tr.createChild('td');
198
- UI.UIUtils.createTextChild(timingBarTitleElement, phaseName);
199
- const barContainer = tr.createChild('td').createChild('div', 'service-worker-update-timing-row');
200
-
201
- const bar = barContainer.createChild('span', 'service-worker-update-timing-bar ' + phaseName.toLowerCase());
202
-
203
- bar.style.left = left + '%';
204
- bar.style.right = right + '%';
205
- bar.textContent = '\u200B'; // Important for 0-time items to have 0 width.
206
-
207
- this.constructUpdateDetails(tr, range);
178
+ })}>
179
+ <td class="service-worker-update-timing-bar-clickable" tabindex="0" role="switch"
180
+ aria-checked=${expanded ? 'true' : 'false'}
181
+ @focus=${this.onFocus}
182
+ @keydown=${(e: Event) => this.onKeydown(e, key)}
183
+ @click=${(e: Event) => this.onClick(e, key)}
184
+ jslog=${VisualLogging.expand('timing-info').track({click: true})}>
185
+ #${range.id}
186
+ </td>
187
+ <td>${phaseName}</td>
188
+ <td>
189
+ <div class="service-worker-update-timing-row">
190
+ <span class="service-worker-update-timing-bar ${phaseName.toLowerCase()}"
191
+ style="left: ${left}%; right: ${right}%;">\u200B</span>
192
+ </div>
193
+ </td>
194
+ </tr>
195
+ <tr class="service-worker-update-timing-bar-details ${expanded ? 'service-worker-update-timing-bar-details-expanded' : 'service-worker-update-timing-bar-details-collapsed'}" tabindex="0">
196
+ <td colspan="3"><span>${i18nString(UIStrings.startTimeS, {PH1: new Date(range.start).toISOString()})}</span></td>
197
+ </tr>
198
+ <tr class="service-worker-update-timing-bar-details ${expanded ? 'service-worker-update-timing-bar-details-expanded' : 'service-worker-update-timing-bar-details-collapsed'}" tabindex="0">
199
+ <td colspan="3"><span>${i18nString(UIStrings.endTimeS, {PH1: new Date(range.end).toISOString()})}</span></td>
200
+ </tr>
201
+ `;
202
+ })}
203
+ `, this.tableElement, {host: this});
204
+ // clang-format on
205
+
206
+ this.rows = Array.from(this.tableElement.querySelectorAll<HTMLTableRowElement>('.service-worker-update-timeline'));
207
+ if (this.selectedRowIndex >= this.rows.length) {
208
+ this.selectedRowIndex = -1;
208
209
  }
209
210
  }
210
211
 
211
- /**
212
- * Detailed information about an update phase. Currently starting and ending time.
213
- */
214
- private constructUpdateDetails(tr: HTMLElement, range: ServiceWorkerUpdateRange): void {
215
- const startRow = this.tableElement.createChild('tr', 'service-worker-update-timing-bar-details');
216
- startRow.classList.add('service-worker-update-timing-bar-details-collapsed');
217
- const startTimeItem = startRow.createChild('td');
218
- startTimeItem.colSpan = 3;
219
- const startTime = (new Date(range.start)).toISOString();
220
- UI.UIUtils.createTextChild(startTimeItem.createChild('span'), i18nString(UIStrings.startTimeS, {PH1: startTime}));
221
- startRow.tabIndex = 0;
222
-
223
- const endRow = this.tableElement.createChild('tr', 'service-worker-update-timing-bar-details');
224
- endRow.classList.add('service-worker-update-timing-bar-details-collapsed');
225
- const endTimeItem = endRow.createChild('td');
226
- endTimeItem.colSpan = 3;
227
- const endTime = (new Date(range.end)).toISOString();
228
- UI.UIUtils.createTextChild(endTimeItem.createChild('span'), i18nString(UIStrings.endTimeS, {PH1: endTime}));
229
- endRow.tabIndex = 0;
230
-
231
- tr.addEventListener('keydown', (event: Event) => {
232
- this.onKeydown(event, startRow, endRow);
233
- });
234
-
235
- tr.addEventListener('click', (event: Event) => {
236
- this.onClick(event, startRow, endRow);
237
- });
238
- }
239
-
240
- private toggle(startRow: Element, endRow: Element, target: Element, expanded: boolean): void {
241
- if (target.classList.contains('service-worker-update-timing-bar-clickable')) {
242
- startRow.classList.toggle('service-worker-update-timing-bar-details-collapsed');
243
- startRow.classList.toggle('service-worker-update-timing-bar-details-expanded');
244
- endRow.classList.toggle('service-worker-update-timing-bar-details-collapsed');
245
- endRow.classList.toggle('service-worker-update-timing-bar-details-expanded');
246
- UI.ARIAUtils.setChecked(target, !expanded);
212
+ private toggle(key: string, expanded: boolean): void {
213
+ if (expanded) {
214
+ this.expandedRows.delete(key);
215
+ } else {
216
+ this.expandedRows.add(key);
247
217
  }
218
+ this.refresh();
248
219
  }
249
220
 
250
221
  private onFocus(event: Event): void {
@@ -260,21 +231,20 @@ export class ServiceWorkerUpdateCycleView {
260
231
  this.selectedRowIndex = this.rows.indexOf(tr);
261
232
  }
262
233
 
263
- private onKeydown(event: Event, startRow: HTMLElement, endRow: HTMLElement): void {
234
+ private onKeydown(event: Event, key: string): void {
264
235
  if (!event.target) {
265
236
  return;
266
237
  }
267
- const target: HTMLElement = event.target as HTMLElement;
268
238
  const keyboardEvent = event as KeyboardEvent;
269
- const expanded = target.getAttribute('aria-checked') === 'true';
239
+ const expanded = this.expandedRows.has(key);
270
240
 
271
241
  if (keyboardEvent.key === 'Enter' || keyboardEvent.key === ' ') {
272
- this.toggle(startRow, endRow, target, expanded);
242
+ this.toggle(key, expanded);
273
243
  event.preventDefault();
274
244
  return;
275
245
  }
276
246
  if ((!expanded && keyboardEvent.key === 'ArrowRight') || (expanded && keyboardEvent.key === 'ArrowLeft')) {
277
- this.toggle(startRow, endRow, target, expanded);
247
+ this.toggle(key, expanded);
278
248
  event.preventDefault();
279
249
  return;
280
250
  }
@@ -346,21 +316,11 @@ export class ServiceWorkerUpdateCycleView {
346
316
  this.focusRow(this.rows[this.selectedRowIndex]);
347
317
  }
348
318
 
349
- private onClick(event: Event, startRow: Element, endRow: Element): void {
350
- const tr = event.target as Element;
351
- if (!tr) {
352
- return;
353
- }
354
-
355
- const expanded = tr.getAttribute('aria-checked') === 'true';
356
- this.toggle(startRow, endRow, tr, expanded);
319
+ private onClick(event: Event, key: string): void {
320
+ const expanded = this.expandedRows.has(key);
321
+ this.toggle(key, expanded);
357
322
  event.preventDefault();
358
323
  }
359
-
360
- refresh(): void {
361
- const timeRanges = this.calculateServiceWorkerUpdateRanges();
362
- this.updateTimingTable(timeRanges);
363
- }
364
324
  }
365
325
 
366
326
  export const enum ServiceWorkerUpdateNames {