chrome-devtools-frontend 1.0.1643099 → 1.0.1645245

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 (99) hide show
  1. package/eslint.config.mjs +3 -1
  2. package/extension-api/ExtensionAPI.d.ts +83 -12
  3. package/front_end/core/host/UserMetrics.ts +3 -3
  4. package/front_end/core/root/ExperimentNames.ts +0 -1
  5. package/front_end/core/sdk/CSSPropertyParserMatchers.ts +2 -3
  6. package/front_end/core/sdk/ConsoleModel.ts +4 -0
  7. package/front_end/core/sdk/NetworkRequest.ts +12 -1
  8. package/front_end/core/sdk/SourceMap.ts +15 -18
  9. package/front_end/entrypoints/greendev_floaty/FloatyEntrypoint.ts +0 -2
  10. package/front_end/entrypoints/greendev_floaty/greendev_floaty.ts +0 -2
  11. package/front_end/entrypoints/heap_snapshot_worker/HeapSnapshot.ts +37 -0
  12. package/front_end/entrypoints/main/MainImpl.ts +0 -6
  13. package/front_end/generated/SupportedCSSProperties.js +4 -2
  14. package/front_end/models/ai_assistance/AiAgent2.ts +23 -12
  15. package/front_end/models/ai_assistance/README.md +5 -4
  16. package/front_end/models/ai_assistance/agents/AccessibilityAgent.ts +22 -16
  17. package/front_end/models/ai_assistance/agents/AiAgent.ts +19 -6
  18. package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +5 -5
  19. package/front_end/models/ai_assistance/agents/ExecuteJavascript.ts +1 -94
  20. package/front_end/models/ai_assistance/agents/NetworkAgent.ts +25 -0
  21. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +37 -0
  22. package/front_end/models/ai_assistance/agents/README.md +57 -0
  23. package/front_end/models/ai_assistance/agents/StorageAgent.ts +54 -1
  24. package/front_end/models/ai_assistance/agents/StylingAgent.snapshot.txt +1 -2
  25. package/front_end/models/ai_assistance/agents/StylingAgent.ts +31 -3
  26. package/front_end/models/ai_assistance/tools/ExecuteJavaScript.ts +12 -21
  27. package/front_end/models/ai_assistance/tools/GetStyles.ts +19 -12
  28. package/front_end/models/ai_assistance/tools/README.md +45 -0
  29. package/front_end/models/ai_assistance/tools/Tool.ts +78 -9
  30. package/front_end/models/ai_assistance/tools/ToolRegistry.ts +21 -5
  31. package/front_end/models/bindings/DebuggerWorkspaceBinding.ts +6 -9
  32. package/front_end/models/bindings/DefaultScriptMapping.ts +2 -1
  33. package/front_end/models/bindings/SymbolizedError.ts +45 -35
  34. package/front_end/models/extensions/ExtensionAPI.ts +138 -47
  35. package/front_end/models/har/Importer.ts +1 -0
  36. package/front_end/models/heap_snapshot/HeapSnapshotModel.ts +18 -2
  37. package/front_end/models/heap_snapshot/HeapSnapshotProxy.ts +4 -0
  38. package/front_end/models/source_map_scopes/FunctionCodeResolver.ts +12 -2
  39. package/front_end/models/stack_trace/DetailedErrorStackParser.ts +58 -52
  40. package/front_end/models/stack_trace/StackTrace.ts +7 -0
  41. package/front_end/models/stack_trace/StackTraceImpl.ts +13 -4
  42. package/front_end/models/stack_trace/StackTraceModel.ts +43 -9
  43. package/front_end/models/trace/Styles.ts +29 -7
  44. package/front_end/models/trace/handlers/PageLoadMetricsHandler.ts +33 -4
  45. package/front_end/models/trace/helpers/Timing.ts +10 -0
  46. package/front_end/models/trace/types/TraceEvents.ts +22 -2
  47. package/front_end/panels/accessibility/AccessibilitySidebarView.ts +2 -1
  48. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +9 -2
  49. package/front_end/panels/ai_assistance/ai_assistance-meta.ts +16 -0
  50. package/front_end/panels/application/ApplicationPanelSidebar.ts +83 -0
  51. package/front_end/panels/application/ApplicationPanelTreeElement.ts +39 -0
  52. package/front_end/panels/application/CookieItemsView.ts +2 -2
  53. package/front_end/panels/application/ServiceWorkersView.ts +2 -2
  54. package/front_end/panels/application/WebMCPView.ts +0 -1
  55. package/front_end/panels/application/components/BackForwardCacheView.ts +1 -2
  56. package/front_end/panels/application/resourcesSidebar.css +11 -0
  57. package/front_end/panels/console/ConsoleView.ts +6 -1
  58. package/front_end/panels/console/ConsoleViewMessage.ts +46 -213
  59. package/front_end/panels/console/SymbolizedErrorWidget.ts +14 -8
  60. package/front_end/panels/elements/AdoptedStyleSheetTreeElement.ts +0 -1
  61. package/front_end/panels/elements/ElementsTreeElement.ts +0 -2
  62. package/front_end/panels/elements/PropertyRenderer.ts +0 -1
  63. package/front_end/panels/elements/StylesSidebarPane.ts +9 -2
  64. package/front_end/panels/issues/AffectedResourcesView.ts +1 -1
  65. package/front_end/panels/issues/AffectedSourcesView.ts +1 -1
  66. package/front_end/panels/lighthouse/LighthouseReportRenderer.ts +0 -1
  67. package/front_end/panels/mobile_throttling/ThrottlingSettingsTab.ts +10 -0
  68. package/front_end/panels/network/NetworkDataGridNode.ts +1 -2
  69. package/front_end/panels/network/NetworkLogView.ts +34 -7
  70. package/front_end/panels/profiler/HeapProfileView.ts +0 -1
  71. package/front_end/panels/profiler/HeapSnapshotGridNodes.ts +0 -1
  72. package/front_end/panels/profiler/HeapSnapshotView.ts +1 -1
  73. package/front_end/panels/settings/components/SyncSection.ts +1 -1
  74. package/front_end/panels/settings/emulation/DevicesSettingsTab.ts +1 -0
  75. package/front_end/panels/sources/SourcesPanel.ts +2 -1
  76. package/front_end/panels/timeline/TimelineFlameChartView.ts +5 -4
  77. package/front_end/panels/timeline/TimelinePanel.ts +7 -0
  78. package/front_end/panels/timeline/TimelineUIUtils.ts +13 -14
  79. package/front_end/panels/timeline/TimingsTrackAppender.ts +7 -5
  80. package/front_end/panels/timeline/components/LayoutShiftDetails.ts +0 -1
  81. package/front_end/panels/timeline/components/NetworkRequestDetails.ts +0 -2
  82. package/front_end/panels/timeline/components/insights/ForcedReflow.ts +0 -1
  83. package/front_end/panels/timeline/components/insights/NodeLink.ts +0 -1
  84. package/front_end/panels/timeline/overlays/OverlaysImpl.ts +2 -0
  85. package/front_end/third_party/chromium/README.chromium +1 -1
  86. package/front_end/ui/helpers/OpenInNewTab.ts +3 -3
  87. package/front_end/ui/kit/link/Link.ts +16 -2
  88. package/front_end/ui/legacy/InspectorDrawerView.ts +14 -5
  89. package/front_end/ui/legacy/InspectorView.ts +4 -1
  90. package/front_end/ui/legacy/PlusButton.ts +6 -1
  91. package/front_end/ui/legacy/StackedPane.ts +229 -0
  92. package/front_end/ui/legacy/ViewManager.ts +59 -169
  93. package/front_end/ui/legacy/Widget.ts +19 -1
  94. package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +95 -31
  95. package/front_end/ui/legacy/components/utils/JSPresentationUtils.ts +0 -1
  96. package/front_end/ui/legacy/components/utils/Linkifier.ts +2 -16
  97. package/front_end/ui/legacy/legacy.ts +3 -1
  98. package/front_end/ui/visual_logging/KnownContextValues.ts +5 -0
  99. package/package.json +1 -1
