chrome-devtools-frontend 1.0.1698733 → 1.0.1700561

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 (164) hide show
  1. package/.agents/skills/devtools-verification/SKILL.md +19 -0
  2. package/.agents/skills/{repro-flaky-tests → fix-tests}/SKILL.md +2 -2
  3. package/front_end/core/sdk/CSSPropertyParserMatchers.ts +2 -2
  4. package/front_end/core/sdk/DOMModel.ts +3 -5
  5. package/front_end/core/sdk/NetworkManager.ts +2 -2
  6. package/front_end/core/sdk/OverlayModel.ts +11 -12
  7. package/front_end/core/sdk/ServiceWorkerCacheModel.ts +3 -0
  8. package/front_end/core/sdk/SourceMapScopeChainEntry.ts +84 -32
  9. package/front_end/core/sdk/StorageBucketsModel.ts +2 -0
  10. package/front_end/entrypoints/main/GlobalAiButton.ts +2 -6
  11. package/front_end/entrypoints/main/MainImpl.ts +8 -1
  12. package/front_end/generated/InspectorBackendCommands.ts +12 -2
  13. package/front_end/generated/SupportedCSSProperties.ts +14 -0
  14. package/front_end/generated/protocol-mapping.d.ts +48 -1
  15. package/front_end/generated/protocol-proxy-api.d.ts +37 -1
  16. package/front_end/generated/protocol.ts +131 -0
  17. package/front_end/models/bindings/NetworkProject.ts +30 -0
  18. package/front_end/models/bindings/ResourceScriptMapping.ts +62 -0
  19. package/front_end/models/change_tracker/ChangeTracker.ts +1 -1
  20. package/front_end/models/comment_manager/CD4ABridge.ts +2 -2
  21. package/front_end/models/comment_manager/CommentManager.ts +18 -0
  22. package/front_end/models/emulation/DeviceModeModel.ts +1 -1
  23. package/front_end/models/extensions/ExtensionAPI.ts +4 -2
  24. package/front_end/models/issues_manager/CookieIssue.ts +0 -15
  25. package/front_end/models/persistence/NetworkPersistenceManager.ts +29 -4
  26. package/front_end/models/source_map_scopes/NamesResolver.ts +34 -4
  27. package/front_end/models/source_map_scopes/ScopeChainModel.ts +31 -7
  28. package/front_end/panels/accessibility/AccessibilityAnnouncementRecordingView.ts +281 -3
  29. package/front_end/panels/accessibility/accessibilityAnnouncementRecordingView.css +85 -0
  30. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +1 -2
  31. package/front_end/panels/ai_assistance/ai_assistance-meta.ts +5 -8
  32. package/front_end/panels/animation/AnimationTimeline.ts +1 -1
  33. package/front_end/panels/application/AppManifestView.ts +128 -134
  34. package/front_end/panels/application/CrashReportContextView.ts +6 -6
  35. package/front_end/panels/application/DeviceBoundSessionsTreeElement.ts +17 -17
  36. package/front_end/panels/application/DeviceBoundSessionsView.ts +136 -137
  37. package/front_end/panels/application/IndexedDBModel.ts +3 -0
  38. package/front_end/panels/application/IndexedDBViews.ts +1 -1
  39. package/front_end/panels/application/KeyValueStorageItemsView.ts +1 -0
  40. package/front_end/panels/application/OriginTrialTreeView.ts +18 -23
  41. package/front_end/panels/application/ReportingApiTreeElement.ts +3 -3
  42. package/front_end/panels/application/ReportingApiView.ts +6 -14
  43. package/front_end/panels/application/application-meta.ts +7 -7
  44. package/front_end/panels/application/components/AdsView.ts +63 -1
  45. package/front_end/panels/application/components/adsView.css +11 -1
  46. package/front_end/panels/common/CommentThreadWidget.ts +20 -22
  47. package/front_end/panels/common/CommentsOverlayWidget.ts +115 -9
  48. package/front_end/panels/common/CommentsStatusBarPill.ts +13 -1
  49. package/front_end/panels/common/ExtensionServer.ts +43 -34
  50. package/front_end/panels/common/GeminiRebrandPromoDialog.ts +1 -1
  51. package/front_end/panels/common/commentThreadWidget.css +6 -0
  52. package/front_end/panels/common/common-meta.ts +4 -1
  53. package/front_end/panels/elements/CSSRuleValidator.ts +1 -1
  54. package/front_end/panels/elements/ElementsPanel.ts +4 -4
  55. package/front_end/panels/elements/ElementsTreeElement.ts +9 -2
  56. package/front_end/panels/elements/ElementsTreeOutline.ts +25 -33
  57. package/front_end/panels/elements/elementsTreeOutline.css +8 -4
  58. package/front_end/panels/event_listeners/EventListenersView.ts +9 -4
  59. package/front_end/panels/issues/IssuesPane.ts +34 -22
  60. package/front_end/panels/lighthouse/LighthouseController.ts +1 -1
  61. package/front_end/panels/mobile_throttling/NetworkPanelIndicator.ts +1 -1
  62. package/front_end/panels/mobile_throttling/ThrottlingManager.ts +1 -1
  63. package/front_end/panels/mobile_throttling/ThrottlingSettingsTab.ts +1 -1
  64. package/front_end/panels/network/RequestPayloadView.ts +17 -15
  65. package/front_end/panels/recorder/RecordingView.ts +32 -53
  66. package/front_end/panels/recorder/StepView.ts +19 -4
  67. package/front_end/panels/recorder/TimelineSection.ts +1 -1
  68. package/front_end/panels/recorder/controlButton.css +6 -0
  69. package/front_end/panels/recorder/recordingView.css +11 -2
  70. package/front_end/panels/settings/BackendLinkingSettingsTab.ts +2 -1
  71. package/front_end/panels/settings/EditFileSystemView.ts +1 -1
  72. package/front_end/panels/sources/CallStackSidebarPane.ts +1 -1
  73. package/front_end/panels/sources/DebuggerPlugin.ts +137 -56
  74. package/front_end/panels/sources/ScopeChainSidebarPane.ts +17 -15
  75. package/front_end/panels/timeline/components/DetailsView.ts +3 -3
  76. package/front_end/panels/timeline/components/IgnoreListSetting.ts +0 -2
  77. package/front_end/panels/timeline/components/LiveMetricsView.ts +59 -64
  78. package/front_end/panels/timeline/components/OriginMap.ts +1 -1
  79. package/front_end/panels/timeline/components/insights/BaseInsightComponent.ts +2 -3
  80. package/front_end/panels/timeline/components/liveMetricsView.css +1 -0
  81. package/front_end/third_party/chromium/README.chromium +1 -1
  82. package/front_end/third_party/lighthouse/README.chromium +2 -2
  83. package/front_end/third_party/lighthouse/lighthouse-dt-bundle.js +1580 -1524
  84. package/front_end/third_party/lighthouse/locales/ar-XB.json +111 -72
  85. package/front_end/third_party/lighthouse/locales/ar.json +113 -74
  86. package/front_end/third_party/lighthouse/locales/bg.json +112 -73
  87. package/front_end/third_party/lighthouse/locales/ca.json +112 -73
  88. package/front_end/third_party/lighthouse/locales/cs.json +112 -73
  89. package/front_end/third_party/lighthouse/locales/da.json +112 -73
  90. package/front_end/third_party/lighthouse/locales/de.json +112 -73
  91. package/front_end/third_party/lighthouse/locales/el.json +112 -73
  92. package/front_end/third_party/lighthouse/locales/en-GB.json +111 -72
  93. package/front_end/third_party/lighthouse/locales/en-US.json +102 -66
  94. package/front_end/third_party/lighthouse/locales/en-XA.json +111 -72
  95. package/front_end/third_party/lighthouse/locales/en-XL.json +102 -66
  96. package/front_end/third_party/lighthouse/locales/es-419.json +112 -73
  97. package/front_end/third_party/lighthouse/locales/es.json +112 -73
  98. package/front_end/third_party/lighthouse/locales/fi.json +112 -73
  99. package/front_end/third_party/lighthouse/locales/fil.json +112 -73
  100. package/front_end/third_party/lighthouse/locales/fr.json +112 -73
  101. package/front_end/third_party/lighthouse/locales/he.json +112 -73
  102. package/front_end/third_party/lighthouse/locales/hi.json +112 -73
  103. package/front_end/third_party/lighthouse/locales/hr.json +112 -73
  104. package/front_end/third_party/lighthouse/locales/hu.json +112 -73
  105. package/front_end/third_party/lighthouse/locales/id.json +112 -73
  106. package/front_end/third_party/lighthouse/locales/it.json +113 -74
  107. package/front_end/third_party/lighthouse/locales/ja.json +112 -73
  108. package/front_end/third_party/lighthouse/locales/ko.json +112 -73
  109. package/front_end/third_party/lighthouse/locales/lt.json +112 -73
  110. package/front_end/third_party/lighthouse/locales/lv.json +112 -73
  111. package/front_end/third_party/lighthouse/locales/nl.json +112 -73
  112. package/front_end/third_party/lighthouse/locales/no.json +112 -73
  113. package/front_end/third_party/lighthouse/locales/pl.json +112 -73
  114. package/front_end/third_party/lighthouse/locales/pt-PT.json +112 -73
  115. package/front_end/third_party/lighthouse/locales/pt.json +112 -73
  116. package/front_end/third_party/lighthouse/locales/ro.json +112 -73
  117. package/front_end/third_party/lighthouse/locales/ru.json +112 -73
  118. package/front_end/third_party/lighthouse/locales/sk.json +112 -73
  119. package/front_end/third_party/lighthouse/locales/sl.json +112 -73
  120. package/front_end/third_party/lighthouse/locales/sr-Latn.json +112 -73
  121. package/front_end/third_party/lighthouse/locales/sr.json +112 -73
  122. package/front_end/third_party/lighthouse/locales/sv.json +124 -85
  123. package/front_end/third_party/lighthouse/locales/ta.json +112 -73
  124. package/front_end/third_party/lighthouse/locales/te.json +112 -73
  125. package/front_end/third_party/lighthouse/locales/th.json +112 -73
  126. package/front_end/third_party/lighthouse/locales/tr.json +113 -74
  127. package/front_end/third_party/lighthouse/locales/uk.json +112 -73
  128. package/front_end/third_party/lighthouse/locales/vi.json +112 -73
  129. package/front_end/third_party/lighthouse/locales/zh-HK.json +112 -73
  130. package/front_end/third_party/lighthouse/locales/zh-TW.json +113 -74
  131. package/front_end/third_party/lighthouse/locales/zh.json +113 -74
  132. package/front_end/third_party/lighthouse/report/bundle.d.ts +4 -4
  133. package/front_end/third_party/lighthouse/report/bundle.js +10 -3
  134. package/front_end/third_party/lighthouse/report-assets/report-generator.mjs +2 -2
  135. package/front_end/third_party/lit/lit.ts +3 -0
  136. package/front_end/third_party/lit/package.json +3 -0
  137. package/front_end/third_party/lit/rebuild.sh +3 -2
  138. package/front_end/ui/comments/CommentAnchorResolver.ts +8 -2
  139. package/front_end/ui/comments/CommentOverlayManager.ts +2 -1
  140. package/front_end/ui/components/buttons/Button.ts +19 -1
  141. package/front_end/ui/legacy/ContextMenu.ts +16 -1
  142. package/front_end/ui/legacy/InspectorView.ts +4 -3
  143. package/front_end/ui/legacy/PlusButton.ts +5 -6
  144. package/front_end/ui/legacy/TabbedPane.ts +121 -80
  145. package/front_end/ui/legacy/Treeoutline.ts +37 -6
  146. package/front_end/ui/legacy/UIUtils.ts +7 -0
  147. package/front_end/ui/legacy/Widget.ts +33 -0
  148. package/front_end/ui/legacy/components/cookie_table/CookiesTable.ts +3 -0
  149. package/front_end/ui/legacy/components/data_grid/DataGridElement.ts +14 -4
  150. package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +157 -171
  151. package/front_end/ui/lit/lit.ts +1 -1
  152. package/front_end/ui/visual_logging/KnownContextValues.ts +7 -0
  153. package/inspector_overlay/highlight_anchor.ts +197 -0
  154. package/inspector_overlay/tool_highlight.ts +8 -0
  155. package/package.json +1 -1
  156. package/.agents/skills/devtools-ci/SKILL.md +0 -65
  157. package/.agents/skills/evaluate-ai-css-completion/SKILL.md +0 -143
  158. package/.agents/skills/evaluate-ai-css-completion/scripts/evaluate.js +0 -104
  159. package/.agents/skills/fixing-skipped-tests/SKILL.md +0 -32
  160. package/.agents/skills/gerrit-cli/SKILL.md +0 -74
  161. package/.agents/skills/gerrit-cli/references/commands.md +0 -169
  162. package/.agents/skills/gerrit-cli/references/workflows.md +0 -63
  163. package/.agents/skills/gerrit-cli/scripts/import_upstream.py +0 -85
  164. package/front_end/models/issues_manager/descriptions/cookieExcludeBlockedWithinRelatedWebsiteSet.md +0 -4