@@ -4,6 +4,7 @@
4
4
 
5
5
  import * as Common from '../../core/common/common.js';
6
6
  import * as i18n from '../../core/i18n/i18n.js';
7
+ import * as Root from '../../core/root/root.js';
7
8
  import * as VisualLogging from '../visual_logging/visual_logging.js';
8
9
 
9
10
  import * as ARIAUtils from './ARIAUtils.js';
@@ -129,11 +130,19 @@ export class InspectorDrawerView {
129
130
  this.#isConsoleOpenInMainAndDrawer = options.isConsoleOpenInMainAndDrawer;
130
131
  this.#drawerMinimizedSetting =
131
132
  Common.Settings.Settings.instance().createLocalSetting('inspector.drawer-minimized', false);
132
- this.tabbedLocation = ViewManager.instance().createTabbedLocation(
133
- options.revealDrawer, 'drawer-view', true, true,
134
- {isLocationVisible: options.isVisible, tabbedPaneFactory: () => new DrawerTabbedPane()});
135
- this.#moreTabsButton = this.tabbedLocation.enableMoreTabsButton();
136
- this.#moreTabsButton.setTitle(i18nString(UIStrings.moreTools));
133
+ this.tabbedLocation = ViewManager.instance().createTabbedLocation(options.revealDrawer, 'drawer-view', true, true, {
134
+ isLocationVisible: options.isVisible,
135
+ tabbedPaneFactory: () => new DrawerTabbedPane(),
136
+ plusButton: {title: i18nString(UIStrings.moreTools), jslogContext: 'plus-button-drawer'},
137
+ });
138
+ // When the plus button is disabled we keep the legacy left-toolbar
139
+ // three-dot menu so users can still reach hidden / addable tools.
140
+ if (Root.Runtime.hostConfig.devToolsPlusButton?.enabled) {
141
+ this.#moreTabsButton = null;
142
+ } else {
143
+ this.#moreTabsButton = this.tabbedLocation.enableMoreTabsButton();
144
+ this.#moreTabsButton.setTitle(i18nString(UIStrings.moreTools));
145
+ }
137
146
  this.tabbedPane = this.tabbedLocation.tabbedPane() as DrawerTabbedPane;
138
147
  this.tabbedPane.element.classList.add('drawer-tabbed-pane');
139
148
  this.tabbedPane.element.setAttribute('jslog', `${VisualLogging.drawer()}`);
@@ -228,7 +228,10 @@ export class InspectorView extends VBox implements ViewLocationResolver {
228
228
  this.tabbedLocation = ViewManager.instance().createTabbedLocation(
229
229
  Host.InspectorFrontendHost.InspectorFrontendHostInstance.bringToFront.bind(
230
230
  Host.InspectorFrontendHost.InspectorFrontendHostInstance),
231
- 'panel', true, true, {defaultTab: Root.Runtime.Runtime.queryParam('panel')});
231
+ 'panel', true, true, {
232
+ defaultTab: Root.Runtime.Runtime.queryParam('panel'),
233
+ plusButton: {jslogContext: 'plus-button-panel'},
234
+ });
232
235
 
233
236
  this.tabbedPane = this.tabbedLocation.tabbedPane();
234
237
  this.tabbedPane.setMinimumSize(MIN_MAIN_PANEL_WIDTH, 0);
@@ -108,8 +108,13 @@ export class PlusButtonPresenter {
108
108
  for (const view of views()) {
109
109
  // Skip views that already have a tab. Hidden tabs are already listed
110
110
  // in the overflow section above, and visible tabs are accessible
111
- // directly in the tab strip.
111
+ // directly in the tab strip. Track their id and title so the
112
+ // cross-location loop below cannot offer a same-titled duplicate
113
+ // (e.g. drawer "Console" while the panel "Console" is already
114
+ // visible — they have different view ids).
112
115
  if (tabbedPane.hasTab(view.viewId())) {
116
+ seenIds.add(view.viewId());
117
+ seenTitles.add(view.title());
113
118
  continue;
114
119
  }
115
120
  // Transient views are not user-addable.
@@ -0,0 +1,229 @@
1
+ // Copyright 2026 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ /* eslint-disable @devtools/no-imperative-dom-api */
6
+
7
+ import type * as Lit from '../../ui/lit/lit.js';
8
+ import {createIcon, type Icon} from '../kit/kit.js';
9
+ import * as VisualLogging from '../visual_logging/visual_logging.js';
10
+
11
+ import * as ARIAUtils from './ARIAUtils.js';
12
+ import type * as Toolbar from './Toolbar.js';
13
+ import {createTextChild} from './UIUtils.js';
14
+ import type {View} from './View.js';
15
+ import viewContainersStyles from './viewContainers.css.js';
16
+ import {type AnyWidget, VBox} from './Widget.js';
17
+
18
+ type CreateToolbarFn = (toolbarItems: Toolbar.ToolbarItem[]|Lit.TemplateResult) => Element|null;
19
+ type SetWidgetForViewFn = (view: View, widget: AnyWidget) => void;
20
+
21
+ export class ExpandableContainerWidget extends VBox {
22
+ private titleElement: HTMLDivElement;
23
+ private readonly titleExpandIcon: Icon;
24
+ private readonly view: View;
25
+ private widget?: AnyWidget;
26
+ private materializePromise?: Promise<void>;
27
+
28
+ constructor(
29
+ view: View,
30
+ private readonly createToolbar: CreateToolbarFn,
31
+ private readonly setWidgetForView: SetWidgetForViewFn,
32
+ private readonly onVisibilityChanged?: (isExpanded: boolean) => void,
33
+ ) {
34
+ super({useShadowDom: true});
35
+ this.element.classList.add('flex-none');
36
+ this.registerRequiredCSS(viewContainersStyles);
37
+
38
+ this.onVisibilityChanged = onVisibilityChanged;
39
+ this.createToolbar = createToolbar;
40
+
41
+ this.titleElement = document.createElement('div');
42
+ this.titleElement.classList.add('expandable-view-title');
43
+ this.titleElement.setAttribute('jslog', `${VisualLogging.sectionHeader().context(view.viewId()).track({
44
+ click: true,
45
+ keydown: 'Enter|Space|ArrowLeft|ArrowRight',
46
+ })}`);
47
+ ARIAUtils.markAsTreeitem(this.titleElement);
48
+ this.titleExpandIcon = createIcon('triangle-right', 'title-expand-icon');
49
+ this.titleElement.appendChild(this.titleExpandIcon);
50
+ const titleText = view.title();
51
+ createTextChild(this.titleElement, titleText);
52
+ ARIAUtils.setLabel(this.titleElement, titleText);
53
+ ARIAUtils.setExpanded(this.titleElement, false);
54
+ this.titleElement.tabIndex = 0;
55
+ self.onInvokeElement(this.titleElement, this.toggleExpanded.bind(this));
56
+ this.titleElement.addEventListener('keydown', this.onTitleKeyDown.bind(this), false);
57
+ this.contentElement.insertBefore(this.titleElement, this.contentElement.firstChild);
58
+
59
+ ARIAUtils.setControls(this.titleElement, this.contentElement.createChild('slot'));
60
+ this.view = view;
61
+ expandableContainerForView.set(view, this);
62
+ }
63
+
64
+ isExpanded(): boolean {
65
+ return this.titleElement.classList.contains('expanded');
66
+ }
67
+
68
+ override wasShown(): void {
69
+ super.wasShown();
70
+ if (this.widget && this.materializePromise) {
71
+ void this.materializePromise.then(() => {
72
+ if (this.isExpanded() && this.widget) {
73
+ this.widget.show(this.element);
74
+ }
75
+ });
76
+ }
77
+ }
78
+
79
+ private materialize(): Promise<void> {
80
+ if (this.materializePromise) {
81
+ return this.materializePromise;
82
+ }
83
+ // TODO(crbug.com/1006759): Transform to async-await
84
+ const promises = [];
85
+ promises.push(this.view.toolbarItems().then(toolbarItems => {
86
+ const toolbarElement = this.createToolbar(toolbarItems);
87
+ if (toolbarElement) {
88
+ this.titleElement.appendChild(toolbarElement);
89
+ }
90
+ }));
91
+ promises.push(this.view.widget().then(widget => {
92
+ this.widget = widget;
93
+ this.setWidgetForView(this.view, widget);
94
+ }));
95
+ this.materializePromise = Promise.all(promises).then(() => {});
96
+ return this.materializePromise;
97
+ }
98
+
99
+ expand(): Promise<void> {
100
+ if (this.isExpanded()) {
101
+ return this.materialize();
102
+ }
103
+ this.titleElement.classList.add('expanded');
104
+ ARIAUtils.setExpanded(this.titleElement, true);
105
+ this.titleExpandIcon.name = 'triangle-down';
106
+ this.onVisibilityChanged?.(true);
107
+ return this.materialize().then(() => {
108
+ if (this.isExpanded() && this.widget) {
109
+ this.widget.show(this.element);
110
+ }
111
+ });
112
+ }
113
+
114
+ private collapse(): void {
115
+ if (!this.isExpanded()) {
116
+ return;
117
+ }
118
+ this.titleElement.classList.remove('expanded');
119
+ ARIAUtils.setExpanded(this.titleElement, false);
120
+ this.titleExpandIcon.name = 'triangle-right';
121
+ this.onVisibilityChanged?.(false);
122
+ void this.materialize().then(() => {
123
+ if (this.widget) {
124
+ this.widget.detach();
125
+ }
126
+ });
127
+ }
128
+
129
+ private toggleExpanded(event: Event): void {
130
+ if (event.type === 'keydown' && event.target !== this.titleElement) {
131
+ return;
132
+ }
133
+ if (this.isExpanded()) {
134
+ this.collapse();
135
+ } else {
136
+ void this.expand();
137
+ }
138
+ }
139
+
140
+ private onTitleKeyDown(event: Event): void {
141
+ if (event.target !== this.titleElement) {
142
+ return;
143
+ }
144
+ const keyEvent = (event as KeyboardEvent);
145
+ if (keyEvent.key === 'ArrowLeft') {
146
+ this.collapse();
147
+ } else if (keyEvent.key === 'ArrowRight') {
148
+ if (!this.isExpanded()) {
149
+ void this.expand();
150
+ } else if (this.widget) {
151
+ this.widget.focus();
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ const expandableContainerForView = new WeakMap<View, ExpandableContainerWidget>();
158
+
159
+ export class StackedPane extends VBox {
160
+ readonly expandableContainers = new Map<string, ExpandableContainerWidget>();
161
+ constructor(
162
+ private readonly createToolbar: CreateToolbarFn,
163
+ private readonly setWidgetForView: SetWidgetForViewFn,
164
+ private readonly onViewVisibilityChanged?: (viewId: string, isExpanded: boolean) => void,
165
+ ) {
166
+ super();
167
+ this.createToolbar = createToolbar;
168
+ this.onViewVisibilityChanged = onViewVisibilityChanged;
169
+ ARIAUtils.markAsTree(this.element);
170
+ }
171
+
172
+ appendView(view: View, insertBefore?: View|null): void {
173
+ let container = this.expandableContainers.get(view.viewId());
174
+ if (!container) {
175
+ container =
176
+ new ExpandableContainerWidget(view, this.createToolbar, this.setWidgetForView,
177
+ isExpanded => this.onViewVisibilityChanged?.(view.viewId(), isExpanded));
178
+ let beforeElement: Node|null = null;
179
+ if (insertBefore) {
180
+ const beforeContainer = expandableContainerForView.get(insertBefore);
181
+ beforeElement = beforeContainer ? beforeContainer.element : null;
182
+ }
183
+ container.show(this.contentElement, beforeElement);
184
+ this.expandableContainers.set(view.viewId(), container);
185
+ }
186
+ }
187
+
188
+ override wasShown(): void {
189
+ super.wasShown();
190
+ for (const [viewId, container] of this.expandableContainers) {
191
+ if (container.isExpanded()) {
192
+ this.onViewVisibilityChanged?.(viewId, true);
193
+ }
194
+ }
195
+ }
196
+
197
+ override willHide(): void {
198
+ super.willHide();
199
+ for (const [viewId, container] of this.expandableContainers) {
200
+ if (container.isExpanded()) {
201
+ this.onViewVisibilityChanged?.(viewId, false);
202
+ }
203
+ }
204
+ }
205
+
206
+ removeView(view: View): void {
207
+ const container = this.expandableContainers.get(view.viewId());
208
+ if (container) {
209
+ container.detach();
210
+ this.expandableContainers.delete(view.viewId());
211
+ }
212
+ }
213
+
214
+ async expandView(view: View): Promise<void> {
215
+ const container = this.expandableContainers.get(view.viewId());
216
+ if (container) {
217
+ await container.expand();
218
+ }
219
+ }
220
+
221
+ isViewExpanded(viewId: string): boolean {
222
+ const container = this.expandableContainers.get(viewId);
223
+ return container ? container.isExpanded() : false;
224
+ }
225
+
226
+ getContainerForView(view: View): ExpandableContainerWidget|undefined {
227
+ return this.expandableContainers.get(view.viewId());
228
+ }
229
+ }
@@ -11,18 +11,17 @@ import * as i18n from '../../core/i18n/i18n.js';
11
11
  import * as Platform from '../../core/platform/platform.js';
12
12
  import * as Root from '../../core/root/root.js';
13
13
  import type * as Foundation from '../../foundation/foundation.js';
14
- import {createIcon, type Icon} from '../kit/kit.js';
14
+ import {createIcon} from '../kit/kit.js';
15
15
  import {render, type TemplateResult} from '../lit/lit.js';
16
16
  import * as VisualLogging from '../visual_logging/visual_logging.js';
17
17
 
18
18
  import * as ARIAUtils from './ARIAUtils.js';
19
19
  import type {ContextMenu} from './ContextMenu.js';
20
20
  import * as PlusButton from './PlusButton.js';
21
+ import {StackedPane} from './StackedPane.js';
21
22
  import {type EventData, Events as TabbedPaneEvents, TabbedPane} from './TabbedPane.js';
22
23
  import {type ItemsProvider, type ToolbarItem, ToolbarMenuButton} from './Toolbar.js';
23
- import {createTextChild} from './UIUtils.js';
24
24
  import type {TabbedViewLocation, View, ViewLocation} from './View.js';
25
- import viewContainersStyles from './viewContainers.css.js';
26
25
  import {
27
26
  getLocalizedViewLocationCategory,
28
27
  getRegisteredLocationResolvers,
@@ -262,6 +261,10 @@ export class ViewManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
262
261
  return toolbar;
263
262
  }
264
263
 
264
+ static setWidgetForView(view: View, widget: AnyWidget): void {
265
+ widgetForView.set(view, widget);
266
+ }
267
+
265
268
  getRegisteredViewExtensions(): PreRegisteredView[] {
266
269
  return this.preRegisteredViews;
267
270
  }
@@ -402,7 +405,7 @@ export class ViewManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
402
405
  return new TabbedLocation(this, revealCallback, location, restoreSelection, allowReorder, options);
403
406
  }
404
407
 
405
- createStackLocation(revealCallback?: (() => void), location?: string, jslogContext?: string): ViewLocation {
408
+ createStackLocation(revealCallback?: (() => void), location?: string, jslogContext?: string): StackLocation {
406
409
  return new StackLocation(this, revealCallback, location, jslogContext);
407
410
  }
408
411
 
@@ -453,7 +456,7 @@ export class ContainerWidget extends VBox {
453
456
  // Move focus from |this| to loaded |widget| if any.
454
457
  const shouldFocus = this.element.hasFocus();
455
458
  this.setDefaultFocusedElement(null);
456
- widgetForView.set(this.view, widget);
459
+ ViewManager.setWidgetForView(this.view, widget);
457
460
  widget.show(this.element);
458
461
  if (shouldFocus) {
459
462
  widget.focus();
@@ -479,131 +482,6 @@ export class ContainerWidget extends VBox {
479
482
  }
480
483
  }
481
484
 
482
- class ExpandableContainerWidget extends VBox {
483
- private titleElement: HTMLDivElement;
484
- private readonly titleExpandIcon: Icon;
485
- private readonly view: View;
486
- private widget?: AnyWidget;
487
- private materializePromise?: Promise<void>;
488
-
489
- constructor(view: View) {
490
- super({useShadowDom: true});
491
- this.element.classList.add('flex-none');
492
- this.registerRequiredCSS(viewContainersStyles);
493
-
494
- this.titleElement = document.createElement('div');
495
- this.titleElement.classList.add('expandable-view-title');
496
- this.titleElement.setAttribute('jslog', `${VisualLogging.sectionHeader().context(view.viewId()).track({
497
- click: true,
498
- keydown: 'Enter|Space|ArrowLeft|ArrowRight',
499
- })}`);
500
- ARIAUtils.markAsTreeitem(this.titleElement);
501
- this.titleExpandIcon = createIcon('triangle-right', 'title-expand-icon');
502
- this.titleElement.appendChild(this.titleExpandIcon);
503
- const titleText = view.title();
504
- createTextChild(this.titleElement, titleText);
505
- ARIAUtils.setLabel(this.titleElement, titleText);
506
- ARIAUtils.setExpanded(this.titleElement, false);
507
- this.titleElement.tabIndex = 0;
508
- self.onInvokeElement(this.titleElement, this.toggleExpanded.bind(this));
509
- this.titleElement.addEventListener('keydown', this.onTitleKeyDown.bind(this), false);
510
- this.contentElement.insertBefore(this.titleElement, this.contentElement.firstChild);
511
-
512
- ARIAUtils.setControls(this.titleElement, this.contentElement.createChild('slot'));
513
- this.view = view;
514
- expandableContainerForView.set(view, this);
515
- }
516
-
517
- override wasShown(): void {
518
- super.wasShown();
519
- if (this.widget && this.materializePromise) {
520
- void this.materializePromise.then(() => {
521
- if (this.titleElement.classList.contains('expanded') && this.widget) {
522
- this.widget.show(this.element);
523
- }
524
- });
525
- }
526
- }
527
-
528
- private materialize(): Promise<void> {
529
- if (this.materializePromise) {
530
- return this.materializePromise;
531
- }
532
- // TODO(crbug.com/1006759): Transform to async-await
533
- const promises = [];
534
- promises.push(this.view.toolbarItems().then(toolbarItems => {
535
- const toolbarElement = ViewManager.createToolbar(toolbarItems);
536
- if (toolbarElement) {
537
- this.titleElement.appendChild(toolbarElement);
538
- }
539
- }));
540
- promises.push(this.view.widget().then(widget => {
541
- this.widget = widget;
542
- widgetForView.set(this.view, widget);
543
- widget.show(this.element);
544
- }));
545
- this.materializePromise = Promise.all(promises).then(() => {});
546
- return this.materializePromise;
547
- }
548
-
549
- expand(): Promise<void> {
550
- if (this.titleElement.classList.contains('expanded')) {
551
- return this.materialize();
552
- }
553
- this.titleElement.classList.add('expanded');
554
- ARIAUtils.setExpanded(this.titleElement, true);
555
- this.titleExpandIcon.name = 'triangle-down';
556
- return this.materialize().then(() => {
557
- if (this.widget) {
558
- this.widget.show(this.element);
559
- }
560
- });
561
- }
562
-
563
- private collapse(): void {
564
- if (!this.titleElement.classList.contains('expanded')) {
565
- return;
566
- }
567
- this.titleElement.classList.remove('expanded');
568
- ARIAUtils.setExpanded(this.titleElement, false);
569
- this.titleExpandIcon.name = 'triangle-right';
570
- void this.materialize().then(() => {
571
- if (this.widget) {
572
- this.widget.detach();
573
- }
574
- });
575
- }
576
-
577
- private toggleExpanded(event: Event): void {
578
- if (event.type === 'keydown' && event.target !== this.titleElement) {
579
- return;
580
- }
581
- if (this.titleElement.classList.contains('expanded')) {
582
- this.collapse();
583
- } else {
584
- void this.expand();
585
- }
586
- }
587
-
588
- private onTitleKeyDown(event: Event): void {
589
- if (event.target !== this.titleElement) {
590
- return;
591
- }
592
- const keyEvent = (event as KeyboardEvent);
593
- if (keyEvent.key === 'ArrowLeft') {
594
- this.collapse();
595
- } else if (keyEvent.key === 'ArrowRight') {
596
- if (!this.titleElement.classList.contains('expanded')) {
597
- void this.expand();
598
- } else if (this.widget) {
599
- this.widget.focus();
600
- }
601
- }
602
- }
603
- }
604
-
605
- const expandableContainerForView = new WeakMap<View, ExpandableContainerWidget>();
606
-
607
485
  class Location {
608
486
  protected readonly manager: ViewManager;
609
487
  private readonly revealCallback: (() => void)|undefined;
@@ -958,68 +836,80 @@ class TabbedLocation extends Location implements TabbedViewLocation {
958
836
  static orderStep = 10; // Keep in sync with descriptors.
959
837
  }
960
838
 
961
- class StackLocation extends Location implements ViewLocation {
962
- private readonly vbox: VBox;
963
- private readonly expandableContainers: Map<string, ExpandableContainerWidget>;
964
-
965
- constructor(manager: ViewManager, revealCallback?: (() => void), location?: string, jslogContext?: string) {
966
- const vbox = new VBox();
967
- vbox.element.setAttribute('jslog', `${VisualLogging.pane(jslogContext || 'sidebar').track({resize: true})}`);
968
- super(manager, vbox, revealCallback);
969
- this.vbox = vbox;
970
- ARIAUtils.markAsTree(vbox.element);
971
-
972
- this.expandableContainers = new Map();
839
+ export class StackLocation extends Location implements ViewLocation {
840
+ private readonly stackedPane: StackedPane;
841
+ private readonly location: string;
842
+ #isVisible: boolean;
843
+
844
+ constructor(manager: ViewManager, revealCallback?: (() => void), location?: string, jslogContext?: string,
845
+ initialVisibility = true) {
846
+ const stackedPane =
847
+ new StackedPane(ViewManager.createToolbar, ViewManager.setWidgetForView, (viewId, isExpanded) => {
848
+ if (this.#isVisible) {
849
+ manager.dispatchEventToListeners(Events.VIEW_VISIBILITY_CHANGED, {
850
+ location: this.location,
851
+ revealedViewId: isExpanded ? viewId : undefined,
852
+ hiddenViewId: isExpanded ? undefined : viewId,
853
+ });
854
+ }
855
+ });
856
+ stackedPane.element.setAttribute('jslog', `${VisualLogging.pane(jslogContext || 'sidebar').track({resize: true})}`);
857
+ super(manager, stackedPane, revealCallback);
858
+ this.location = location || '';
859
+ this.stackedPane = stackedPane;
860
+ this.#isVisible = initialVisibility;
973
861
 
974
862
  if (location) {
975
863
  this.appendApplicableItems(location);
976
864
  }
977
865
  }
978
866
 
867
+ // Blink test(s) rely on this
868
+ get expandableContainers(): Map<string, AnyWidget> {
869
+ return this.stackedPane.expandableContainers;
870
+ }
871
+
872
+ notifyVisibilityChanged(isVisible: boolean): void {
873
+ if (this.#isVisible === isVisible) {
874
+ return;
875
+ }
876
+ this.#isVisible = isVisible;
877
+
878
+ for (const [viewId, container] of this.stackedPane.expandableContainers) {
879
+ if (container.isExpanded()) {
880
+ this.manager.dispatchEventToListeners(Events.VIEW_VISIBILITY_CHANGED, {
881
+ location: this.location,
882
+ revealedViewId: isVisible ? viewId : undefined,
883
+ hiddenViewId: isVisible ? undefined : viewId,
884
+ });
885
+ }
886
+ }
887
+ }
888
+
979
889
  appendView(view: View, insertBefore?: View|null): void {
980
890
  const oldLocation = locationForView.get(view);
981
891
  if (oldLocation && oldLocation !== this) {
982
892
  oldLocation.removeView(view);
983
893
  }
984
894
 
985
- let container = this.expandableContainers.get(view.viewId());
986
- if (!container) {
987
- locationForView.set(view, this);
988
- this.manager.views.set(view.viewId(), view);
989
- container = new ExpandableContainerWidget(view);
990
- let beforeElement: Node|null = null;
991
- if (insertBefore) {
992
- const beforeContainer = expandableContainerForView.get(insertBefore);
993
- beforeElement = beforeContainer ? beforeContainer.element : null;
994
- }
995
- container.show(this.vbox.contentElement, beforeElement);
996
- this.expandableContainers.set(view.viewId(), container);
997
- }
895
+ locationForView.set(view, this);
896
+ this.manager.views.set(view.viewId(), view);
897
+ this.stackedPane.appendView(view, insertBefore);
998
898
  }
999
899
 
1000
900
  override async showView(view: View, insertBefore?: View|null): Promise<void> {
1001
901
  this.appendView(view, insertBefore);
1002
- const container = this.expandableContainers.get(view.viewId());
1003
- if (container) {
1004
- await container.expand();
1005
- }
902
+ await this.stackedPane.expandView(view);
1006
903
  }
1007
904
 
1008
905
  override removeView(view: View): void {
1009
- const container = this.expandableContainers.get(view.viewId());
1010
- if (!container) {
1011
- return;
1012
- }
1013
-
1014
- container.detach();
1015
- this.expandableContainers.delete(view.viewId());
906
+ this.stackedPane.removeView(view);
1016
907
  locationForView.delete(view);
1017
908
  this.manager.views.delete(view.viewId());
1018
909
  }
1019
910
 
1020
- override isViewVisible(_view: View): boolean {
1021
- // TODO(crbug.com/435356108): Implement this
1022
- throw new Error('not implemented');
911
+ override isViewVisible(view: View): boolean {
912
+ return this.#isVisible && this.stackedPane.isViewExpanded(view.viewId());
1023
913
  }
1024
914
 
1025
915
  appendApplicableItems(locationName: string): void {
@@ -483,6 +483,12 @@ export type WidgetOptions<ContentTypeT extends HTMLElement|DocumentFragment = HT
483
483
  classes?: never,
484
484
  });
485
485
 
486
+ const enum UpdateState {
487
+ NORMAL = 'NORMAL', // Standard state: update can be aborted for efficiency.
488
+ INTERRUPTED = 'INTERRUPTED', // An update was physically running and got aborted; replacement must be shielded.
489
+ SHIELDED = 'SHIELDED', // Current update is a replacement for an interrupted one and cannot be aborted.
490
+ }
491
+
486
492
  export class Widget<ContentTypeT extends HTMLElement|DocumentFragment = HTMLElement> {
487
493
  readonly element: HTMLElement;
488
494
  #contentElement: ContentTypeT;
@@ -501,6 +507,7 @@ export class Widget<ContentTypeT extends HTMLElement|DocumentFragment = HTMLElem
501
507
  #externallyManaged?: boolean;
502
508
  #updateComplete = UPDATE_COMPLETE;
503
509
  #updateController?: AbortController;
510
+ #updateState = UpdateState.NORMAL;
504
511
 
505
512
  /**
506
513
  * Constructs a new `Widget` with the given `options`.
@@ -1165,12 +1172,17 @@ export class Widget<ContentTypeT extends HTMLElement|DocumentFragment = HTMLElem
1165
1172
  }
1166
1173
 
1167
1174
  addUpdateController(controller: AbortController): void {
1175
+ const wasInterrupted = this.#updateState === UpdateState.INTERRUPTED;
1168
1176
  this.#updateController?.abort();
1169
1177
  this.#updateController = controller;
1178
+ // Transition to SHIELDED if we are replacing a starved update, otherwise reset to NORMAL.
1179
+ this.#updateState = wasInterrupted ? UpdateState.SHIELDED : UpdateState.NORMAL;
1170
1180
  }
1171
1181
 
1172
1182
  cancelUpdateController(): void {
1173
1183
  this.#updateController?.abort();
1184
+ this.#updateController = undefined;
1185
+ this.#updateState = UpdateState.NORMAL;
1174
1186
  }
1175
1187
 
1176
1188
  /**
@@ -1180,7 +1192,13 @@ export class Widget<ContentTypeT extends HTMLElement|DocumentFragment = HTMLElem
1180
1192
  * frame.
1181
1193
  */
1182
1194
  requestUpdate(): void {
1183
- this.#updateController?.abort();
1195
+ // If the state is SHIELDED, we skip the abort call entirely to break the starvation loop.
1196
+ if (this.#updateState !== UpdateState.SHIELDED) {
1197
+ if (currentlyProcessed.has(this)) {
1198
+ this.#updateState = UpdateState.INTERRUPTED;
1199
+ }
1200
+ this.#updateController?.abort();
1201
+ }
1184
1202
  this.#updateComplete = enqueueWidgetUpdate(this);
1185
1203
  }
1186
1204