@@ -9,4 +9,7 @@ import * as Directive from './lib/directive.js';
9
9
  import * as Decorators from './lib/decorators.js';
10
10
  import * as StaticHtml from './lib/static-html.js';
11
11
 
12
+ // Required for lit-analyzer
13
+ export type {DirectiveResult} from './lib/directive.js';
14
+
12
15
  export {AsyncDirective, Directives, Directive, Decorators, StaticHtml};
@@ -6,6 +6,9 @@
6
6
  "lit": "3.3.3"
7
7
  },
8
8
  "devDependencies": {
9
+ "@rollup/plugin-node-resolve": "16.0.3",
10
+ "@rollup/plugin-terser": "1.0.0",
11
+ "rollup": "4.62.4",
9
12
  "rollup-plugin-dts": "6.4.1"
10
13
  }
11
14
  }
@@ -1,6 +1,7 @@
1
+ set -e
1
2
  npm install
2
- ../../../node_modules/.bin/tsc --ignoreConfig -d -t esnext -m esnext --moduleResolution bundler src/*
3
- ../../../node_modules/rollup/dist/bin/rollup -c
3
+ vpython3 ../../../third_party/typescript/typescript.py --ignoreConfig --skipLibCheck -d -t esnext -m esnext --moduleResolution bundler src/*
4
+ node_modules/rollup/dist/bin/rollup -c
4
5
  rm -rf node_modules src/*.js src/*.d.ts
5
6
  # Because there's a bug in clang causing it to reformat import lists even where formatting is disabled, run it right away
6
7
  git cl format --js
@@ -29,6 +29,12 @@ const DISALLOWED_COMMENT_TARGETS = new Set<number>([
29
29
  VisualLogging.VisualElements.Toolbar,
30
30
  ]);
31
31
 
32
+ /**
33
+ * The comment thread UI itself is never a valid comment target: anything inside it (including the
34
+ * DOM node link in its header) must stay inert while comment mode is on.
35
+ */
36
+ export const COMMENT_THREAD_UI_SELECTOR = '.comment-thread-widget';
37
+
32
38
  /**
33
39
  * Finds the closest ancestor (or the element itself) matching a CSS selector,
34
40
  * traversing across Shadow DOM boundaries (shadow root boundaries to shadow hosts).
@@ -268,7 +274,7 @@ function resolveCodeMirrorLineInfo(element: Element): CodeMirrorLineInfo|null {
268
274
  * Resolves an arbitrary clicked or targeted DOM element to its appropriate semantic comment anchor element.
269
275
  *
270
276
  * Traversal hierarchy:
271
- * 1. Checks if the element is part of a tab title (returns null if so).
277
+ * 1. Checks if the element is part of a tab title or of the comment thread UI (returns null if so).
272
278
  * 2. Escalates CodeMirror line/gutter elements to .cm-editor (only if the clicked line is non-empty).
273
279
  * 3. Checks for domain IDs (`data-network-request-id` or `data-backend-node-id`) across shadow boundaries,
274
280
  * returning the owning domain element.
@@ -281,7 +287,7 @@ function resolveCodeMirrorLineInfo(element: Element): CodeMirrorLineInfo|null {
281
287
  */
282
288
  export function resolveCommentAnchorElement(
283
289
  element: Element, options?: {clientX: number, clientY: number, forHover?: boolean}): Element|null {
284
- if (isTabTitle(element)) {
290
+ if (isTabTitle(element) || closestAcrossShadow(element, COMMENT_THREAD_UI_SELECTOR)) {
285
291
  return null;
286
292
  }
287
293
  const customResolver = getCustomAnchorResolverForElement(element);
@@ -7,6 +7,7 @@ import * as CommentManager from '../../models/comment_manager/comment_manager.js
7
7
 
8
8
  import {
9
9
  closestAcrossShadow,
10
+ COMMENT_THREAD_UI_SELECTOR,
10
11
  type CommentAnchorSignature,
11
12
  type CommentThread,
12
13
  computeVisibleRect,
@@ -238,7 +239,7 @@ export class CommentOverlayManager extends Common.ObjectWrapper.ObjectWrapper<Ev
238
239
  }
239
240
 
240
241
  handleElementClick(element: Element, options?: {clientX: number, clientY: number}): boolean {
241
- if (!this.isCommentMode() || closestAcrossShadow(element, '.comment-thread-widget')) {
242
+ if (!this.isCommentMode() || closestAcrossShadow(element, COMMENT_THREAD_UI_SELECTOR)) {
242
243
  return false;
243
244
  }
244
245
  this.clearDraftThreads();
@@ -48,6 +48,7 @@ interface ButtonState {
48
48
  variant: Variant;
49
49
  size?: Size;
50
50
  reducedFocusRing?: boolean;
51
+ buttonTitle?: string;
51
52
  disabled: boolean;
52
53
  toggled?: boolean;
53
54
  toggleOnClick?: boolean;
@@ -80,7 +81,14 @@ interface CommonButtonData {
80
81
  spinner?: boolean;
81
82
  type?: ButtonType;
82
83
  value?: string;
84
+ /**
85
+ * Sets title on the host and, unless buttonTitle is provided, the internal <button> element.
86
+ */
83
87
  title?: string;
88
+ /**
89
+ * Sets title on the internal <button> element, taking precedence over title.
90
+ */
91
+ buttonTitle?: string;
84
92
  jslogContext?: string;
85
93
  longClickable?: boolean;
86
94
  inverseColorTheme?: boolean;
@@ -166,6 +174,7 @@ export class Button extends HTMLElement {
166
174
  if (data.title) {
167
175
  this.title = data.title;
168
176
  }
177
+ this.#props.buttonTitle = data.buttonTitle;
169
178
 
170
179
  if (data.accessibleLabel) {
171
180
  this.accessibleLabel = data.accessibleLabel;
@@ -245,6 +254,15 @@ export class Button extends HTMLElement {
245
254
  this.#render();
246
255
  }
247
256
 
257
+ get buttonTitle(): string|undefined {
258
+ return this.#props.buttonTitle;
259
+ }
260
+
261
+ set buttonTitle(buttonTitle: string|undefined) {
262
+ this.#props.buttonTitle = buttonTitle;
263
+ this.#render();
264
+ }
265
+
248
266
  get disabled(): boolean {
249
267
  return this.#props.disabled;
250
268
  }
@@ -396,7 +414,7 @@ export class Button extends HTMLElement {
396
414
  Lit.render(
397
415
  html`
398
416
  <style>${buttonStyles}</style>
399
- <button title=${ifDefined(this.title || undefined)}
417
+ <button title=${ifDefined(this.buttonTitle || this.title || undefined)}
400
418
  ?disabled=${this.#props.disabled}
401
419
  class=${classMap(classes)}
402
420
  aria-pressed=${ifDefined(this.#props.toggled)}
@@ -989,6 +989,7 @@ export class ContextMenu extends SubMenu {
989
989
  * @property keepOpen -Reflects the `"keep-open"` attribute.
990
990
  * @property iconName - Reflects the `"icon-name"` attribute.
991
991
  * @property disabled - Reflects the `"disabled"` attribute.
992
+ * @property accessibleLabel - Sets the accessible name and tooltip on the internal button.
992
993
  * @attribute soft-menu - Whether to use the soft menu implementation.
993
994
  * @attribute keep-open - Whether the menu should stay open after an item is clicked.
994
995
  * @attribute icon-name - Name of the icon to display on the button.
@@ -1001,6 +1002,7 @@ export class MenuButton extends HTMLElement {
1001
1002
  readonly #shadow = this.attachShadow({mode: 'open'});
1002
1003
  #triggerTimeoutId?: number;
1003
1004
  #populateMenuCall?: (arg0: ContextMenu) => void;
1005
+ #accessibleLabel?: string;
1004
1006
 
1005
1007
  /**
1006
1008
  * Sets the callback function used to populate the context menu when the button is clicked.
@@ -1010,6 +1012,17 @@ export class MenuButton extends HTMLElement {
1010
1012
  this.#populateMenuCall = populateCall;
1011
1013
  }
1012
1014
 
1015
+ set accessibleLabel(accessibleLabel: string|undefined) {
1016
+ this.#accessibleLabel = accessibleLabel;
1017
+ if (this.iconName) {
1018
+ this.#render();
1019
+ }
1020
+ }
1021
+
1022
+ get accessibleLabel(): string|undefined {
1023
+ return this.#accessibleLabel;
1024
+ }
1025
+
1013
1026
  /**
1014
1027
  * Reflects the `soft-menu` attribute. If true, uses the `SoftContextMenu` implementation.
1015
1028
  * @default false
@@ -1120,13 +1133,15 @@ export class MenuButton extends HTMLElement {
1120
1133
  if (!this.iconName) {
1121
1134
  throw new Error('<devtools-menu-button> expects an icon.');
1122
1135
  }
1136
+ const accessibleLabel = this.accessibleLabel ?? this.title;
1123
1137
 
1124
1138
  // clang-format off
1125
1139
  render(html`
1126
1140
  <devtools-button .disabled=${this.disabled}
1127
1141
  .iconName=${this.iconName}
1128
1142
  .variant=${Buttons.Button.Variant.ICON}
1129
- .title=${this.title}
1143
+ .accessibleLabel=${accessibleLabel}
1144
+ .buttonTitle=${accessibleLabel}
1130
1145
  aria-haspopup='menu'
1131
1146
  @click=${this.#triggerContextMenu}>
1132
1147
  </devtools-button>`,
@@ -853,15 +853,16 @@ export class InspectorView extends VBox implements ViewLocationResolver {
853
853
  }
854
854
  }
855
855
 
856
- renderStatusBar(): void {
856
+ renderStatusBar(visible = true): void {
857
857
  if (!this.#statusBarContainer) {
858
858
  this.#statusBarContainer = document.createElement('div');
859
859
  this.#statusBarContainer.style.display = 'contents';
860
860
  this.element.appendChild(this.#statusBarContainer);
861
861
  }
862
862
  // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
863
- Lit.render(html`<devtools-widget class="flex-none" ${widget(StatusBarWidget)}></devtools-widget>`,
864
- this.#statusBarContainer);
863
+ Lit.render(
864
+ visible ? html`<devtools-widget class="flex-none" ${widget(StatusBarWidget)}></devtools-widget>` : Lit.nothing,
865
+ this.#statusBarContainer);
865
866
  }
866
867
 
867
868
  private createInfoBarDiv(): void {
@@ -233,19 +233,18 @@ interface PlusButtonViewInput {
233
233
  */
234
234
  export const PLUS_BUTTON_VIEW =
235
235
  (input: PlusButtonViewInput, output: {button?: MenuButton}, target: HTMLElement): void => {
236
- render(
237
- html`
236
+ render(html`
238
237
  <devtools-menu-button
239
238
  ${Directives.ref(el => {
240
- output.button = el as MenuButton | undefined;
241
- })}
239
+ output.button = el as MenuButton | undefined;
240
+ })}
242
241
  slot="trailing-button"
243
242
  .iconName=${'plus'}
244
- .title=${input.title}
243
+ .accessibleLabel=${input.title}
245
244
  .jslogContext=${input.jslogContext}
246
245
  .populateMenuCall=${input.populateMenuCall}>
247
246
  </devtools-menu-button>`,
248
- target);
247
+ target);
249
248
  };
250
249
 
251
250
  /**
@@ -77,6 +77,8 @@ export interface TabInfo {
77
77
  jslogContext?: string;
78
78
  enabled?: boolean;
79
79
  selected?: boolean;
80
+ icon?: Element|null;
81
+ suffix?: Element|null;
80
82
  }
81
83
 
82
84
  const TabbedPaneBase: Common.ObjectWrapper.EventMixin<EventTypes, typeof VBox> = Common.ObjectWrapper.eventMixin(
@@ -120,6 +122,8 @@ export class TabbedPane extends TabbedPaneBase {
120
122
  this.registerRequiredCSS(tabbedPaneStyles);
121
123
  this.element.classList.add('tabbed-pane');
122
124
  this.contentElement.classList.add('tabbed-pane-shadow');
125
+ this.element.classList.add('flex-auto', 'vbox');
126
+ this.contentElement.classList.add('flex-auto', 'vbox');
123
127
  this.contentElement.tabIndex = -1;
124
128
  this.setDefaultFocusedElement(this.contentElement);
125
129
  this.#headerElement = this.contentElement.createChild('div', 'tabbed-pane-header');
@@ -565,47 +569,6 @@ export class TabbedPane extends TabbedPaneBase {
565
569
  }));
566
570
  }
567
571
 
568
- set tabs(tabs: TabInfo[]) {
569
- const newIds = new Set(tabs.map(tab => tab.id));
570
- for (const id of this.tabsById.keys()) {
571
- if (!newIds.has(id)) {
572
- this.#closeTab(id);
573
- }
574
- }
575
- let index = 0;
576
- for (const tab of tabs) {
577
- const existingTab = this.tabsById.get(tab.id);
578
- if (existingTab) {
579
- this.changeTabView(tab.id, tab.view);
580
- this.changeTabTitle(tab.id, tab.title, tab.tabTooltip);
581
- if (tab.jslogContext !== undefined) {
582
- existingTab.jslogContext = tab.jslogContext;
583
- }
584
- if (tab.isCloseable !== undefined) {
585
- existingTab.closeable = tab.isCloseable;
586
- }
587
- if (tab.previewFeature !== undefined) {
588
- existingTab.previewFeature = tab.previewFeature;
589
- }
590
- const currentIndex = this.#tabs.indexOf(existingTab);
591
- if (currentIndex !== index) {
592
- this.insertBefore(existingTab, index);
593
- }
594
- } else {
595
- this.appendTab(
596
- tab.id, tab.title, tab.view, tab.tabTooltip, /* userGesture=*/ false, tab.isCloseable, tab.previewFeature,
597
- index, tab.jslogContext);
598
- }
599
- if (tab.enabled !== undefined) {
600
- this.setTabEnabled(tab.id, tab.enabled);
601
- }
602
- if (tab.selected) {
603
- this.selectTab(tab.id);
604
- }
605
- ++index;
606
- }
607
- }
608
-
609
572
  override onResize(): void {
610
573
  if (this.currentDevicePixelRatio !== window.devicePixelRatio) {
611
574
  // Force recalculation of all tab widths on a DPI change
@@ -1694,42 +1657,72 @@ export class TabbedPaneElement extends WidgetElement<TabbedPane> {
1694
1657
  this.#automaticReorder = automatic;
1695
1658
  this.getWidget()?.setAllowTabReorder(this.#allowTabReorder, this.#automaticReorder);
1696
1659
  }
1660
+
1661
+ get tabs(): TabInfo[] {
1662
+ const widget = Widget.getOrCreateWidget(this) as TabbedPane;
1663
+ if (widget) {
1664
+ this.#updateTabs(widget);
1665
+ return widget.tabs;
1666
+ }
1667
+ return [];
1668
+ }
1669
+
1670
+ #delegate?: TabbedPaneTabDelegate;
1671
+ set tabDelegate(delegate: TabbedPaneTabDelegate) {
1672
+ this.#delegate = delegate;
1673
+ this.getWidget()?.setTabDelegate(delegate);
1674
+ }
1675
+
1676
+ #placeholderElement?: Element;
1677
+ #managedTabIds = new Set<string>();
1678
+ set placeholder(element: Element) {
1679
+ this.#placeholderElement = element;
1680
+ this.getWidget()?.setPlaceholderElement(element);
1681
+ }
1682
+
1697
1683
  readonly #tabObserver = new MutationObserver(() => this.#updateTabs());
1698
1684
 
1699
1685
  constructor() {
1700
1686
  super();
1701
1687
 
1702
- registerWidgetConfig(this, widgetConfig(element => {
1703
- const widget = new TabbedPane(element as TabbedPaneElement);
1704
- widget.setCloseableTabs(this.#closeableTabs);
1705
- widget.setAllowTabReorder(this.#allowTabReorder, this.#automaticReorder);
1706
- const slot = widget.contentElement.querySelector('slot:not([name])');
1707
- if (slot) {
1708
- slot.addEventListener('slotchange', () => this.#syncTabs());
1709
- }
1710
- widget.addEventListener(Events.TabSelected, () => {
1711
- const slot =
1712
- widget.contentElement.querySelector('slot:not([name])') as HTMLSlotElement | null;
1713
- const nodes = slot ? slot.assignedElements() : [];
1714
- for (const child of nodes) {
1715
- if (child.id === widget.selectedTabId) {
1716
- child.setAttribute('selected', '');
1717
- } else {
1718
- child.removeAttribute('selected');
1719
- }
1720
- }
1721
- this.dispatchEvent(new CustomEvent('select', {detail: {tabId: widget.selectedTabId}}));
1722
- });
1723
- widget.addEventListener(Events.TabClosed, event => {
1724
- this.dispatchEvent(new CustomEvent('close', {detail: {tabId: event.data.tabId}}));
1725
- });
1726
- widget.addEventListener(Events.TabOrderChanged, event => {
1727
- this.dispatchEvent(new CustomEvent(
1728
- 'taborderchanged', {detail: {tabId: event.data.tabId, tabIds: widget.tabIds()}}));
1729
- });
1730
- this.#syncTabs(widget);
1731
- return widget;
1732
- }));
1688
+ registerWidgetConfig(
1689
+ this, widgetConfig(element => {
1690
+ const widget = new TabbedPane(element as TabbedPaneElement);
1691
+ widget.setCloseableTabs(this.#closeableTabs);
1692
+ widget.setAllowTabReorder(this.#allowTabReorder, this.#automaticReorder);
1693
+ if (this.#delegate) {
1694
+ widget.setTabDelegate(this.#delegate);
1695
+ }
1696
+ const slot = widget.contentElement.querySelector('slot:not([name])');
1697
+ if (slot) {
1698
+ slot.addEventListener('slotchange', () => this.#syncTabs());
1699
+ }
1700
+ widget.addEventListener(Events.TabSelected, event => {
1701
+ const nodes = this.#getTabNodes(widget);
1702
+ for (const child of nodes) {
1703
+ if (child.id === widget.selectedTabId) {
1704
+ child.setAttribute('selected', '');
1705
+ } else {
1706
+ child.removeAttribute('selected');
1707
+ }
1708
+ }
1709
+ this.dispatchEvent(new CustomEvent(
1710
+ 'select', {detail: {tabId: widget.selectedTabId, isUserGesture: event.data?.isUserGesture}}));
1711
+ });
1712
+ widget.addEventListener(Events.TabClosed, event => {
1713
+ this.dispatchEvent(
1714
+ new CustomEvent('close', {detail: {tabId: event.data.tabId, isUserGesture: event.data.isUserGesture}}));
1715
+ });
1716
+ widget.addEventListener(Events.TabOrderChanged, event => {
1717
+ this.dispatchEvent(
1718
+ new CustomEvent('taborderchanged', {detail: {tabId: event.data.tabId, tabIds: widget.tabIds()}}));
1719
+ });
1720
+ if (this.#placeholderElement) {
1721
+ widget.setPlaceholderElement(this.#placeholderElement);
1722
+ }
1723
+ this.#syncTabs(widget);
1724
+ return widget;
1725
+ }));
1733
1726
  }
1734
1727
 
1735
1728
  override disconnectedCallback(): void {
@@ -1747,8 +1740,7 @@ export class TabbedPaneElement extends WidgetElement<TabbedPane> {
1747
1740
 
1748
1741
  #updateObserver(widget: TabbedPane): void {
1749
1742
  this.#tabObserver.disconnect();
1750
- const slot = widget.contentElement.querySelector('slot:not([name])') as HTMLSlotElement | null;
1751
- const nodes = slot ? slot.assignedElements() : [];
1743
+ const nodes = this.#getTabNodes(widget);
1752
1744
  for (const child of nodes) {
1753
1745
  this.#tabObserver.observe(child, {
1754
1746
  attributes: true,
@@ -1757,13 +1749,18 @@ export class TabbedPaneElement extends WidgetElement<TabbedPane> {
1757
1749
  }
1758
1750
  }
1759
1751
 
1752
+ #getTabNodes(widget: TabbedPane): Element[] {
1753
+ return Array.from(widget.element.children)
1754
+ .filter(c => c.id !== '' && !c.hasAttribute('slot') && !c.classList.contains('tabbed-pane-header') &&
1755
+ !c.classList.contains('tabbed-pane-content'));
1756
+ }
1757
+
1760
1758
  #updateTabs(widget = this.getWidget()): void {
1761
1759
  if (!widget) {
1762
1760
  return;
1763
1761
  }
1764
1762
  const tabs: TabInfo[] = [];
1765
- const slot = widget.contentElement.querySelector('slot:not([name])') as HTMLSlotElement | null;
1766
- const nodes = slot ? slot.assignedElements() : [];
1763
+ const nodes = this.#getTabNodes(widget);
1767
1764
  for (const child of nodes) {
1768
1765
  const id = child.id;
1769
1766
  const title = child.getAttribute('title') || '';
@@ -1772,13 +1769,12 @@ export class TabbedPaneElement extends WidgetElement<TabbedPane> {
1772
1769
  const enabled = !child.hasAttribute('disabled');
1773
1770
  const isCloseable =
1774
1771
  child.hasAttribute('closeable') ? true : (child.hasAttribute('uncloseable') ? false : undefined);
1772
+ const icon = child.querySelector('[slot="icon"]') ?? undefined;
1773
+ const suffix = child.querySelector('[slot="suffix"]') ?? undefined;
1775
1774
  const view = Widget.getOrCreateWidget(child as HTMLElement);
1776
1775
  view.setHideOnDetach();
1777
1776
  if (widget.selectedTabId !== id) {
1778
- view.hideWidget();
1779
1777
  child.classList.add('hidden');
1780
- } else {
1781
- view.showWidget();
1782
1778
  }
1783
1779
  tabs.push({
1784
1780
  id,
@@ -1788,10 +1784,55 @@ export class TabbedPaneElement extends WidgetElement<TabbedPane> {
1788
1784
  selected,
1789
1785
  enabled,
1790
1786
  isCloseable,
1787
+ icon,
1788
+ suffix,
1791
1789
  });
1792
1790
  }
1793
1791
 
1794
- widget.tabs = tabs;
1792
+ const newIds = new Set(tabs.map(tab => tab.id));
1793
+ for (const id of this.#managedTabIds) {
1794
+ if (!newIds.has(id)) {
1795
+ widget.closeTab(id);
1796
+ }
1797
+ }
1798
+ this.#managedTabIds = newIds;
1799
+ let index = 0;
1800
+ for (const tab of tabs) {
1801
+ const existingTab = widget.tabsById.get(tab.id);
1802
+ if (existingTab) {
1803
+ widget.changeTabView(tab.id, tab.view);
1804
+ widget.changeTabTitle(tab.id, tab.title, tab.tabTooltip);
1805
+ if (tab.jslogContext !== undefined) {
1806
+ existingTab.jslogContext = tab.jslogContext;
1807
+ }
1808
+ if (tab.isCloseable !== undefined) {
1809
+ existingTab.closeable = tab.isCloseable;
1810
+ }
1811
+ if (tab.previewFeature !== undefined) {
1812
+ existingTab.previewFeature = tab.previewFeature;
1813
+ }
1814
+ const currentIndex = widget.tabIndex(tab.id);
1815
+ if (currentIndex !== index) {
1816
+ widget.insertBefore(existingTab, index);
1817
+ }
1818
+ } else {
1819
+ widget.appendTab(tab.id, tab.title, tab.view, tab.tabTooltip, /* userGesture=*/ false, tab.isCloseable,
1820
+ tab.previewFeature, index, tab.jslogContext);
1821
+ }
1822
+ if (tab.icon !== undefined) {
1823
+ widget.setTabIcon(tab.id, tab.icon as unknown as Icon);
1824
+ }
1825
+ if (tab.suffix !== undefined) {
1826
+ widget.setSuffixElement(tab.id, tab.suffix as unknown as HTMLElement);
1827
+ }
1828
+ if (tab.enabled !== undefined) {
1829
+ widget.setTabEnabled(tab.id, tab.enabled);
1830
+ }
1831
+ if (tab.selected) {
1832
+ widget.selectTab(tab.id);
1833
+ }
1834
+ ++index;
1835
+ }
1795
1836
  }
1796
1837
  }
1797
1838
 
@@ -60,6 +60,7 @@ import {
60
60
  HTMLElementWithLightDOMTemplate,
61
61
  isEditing,
62
62
  } from './UIUtils.js';
63
+ import {Widget} from './Widget.js';
63
64
 
64
65
  const UIStrings = {
65
66
  /**
@@ -1304,9 +1305,9 @@ export class TreeElement {
1304
1305
  }
1305
1306
  }
1306
1307
 
1307
- revealAndSelect(omitFocus?: boolean): void {
1308
+ revealAndSelect(omitFocus?: boolean, selectedByUser?: boolean): void {
1308
1309
  this.reveal(true);
1309
- this.select(omitFocus);
1310
+ this.select(omitFocus, selectedByUser ?? false);
1310
1311
  }
1311
1312
 
1312
1313
  deselect(): void {
@@ -1653,11 +1654,18 @@ class TreeViewTreeElement extends TreeElement {
1653
1654
  }
1654
1655
  this.#refreshScheduled = true;
1655
1656
  queueMicrotask(() => {
1656
- this.#refreshScheduled = false;
1657
- this.refresh();
1657
+ if (this.#refreshScheduled) {
1658
+ this.refresh();
1659
+ }
1658
1660
  });
1659
1661
  }
1660
1662
 
1663
+ flushPendingRefreshForTesting(): void {
1664
+ if (this.#refreshScheduled) {
1665
+ this.refresh();
1666
+ }
1667
+ }
1668
+
1661
1669
  updateAttributes(): void {
1662
1670
  const expandable = Boolean(this.configElement.querySelector(':scope > ul[role="group"]'));
1663
1671
  this.setExpandable(expandable);
@@ -1678,10 +1686,13 @@ class TreeViewTreeElement extends TreeElement {
1678
1686
  this.#clonedClasses.add(className);
1679
1687
  }
1680
1688
  this.hidden = hasBooleanAttribute(this.configElement, 'hidden');
1689
+ this.selectable =
1690
+ !this.configElement.hasAttribute('selectable') || hasBooleanAttribute(this.configElement, 'selectable');
1681
1691
  this.updateExpansionFromAttribute();
1682
1692
  }
1683
1693
 
1684
1694
  refresh(): void {
1695
+ this.#refreshScheduled = false;
1685
1696
  const hadFocus = this.listItemElement.hasFocus();
1686
1697
  this.titleElement.textContent = '';
1687
1698
  this.updateAttributes();
@@ -1724,7 +1735,7 @@ class TreeViewTreeElement extends TreeElement {
1724
1735
  return super.onenter();
1725
1736
  }
1726
1737
 
1727
- remove(): void {
1738
+ removeFromTree(): void {
1728
1739
  removeNode(this,
1729
1740
  Boolean(this.parent &&
1730
1741
  (this.parent as TreeViewTreeElement).configElement?.querySelector(':scope > ul[role="group"]')));
@@ -1890,6 +1901,26 @@ export class TreeViewElement extends HTMLElementWithLightDOMTemplate {
1890
1901
  return this.#treeOutline;
1891
1902
  }
1892
1903
 
1904
+ flushPendingUpdatesForTesting(): void {
1905
+ this.flushPendingMutationsForTesting();
1906
+ const stack: TreeElement[] = [...this.#treeOutline.rootElement().children()];
1907
+ while (stack.length > 0) {
1908
+ const item = stack.pop();
1909
+ if (!item) {
1910
+ continue;
1911
+ }
1912
+ if (item instanceof TreeViewTreeElement) {
1913
+ item.flushPendingRefreshForTesting();
1914
+ }
1915
+ if (item.children()) {
1916
+ stack.push(...item.children());
1917
+ }
1918
+ }
1919
+ for (const widgetEl of this.#treeOutline.shadowRoot.querySelectorAll('devtools-widget')) {
1920
+ void Widget.get(widgetEl)?.performUpdate();
1921
+ }
1922
+ }
1923
+
1893
1924
  override focus(): void {
1894
1925
  if (!this.#treeOutline.selectedTreeElement && this.#treeOutline.firstChild()) {
1895
1926
  this.#treeOutline.firstChild()?.select(/* omitFocus */ true, /* selectedByUser */ false);
@@ -2028,7 +2059,7 @@ export class TreeViewElement extends HTMLElementWithLightDOMTemplate {
2028
2059
  protected override removeNodes(nodes: NodeList|Node[]): void {
2029
2060
  for (const node of getTreeNodes(nodes)) {
2030
2061
  if (node instanceof HTMLLIElement) {
2031
- TreeViewTreeElement.get(node)?.remove();
2062
+ TreeViewTreeElement.get(node)?.removeFromTree();
2032
2063
  } else if (node.treeElement) {
2033
2064
  removeNode(node.treeElement,
2034
2065
  Boolean(node.treeElement.parent &&
@@ -2196,6 +2196,13 @@ export class HTMLElementWithLightDOMTemplate extends HTMLElement {
2196
2196
  render(template, this.#contentTemplate.content);
2197
2197
  }
2198
2198
 
2199
+ flushPendingMutationsForTesting(): void {
2200
+ const records = this.#mutationObserver.takeRecords();
2201
+ if (records.length > 0) {
2202
+ this.#onChange(records);
2203
+ }
2204
+ }
2205
+
2199
2206
  #onChange(mutationList: MutationRecord[]): void {
2200
2207
  this.onChange(mutationList);
2201
2208
  const addedNodes = new Set<Node>();
@@ -1454,3 +1454,36 @@ Node.prototype.removeChildren = function(): void {
1454
1454
  }
1455
1455
  return originalRemoveChildren.call(this);
1456
1456
  };
1457
+
1458
+ export interface WrapperWidgetParams {
1459
+ widget: Widget;
1460
+ }
1461
+
1462
+ export class WrapperWidget extends Widget {
1463
+ #widget: Widget|null = null;
1464
+ constructor(element: HTMLElement, _deps: never[], params?: WrapperWidgetParams) {
1465
+ super(element);
1466
+ this.element.style.setProperty('display', 'contents');
1467
+ if (params?.widget) {
1468
+ this.widget = params.widget;
1469
+ }
1470
+ }
1471
+
1472
+ set widget(widget: Widget|null) {
1473
+ if (this.#widget === widget) {
1474
+ return;
1475
+ }
1476
+ if (this.#widget) {
1477
+ this.#widget.detach();
1478
+ }
1479
+ this.#widget = widget;
1480
+ if (this.#widget) {
1481
+ this.#widget.show(this.element, undefined, /* suppressOrphanWidgetError */ true);
1482
+ }
1483
+ }
1484
+ override focus(): void {
1485
+ if (this.#widget) {
1486
+ this.#widget.focus();
1487
+ }
1488
+ }
1489
+ }