chrome-devtools-frontend 1.0.1628368 → 1.0.1629211

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 (31) hide show
  1. package/front_end/core/host/UserMetrics.ts +0 -1
  2. package/front_end/core/root/ExperimentNames.ts +0 -2
  3. package/front_end/core/sdk/DOMModel.ts +0 -5
  4. package/front_end/entrypoints/greendev_floaty/FloatyEntrypoint.ts +0 -2
  5. package/front_end/entrypoints/greendev_floaty/greendev_floaty.ts +0 -2
  6. package/front_end/entrypoints/main/MainImpl.ts +0 -3
  7. package/front_end/generated/InspectorBackendCommands.ts +2 -2
  8. package/front_end/generated/protocol.ts +5 -0
  9. package/front_end/models/emulation/DeviceModeModel.ts +4 -0
  10. package/front_end/models/issues_manager/GenericIssue.ts +18 -1
  11. package/front_end/models/issues_manager/descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
  12. package/front_end/panels/common/freDialog.css +1 -0
  13. package/front_end/panels/elements/StylesSidebarPane.ts +1 -1
  14. package/front_end/panels/elements/elements-meta.ts +0 -25
  15. package/front_end/panels/elements/elements.ts +0 -3
  16. package/front_end/panels/emulation/DeviceModeToolbar.ts +335 -248
  17. package/front_end/panels/profiler/ProfilesPanel.ts +7 -1
  18. package/front_end/panels/sources/WatchExpressionsSidebarPane.ts +29 -18
  19. package/front_end/panels/sources/sources-meta.ts +7 -0
  20. package/front_end/third_party/chromium/README.chromium +1 -1
  21. package/front_end/ui/legacy/TabbedPane.ts +142 -16
  22. package/front_end/ui/legacy/TextPrompt.docs.ts +51 -0
  23. package/front_end/ui/legacy/TextPrompt.ts +55 -6
  24. package/front_end/ui/legacy/Toolbar.ts +2 -1
  25. package/front_end/ui/legacy/View.ts +2 -1
  26. package/front_end/ui/legacy/ViewManager.ts +11 -5
  27. package/front_end/ui/legacy/textPrompt.css +4 -0
  28. package/mcp/mcp.ts +1 -0
  29. package/package.json +1 -1
  30. package/front_end/panels/elements/NodeStackTraceWidget.ts +0 -76
  31. package/front_end/panels/elements/nodeStackTraceWidget.css +0 -11
@@ -37,6 +37,7 @@ import {createIcon} from '../../ui/kit/kit.js';
37
37
  // eslint-disable-next-line @devtools/es-modules-import
38
38
  import objectValueStyles from '../../ui/legacy/components/object_ui/objectValue.css.js';
39
39
  import * as UI from '../../ui/legacy/legacy.js';
40
+ import {render} from '../../ui/lit/lit.js';
40
41
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
41
42
 
42
43
  import {
@@ -473,7 +474,12 @@ export class ProfilesPanel extends UI.Panel.PanelWithSidebar implements DataDisp
473
474
  this.profileViewToolbar.removeToolbarItems();
474
475
 
475
476
  void (view as unknown as UI.View.View).toolbarItems().then(items => {
476
- items.map(item => this.profileViewToolbar.appendToolbarItem(item));
477
+ if (Array.isArray(items)) {
478
+ items.map(item => this.profileViewToolbar.appendToolbarItem(item));
479
+ } else {
480
+ // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
481
+ render(items, this.profileViewToolbar);
482
+ }
477
483
  });
478
484
 
479
485
  return view;
@@ -49,7 +49,7 @@ import * as ObjectUI from '../../ui/legacy/components/object_ui/object_ui.js';
49
49
  import objectValueStyles from '../../ui/legacy/components/object_ui/objectValue.css.js';
50
50
  import * as Components from '../../ui/legacy/components/utils/utils.js';
51
51
  import * as UI from '../../ui/legacy/legacy.js';
52
- import {Directives, html, render} from '../../ui/lit/lit.js';
52
+ import {Directives, html, render, type TemplateResult} from '../../ui/lit/lit.js';
53
53
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
54
54
 
55
55
  import {UISourceCodeFrame} from './UISourceCodeFrame.js';
@@ -179,8 +179,6 @@ export class WatchExpressionsSidebarPane extends UI.Widget.VBox implements UI.Ac
179
179
  UI.Toolbar.ItemsProvider {
180
180
  #watchExpressions: WatchExpression[];
181
181
  #watchExpressionsSetting: Common.Settings.Setting<string[]>;
182
- private readonly addButton: UI.Toolbar.ToolbarButton;
183
- private readonly refreshButton: UI.Toolbar.ToolbarButton;
184
182
  private readonly linkifier: Components.Linkifier.Linkifier;
185
183
  #view: View;
186
184
  #expandControllers = new Map<string, ObjectUI.ObjectPropertiesSection.ObjectTreeExpansionTracker>();
@@ -192,17 +190,6 @@ export class WatchExpressionsSidebarPane extends UI.Widget.VBox implements UI.Ac
192
190
  this.#watchExpressionsSetting =
193
191
  Common.Settings.Settings.instance().createLocalSetting<string[]>('watch-expressions', []);
194
192
 
195
- this.addButton = new UI.Toolbar.ToolbarButton(
196
- i18nString(UIStrings.addWatchExpression), 'plus', undefined, 'add-watch-expression');
197
- this.addButton.setSize(Buttons.Button.Size.SMALL);
198
- this.addButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, _event => {
199
- void this.addButtonClicked();
200
- });
201
- this.refreshButton = new UI.Toolbar.ToolbarButton(
202
- i18nString(UIStrings.refreshWatchExpressions), 'refresh', undefined, 'refresh-watch-expressions');
203
- this.refreshButton.setSize(Buttons.Button.Size.SMALL);
204
- this.refreshButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, this.#refreshExpressions, this);
205
-
206
193
  UI.Context.Context.instance().addFlavorChangeListener(
207
194
  SDK.RuntimeModel.ExecutionContext, this.#refreshExpressions, this);
208
195
  UI.Context.Context.instance().addFlavorChangeListener(
@@ -223,8 +210,27 @@ export class WatchExpressionsSidebarPane extends UI.Widget.VBox implements UI.Ac
223
210
  return this.#watchExpressions;
224
211
  }
225
212
 
226
- toolbarItems(): UI.Toolbar.ToolbarItem[] {
227
- return [this.addButton, this.refreshButton];
213
+ toolbarItems(): TemplateResult {
214
+ // clang-format off
215
+ return html`
216
+ <devtools-button .data=${{
217
+ variant: Buttons.Button.Variant.TOOLBAR,
218
+ iconName: 'plus',
219
+ size: Buttons.Button.Size.SMALL,
220
+ title: i18nString(UIStrings.addWatchExpression),
221
+ jslogContext: 'add-watch-expression',
222
+ } as Buttons.Button.ButtonData}
223
+ @click=${(e: Event) => this.addButtonClicked(e)}></devtools-button>
224
+ <devtools-button .data=${{
225
+ variant: Buttons.Button.Variant.TOOLBAR,
226
+ iconName: 'refresh',
227
+ size: Buttons.Button.Size.SMALL,
228
+ title: i18nString(UIStrings.refreshWatchExpressions),
229
+ jslogContext: 'refresh-watch-expressions',
230
+ } as Buttons.Button.ButtonData}
231
+ @click=${(e: Event) => this.refreshButtonClicked(e)}></devtools-button>
232
+ `;
233
+ // clang-format on
228
234
  }
229
235
 
230
236
  private saveExpressions(): void {
@@ -239,7 +245,8 @@ export class WatchExpressionsSidebarPane extends UI.Widget.VBox implements UI.Ac
239
245
  this.#watchExpressionsSetting.set(toSave);
240
246
  }
241
247
 
242
- private async addButtonClicked(): Promise<void> {
248
+ private async addButtonClicked(event?: Event): Promise<void> {
249
+ event?.consume(true);
243
250
  await UI.ViewManager.ViewManager.instance().showView('sources.watch');
244
251
  const watchExpression = this.createWatchExpression(null);
245
252
  this.requestUpdate();
@@ -250,6 +257,11 @@ export class WatchExpressionsSidebarPane extends UI.Widget.VBox implements UI.Ac
250
257
  watchExpression.startEditing();
251
258
  }
252
259
 
260
+ private refreshButtonClicked(event: Event): void {
261
+ event.consume(true);
262
+ this.#refreshExpressions();
263
+ }
264
+
253
265
  #refreshExpressions(): void {
254
266
  this.linkifier.reset();
255
267
  for (const expression of this.#watchExpressions) {
@@ -407,7 +419,6 @@ export class WatchExpression extends Common.ObjectWrapper.ObjectWrapper<EventTyp
407
419
  get result(): SDK.RemoteObject.RemoteObject|null {
408
420
  return this.#result ?? null;
409
421
  }
410
-
411
422
  get updateComplete(): Promise<void> {
412
423
  return this.#updateComplete;
413
424
  }
@@ -1076,6 +1076,13 @@ UI.ActionRegistration.registerActionExtension({
1076
1076
  UI.ActionRegistration.KeybindSet.VS_CODE,
1077
1077
  ],
1078
1078
  },
1079
+ {
1080
+ shortcut: 'Alt+g',
1081
+ keybindSets: [
1082
+ UI.ActionRegistration.KeybindSet.DEVTOOLS_DEFAULT,
1083
+ UI.ActionRegistration.KeybindSet.VS_CODE,
1084
+ ],
1085
+ },
1079
1086
  ],
1080
1087
  });
1081
1088
 
@@ -1,7 +1,7 @@
1
1
  Name: Dependencies sourced from the upstream `chromium` repository
2
2
  URL: Internal
3
3
  Version: N/A
4
- Revision: 06942485b8753eb50f0a5324c6e1329f955634c8
4
+ Revision: f24b2d64e5f1e00acb4dadb6becaa6aef859c307
5
5
  Update Mechanism: Manual (https://crbug.com/428069060)
6
6
  License: BSD-3-Clause
7
7
  License File: LICENSE
@@ -110,6 +110,8 @@ export class TabbedPane extends Common.ObjectWrapper.eventMixin<EventTypes, type
110
110
  private measuredDropDownButtonWidth?: number;
111
111
  #leftToolbar?: Toolbar;
112
112
  #rightToolbar?: Toolbar;
113
+ #trailingSlot!: HTMLSlotElement;
114
+ #lastDispatchedHiddenTabIds = '';
113
115
  allowTabReorder?: boolean;
114
116
  private automaticReorder?: boolean;
115
117
 
@@ -144,6 +146,25 @@ export class TabbedPane extends Common.ObjectWrapper.eventMixin<EventTypes, type
144
146
  this.tabSlider = document.createElement('div');
145
147
  this.tabSlider.classList.add('tabbed-pane-tab-slider');
146
148
  this.tabsElement = this.headerContentsElement.createChild('div', 'tabbed-pane-header-tabs');
149
+
150
+ // Declarative slot for a trailing affordance (e.g. a plus button).
151
+ // When something is assigned to this slot, the legacy overflow
152
+ // chevron is suppressed and the slotted element's width is reserved
153
+ // during overflow detection so it always stays visible. The slot
154
+ // itself has `display: contents` and so contributes no layout box
155
+ // when empty.
156
+ this.#trailingSlot = document.createElement('slot');
157
+ this.#trailingSlot.name = 'trailing-button';
158
+ this.#trailingSlot.classList.add('tabbed-pane-trailing-button');
159
+ this.headerContentsElement.appendChild(this.#trailingSlot);
160
+ this.#trailingSlot.addEventListener('slotchange', () => this.requestUpdate());
161
+
162
+ // Re-run overflow detection whenever the available space for tabs
163
+ // changes. The header is a flex container in which `headerContentsElement`
164
+ // is `flex: auto`, so any growth in the left/right toolbars (e.g. the
165
+ // issues counter loading asynchronously into the right toolbar) shrinks
166
+ // `headerContentsElement` and is captured by this single observer.
167
+ new ResizeObserver(() => this.requestUpdate()).observe(this.headerContentsElement);
147
168
  this.tabsElement.setAttribute('role', 'tablist');
148
169
  this.tabsElement.addEventListener('keydown', this.keyDown.bind(this), false);
149
170
  this.#contentElement = this.contentElement.createChild('div', 'tabbed-pane-content');
@@ -737,15 +758,6 @@ export class TabbedPane extends Common.ObjectWrapper.eventMixin<EventTypes, type
737
758
  }
738
759
  }
739
760
 
740
- private showTabElement(index: number, tab: TabbedPaneTab): void {
741
- if (index >= this.tabsElement.children.length) {
742
- this.tabsElement.appendChild(tab.tabElement);
743
- } else {
744
- this.tabsElement.insertBefore(tab.tabElement, this.tabsElement.children[index]);
745
- }
746
- tab.shown = true;
747
- }
748
-
749
761
  private hideTabElement(tab: TabbedPaneTab): void {
750
762
  this.tabsElement.removeChild(tab.tabElement);
751
763
  tab.shown = false;
@@ -831,8 +843,16 @@ export class TabbedPane extends Common.ObjectWrapper.eventMixin<EventTypes, type
831
843
  }
832
844
 
833
845
  private updateTabsDropDown(): void {
834
- const tabsToShowIndexes =
835
- this.tabsToShowIndexes(this.#tabs, this.tabsHistory, this.totalWidth(), this.measuredDropDownButtonWidth || 0);
846
+ // Reserve width for the trailing affordance so it never gets clipped
847
+ // or pushed past the right edge. When a `trailing-button` slot is
848
+ // populated (e.g. with a plus button), reserve that element's width
849
+ // and never use the legacy chevron. Otherwise, fall back to the
850
+ // legacy chevron behavior: reserve its measured width only when at
851
+ // least one tab is overflowed.
852
+ const slottedTrailing = this.#trailingSlot.assignedElements()[0];
853
+ const reservedWidth =
854
+ slottedTrailing ? slottedTrailing.getBoundingClientRect().width : (this.measuredDropDownButtonWidth || 0);
855
+ const tabsToShowIndexes = this.tabsToShowIndexes(this.#tabs, this.tabsHistory, this.totalWidth(), reservedWidth);
836
856
  if (this.lastSelectedOverflowTab && this.numberOfTabsShown() !== tabsToShowIndexes.length) {
837
857
  delete this.lastSelectedOverflowTab;
838
858
  this.updateTabsDropDown();
@@ -844,24 +864,121 @@ export class TabbedPane extends Common.ObjectWrapper.eventMixin<EventTypes, type
844
864
  this.hideTabElement(this.#tabs[i]);
845
865
  }
846
866
  }
867
+ // Synchronize the DOM order of the visible tab row to match
868
+ // `tabsToShowIndexes`. `insertBefore` moves a node if it is
869
+ // already in the DOM (preserving focus/selection) and inserts
870
+ // it otherwise, so a single pass handles both newly-visible
871
+ // tabs and reorderings of currently-visible tabs without any
872
+ // imperative DOM manipulation from callers like `moveTab`.
847
873
  for (let i = 0; i < tabsToShowIndexes.length; ++i) {
848
874
  const tab = this.#tabs[tabsToShowIndexes[i]];
849
- if (!tab.shown) {
850
- this.showTabElement(i, tab);
875
+ if (this.tabsElement.children[i] !== tab.tabElement) {
876
+ this.tabsElement.insertBefore(tab.tabElement, this.tabsElement.children[i] ?? null);
851
877
  }
878
+ tab.shown = true;
852
879
  }
853
880
 
854
881
  this.maybeShowDropDown(tabsToShowIndexes.length !== this.#tabs.length);
882
+ this.#dispatchOverflowTabsChangedIfNeeded();
855
883
  }
856
884
 
857
885
  private maybeShowDropDown(hasMoreTabs: boolean): void {
858
- if (hasMoreTabs && !this.dropDownButton.parentElement) {
886
+ // The legacy chevron is suppressed when a trailing-button slot is
887
+ // populated; the slotted element takes its place.
888
+ const shouldShow = this.#trailingSlot.assignedElements().length === 0 && hasMoreTabs;
889
+ if (shouldShow && !this.dropDownButton.parentElement) {
859
890
  this.headerContentsElement.appendChild(this.dropDownButton);
860
- } else if (!hasMoreTabs && this.dropDownButton.parentElement) {
891
+ } else if (!shouldShow && this.dropDownButton.parentElement) {
861
892
  this.headerContentsElement.removeChild(this.dropDownButton);
862
893
  }
863
894
  }
864
895
 
896
+ /**
897
+ * Dispatches an `overflow-tabs-changed` DOM event whenever the set of
898
+ * hidden (overflowed) tabs changes. The event bubbles and is composed
899
+ * so consumers slotted into `trailing-button` can listen via the
900
+ * standard `addEventListener` / lit-html `@overflow-tabs-changed=`
901
+ * syntax. Payload (`event.detail.hiddenTabs`) lists hidden tabs in
902
+ * tab order.
903
+ *
904
+ * `updateTabsDropDown()` runs on every layout/resize tick (including
905
+ * ResizeObserver callbacks on the toolbars), but in the steady state
906
+ * the overflow set rarely changes. Dedup against the last dispatched
907
+ * tab-id list so consumers (e.g. a plus button rendering the hidden
908
+ * tabs in its menu) are not re-notified on every paint.
909
+ */
910
+ #dispatchOverflowTabsChangedIfNeeded(): void {
911
+ const hidden = this.hiddenTabs();
912
+ const key = hidden.map(t => t.id).join('\u0000');
913
+ if (key === this.#lastDispatchedHiddenTabIds) {
914
+ return;
915
+ }
916
+ this.#lastDispatchedHiddenTabIds = key;
917
+ this.dispatchDOMEvent(new CustomEvent('overflow-tabs-changed', {
918
+ bubbles: true,
919
+ composed: true,
920
+ detail: {hiddenTabs: hidden},
921
+ }));
922
+ }
923
+
924
+ /**
925
+ * Returns the tabs that are currently hidden because of overflow, in
926
+ * tab order. Mirrors the payload of the `overflow-tabs-changed` event.
927
+ */
928
+ hiddenTabs(): Array<{id: string, title: string, jslogContext?: string}> {
929
+ return this.#tabs.filter(tab => !tab.shown).map(tab => ({
930
+ id: tab.id,
931
+ title: tab.title,
932
+ jslogContext: tab.jslogContext,
933
+ }));
934
+ }
935
+
936
+ /**
937
+ * Returns the index in tab order of the first overflowed (hidden) tab,
938
+ * or -1 if every tab fits.
939
+ */
940
+ firstHiddenTabIndex(): number {
941
+ return this.#tabs.findIndex(tab => !tab.shown);
942
+ }
943
+
944
+ /**
945
+ * Reorders `tabId` to position `newIndex` in the tab list. Unlike
946
+ * {@link insertBefore}, this does not require the tab to currently be in
947
+ * the DOM (overflowed tabs are detached). The change is persisted via the
948
+ * {@link Events.TabOrderChanged} event so the new order survives across
949
+ * DevTools reloads.
950
+ */
951
+ moveTab(tabId: string, newIndex: number): void {
952
+ const tab = this.tabsById.get(tabId);
953
+ if (!tab) {
954
+ return;
955
+ }
956
+ const oldIndex = this.#tabs.indexOf(tab);
957
+ if (oldIndex === -1) {
958
+ return;
959
+ }
960
+ // `newIndex` is the desired final index of the tab. The maximum
961
+ // valid final index for an existing item is `length - 1`; clamp
962
+ // here so the no-op early return below catches calls like
963
+ // `moveTab(lastTabId, 999)` instead of doing redundant work and
964
+ // firing a spurious TabOrderChanged event.
965
+ const targetIndex = Math.max(0, Math.min(newIndex, this.#tabs.length - 1));
966
+ if (oldIndex === targetIndex) {
967
+ return;
968
+ }
969
+ this.#tabs.splice(oldIndex, 1);
970
+ // After the splice-remove the array is one shorter, so inserting at
971
+ // `targetIndex` lands the tab at exactly the requested final index
972
+ // regardless of move direction.
973
+ this.#tabs.splice(targetIndex, 0, tab);
974
+ // The visible row's DOM order is reconciled declaratively in
975
+ // `updateTabsDropDown()` (scheduled by `requestUpdate()` below),
976
+ // so no imperative DOM reordering is needed here.
977
+ const eventData: EventData = {tabId: tab.id, view: tab.view};
978
+ this.dispatchEventToListeners(Events.TabOrderChanged, eventData);
979
+ this.requestUpdate();
980
+ }
981
+
865
982
  private measureDropDownButton(): void {
866
983
  if (this.measuredDropDownButtonWidth) {
867
984
  return;
@@ -964,11 +1081,20 @@ export class TabbedPane extends Common.ObjectWrapper.eventMixin<EventTypes, type
964
1081
  if (this.lastSelectedOverflowTab !== undefined) {
965
1082
  tabsToLookAt.unshift(tabsToLookAt.splice(tabsToLookAt.indexOf(this.lastSelectedOverflowTab), 1)[0]);
966
1083
  }
1084
+ // The trailing affordance only steals space when it is shown. The
1085
+ // legacy chevron is conditional on actual overflow (so it can be
1086
+ // skipped on the very last tab if everything fits), but a slotted
1087
+ // trailing button (e.g. a plus button) is always rendered and must
1088
+ // therefore always be reserved for — even when the last tab would
1089
+ // otherwise just barely fit by itself — otherwise it gets pushed
1090
+ // past the right edge and clipped by the surrounding
1091
+ // `overflow: hidden` containers.
1092
+ const reserveOnLastTab = this.#trailingSlot.assignedElements().length > 0;
967
1093
  for (let i = 0; i < tabCount; ++i) {
968
1094
  const tab = this.automaticReorder ? tabsHistory[i] : tabsToLookAt[i];
969
1095
  totalTabsWidth += tab.width();
970
1096
  let minimalRequiredWidth = totalTabsWidth;
971
- if (i !== tabCount - 1) {
1097
+ if (i !== tabCount - 1 || reserveOnLastTab) {
972
1098
  minimalRequiredWidth += measuredDropDownButtonWidth;
973
1099
  }
974
1100
  if (!this.verticalTabLayout && minimalRequiredWidth > totalWidth) {
@@ -0,0 +1,51 @@
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
+ import './legacy.js';
6
+
7
+ import type * as Platform from '../../core/platform/platform.js';
8
+ import * as Lit from '../lit/lit.js';
9
+
10
+ import type * as UI from './legacy.js';
11
+
12
+ const {html} = Lit;
13
+
14
+ export function render(container: HTMLElement): void {
15
+ const validator = (text: string): null|Platform.UIString.LocalizedString => {
16
+ return text === 'invalid' ? 'This value is invalid' as Platform.UIString.LocalizedString : null;
17
+ };
18
+
19
+ Lit.render(
20
+ html`
21
+ <style>
22
+ .example {
23
+ margin: 20px;
24
+ padding: 20px;
25
+ border: 1px solid var(--sys-color-divider);
26
+ }
27
+ .label {
28
+ margin-bottom: 8px;
29
+ font-weight: bold;
30
+ }
31
+ </style>
32
+ <div class="example">
33
+ <div class="label">TextPrompt with validation (type 'invalid' and press Enter):</div>
34
+ <devtools-prompt
35
+ editing
36
+ .validator=${validator}
37
+ @commit=${(e: UI.TextPrompt.TextPromptElement.CommitEvent) => console.log('Committed:', e.detail)}
38
+ ></devtools-prompt>
39
+ </div>
40
+
41
+ <div class="example">
42
+ <div class="label">TextPrompt with cancel-on-blur:</div>
43
+ <devtools-prompt
44
+ editing
45
+ cancel-on-blur
46
+ @cancel=${() => console.log('Cancelled')}
47
+ >Initial content</devtools-prompt>
48
+ </div>
49
+ `,
50
+ container);
51
+ }
@@ -67,19 +67,39 @@ import {cloneCustomElement, ElementFocusRestorer} from './UIUtils.js';
67
67
  * large.
68
68
  */
69
69
  export class TextPromptElement extends HTMLElement {
70
- static readonly observedAttributes = ['editing', 'completions', 'placeholder'];
70
+ static readonly observedAttributes = ['editing', 'completions', 'placeholder', 'cancel-on-blur'];
71
+ static formAssociated = true;
71
72
  readonly #shadow = this.attachShadow({mode: 'open'});
73
+ readonly #internals = this.attachInternals();
72
74
  readonly #entrypoint = this.#shadow.createChild('span');
73
75
  readonly #slot = this.#entrypoint.createChild('slot');
74
76
  readonly #textPrompt = new TextPrompt();
75
77
  #completionTimeout: number|null = null;
76
78
  #completionObserver = new MutationObserver(this.#onMutate.bind(this));
79
+ #validator?: (value: string) => null | Platform.UIString.LocalizedString;
80
+ #cancelOnBlur = false;
77
81
 
78
82
  constructor() {
79
83
  super();
80
84
  this.#textPrompt.initialize(this.#willAutoComplete.bind(this));
81
85
  }
82
86
 
87
+ set validator(v: (value: string) => (null | Platform.UIString.LocalizedString)) {
88
+ this.#validator = v;
89
+ }
90
+
91
+ get validator(): undefined|((value: string) => (null | Platform.UIString.LocalizedString)) {
92
+ return this.#validator;
93
+ }
94
+
95
+ set cancelOnBlur(cancelOnBlur: boolean) {
96
+ this.setAttribute('cancel-on-blur', `${cancelOnBlur}`);
97
+ }
98
+
99
+ get cancelOnBlur(): boolean {
100
+ return this.#cancelOnBlur;
101
+ }
102
+
83
103
  #onMutate(changes: MutationRecord[]): void {
84
104
  const listId = this.getAttribute('completions');
85
105
  if (!listId) {
@@ -103,17 +123,27 @@ export class TextPromptElement extends HTMLElement {
103
123
  this.#updateCompletions();
104
124
  }
105
125
  }
106
-
107
126
  attributeChangedCallback(name: string, oldValue: string|null, newValue: string|null): void {
108
127
  if (oldValue === newValue) {
109
128
  return;
110
129
  }
111
130
 
131
+ const isTruthy = (value: string|null): boolean => value !== null && value !== 'false';
132
+
112
133
  switch (name) {
134
+ case 'cancel-on-blur':
135
+ if (isTruthy(newValue)) {
136
+ this.#cancelOnBlur = true;
137
+ } else {
138
+ this.#cancelOnBlur = false;
139
+ }
140
+ break;
113
141
  case 'editing':
114
142
  if (this.isConnected) {
115
- if (newValue !== null && newValue !== 'false' && oldValue === null) {
116
- this.#startEditing();
143
+ if (isTruthy(newValue)) {
144
+ if (!isTruthy(oldValue)) {
145
+ this.#startEditing();
146
+ }
117
147
  } else {
118
148
  this.#stopEditing();
119
149
  }
@@ -166,9 +196,11 @@ export class TextPromptElement extends HTMLElement {
166
196
  }
167
197
  this.#slot.remove();
168
198
 
169
- const proxy = this.#textPrompt.attachAndStartEditing(placeholder, e => this.#done(e, /* commit=*/ true));
199
+ const proxy =
200
+ this.#textPrompt.attachAndStartEditing(placeholder, e => this.#done(e, /* commit=*/ !this.#cancelOnBlur));
170
201
  proxy.addEventListener('keydown', this.#editingValueKeyDown.bind(this));
171
202
  placeholder.getComponentSelection()?.selectAllChildren(placeholder);
203
+ this.#textPrompt.focus();
172
204
  }
173
205
 
174
206
  #stopEditing(): void {
@@ -183,10 +215,26 @@ export class TextPromptElement extends HTMLElement {
183
215
  }
184
216
  }
185
217
 
218
+ disconnectedCallback(): void {
219
+ if (this.hasAttribute('editing')) {
220
+ this.#stopEditing();
221
+ }
222
+ }
223
+
186
224
  #done(e: Event, commit: boolean): void {
187
225
  const target = e.target as HTMLElement;
188
226
  const text = target.textContent || '';
227
+ this.#internals.setValidity({});
189
228
  if (commit) {
229
+ const validationMessage = this.#validator?.(text) ?? '';
230
+ if (validationMessage) {
231
+ this.#internals.setValidity({customError: true}, validationMessage, this.#entrypoint);
232
+ }
233
+
234
+ if (!this.#internals.reportValidity()) {
235
+ return;
236
+ }
237
+
190
238
  this.dispatchEvent(new TextPromptElement.CommitEvent(text));
191
239
  } else {
192
240
  this.dispatchEvent(new TextPromptElement.CancelEvent());
@@ -198,6 +246,7 @@ export class TextPromptElement extends HTMLElement {
198
246
  if (event.handled || !(event instanceof KeyboardEvent)) {
199
247
  return;
200
248
  }
249
+ this.#internals.setValidity({});
201
250
 
202
251
  if (event.key === 'Enter') {
203
252
  this.#done(event, /* commit=*/ true);
@@ -226,7 +275,7 @@ export namespace TextPromptElement {
226
275
  super('commit', {detail});
227
276
  }
228
277
  }
229
- export class CancelEvent extends CustomEvent<string> {
278
+ export class CancelEvent extends CustomEvent<void> {
230
279
  constructor() {
231
280
  super('cancel');
232
281
  }
@@ -11,6 +11,7 @@ import * as Root from '../../core/root/root.js';
11
11
  import * as Buttons from '../../ui/components/buttons/buttons.js';
12
12
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
13
13
  import {createIcon} from '../kit/kit.js';
14
+ import type {TemplateResult} from '../lit/lit.js';
14
15
 
15
16
  import {type Action, Events as ActionEvents} from './ActionRegistration.js';
16
17
  import {ActionRegistry} from './ActionRegistry.js';
@@ -1204,7 +1205,7 @@ export interface Provider {
1204
1205
  }
1205
1206
 
1206
1207
  export interface ItemsProvider {
1207
- toolbarItems(): ToolbarItem[];
1208
+ toolbarItems(): ToolbarItem[]|TemplateResult;
1208
1209
  }
1209
1210
 
1210
1211
  export class ToolbarComboBox extends ToolbarItem<void, HTMLSelectElement> {
@@ -3,6 +3,7 @@
3
3
  // found in the LICENSE file.
4
4
 
5
5
  import * as Platform from '../../core/platform/platform.js';
6
+ import type {TemplateResult} from '../lit/lit.js';
6
7
 
7
8
  import type {TabbedPane} from './TabbedPane.js';
8
9
  import type {ToolbarItem, ToolbarMenuButton} from './Toolbar.js';
@@ -22,7 +23,7 @@ export interface View {
22
23
 
23
24
  isTransient(): boolean;
24
25
 
25
- toolbarItems(): Promise<ToolbarItem[]>;
26
+ toolbarItems(): Promise<ToolbarItem[]|TemplateResult>;
26
27
 
27
28
  widget(): Promise<AnyWidget>;
28
29
 
@@ -12,6 +12,7 @@ import * as Platform from '../../core/platform/platform.js';
12
12
  import type * as Root from '../../core/root/root.js';
13
13
  import type * as Foundation from '../../foundation/foundation.js';
14
14
  import {createIcon, type Icon} from '../kit/kit.js';
15
+ import {render, type TemplateResult} from '../lit/lit.js';
15
16
  import * as VisualLogging from '../visual_logging/visual_logging.js';
16
17
 
17
18
  import * as ARIAUtils from './ARIAUtils.js';
@@ -119,7 +120,7 @@ export class PreRegisteredView implements View {
119
120
  return this.viewRegistration.persistence;
120
121
  }
121
122
 
122
- async toolbarItems(): Promise<ToolbarItem[]> {
123
+ async toolbarItems(): Promise<ToolbarItem[]|TemplateResult> {
123
124
  if (!this.viewRegistration.hasToolbar) {
124
125
  return [];
125
126
  }
@@ -244,13 +245,18 @@ export class ViewManager extends Common.ObjectWrapper.ObjectWrapper<EventTypes>
244
245
  viewManagerInstance = undefined;
245
246
  }
246
247
 
247
- static createToolbar(toolbarItems: ToolbarItem[]): Element|null {
248
- if (!toolbarItems.length) {
248
+ static createToolbar(toolbarItems: ToolbarItem[]|TemplateResult): Element|null {
249
+ if (Array.isArray(toolbarItems) && !toolbarItems.length) {
249
250
  return null;
250
251
  }
251
252
  const toolbar = document.createElement('devtools-toolbar');
252
- for (const item of toolbarItems) {
253
- toolbar.appendToolbarItem(item);
253
+ if (Array.isArray(toolbarItems)) {
254
+ for (const item of toolbarItems) {
255
+ toolbar.appendToolbarItem(item);
256
+ }
257
+ } else {
258
+ // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
259
+ render(toolbarItems, toolbar);
254
260
  }
255
261
  return toolbar;
256
262
  }
@@ -37,6 +37,10 @@
37
37
  color: var(--sys-color-token-subtle) !important; /* stylelint-disable-line declaration-no-important */
38
38
  }
39
39
 
40
+ :host(:invalid) .text-prompt {
41
+ outline: var(--sys-color-error) auto var(--sys-size-1);
42
+ }
43
+
40
44
  .text-prompt[data-placeholder] {
41
45
  &:empty::before {
42
46
  content: attr(data-placeholder);
package/mcp/mcp.ts CHANGED
@@ -23,6 +23,7 @@ export type * as CDPConnection from '../front_end/core/protocol_client/CDPConnec
23
23
  export {ConnectionTransport} from '../front_end/core/protocol_client/ConnectionTransport.js';
24
24
  export * as ProtocolClient from '../front_end/core/protocol_client/protocol_client.js';
25
25
  export {DebuggerModel} from '../front_end/core/sdk/DebuggerModel.js';
26
+ export * as NetworkManager from '../front_end/core/sdk/NetworkManager.js';
26
27
  export {RuntimeModel} from '../front_end/core/sdk/RuntimeModel.js';
27
28
  export {Target} from '../front_end/core/sdk/Target.js';
28
29
  export {TargetManager} from '../front_end/core/sdk/TargetManager.js';
package/package.json CHANGED
@@ -105,5 +105,5 @@
105
105
  "ip-address": "10.1.0",
106
106
  "basic-ftp": "5.2.2"
107
107
  },
108
- "version": "1.0.1628368"
108
+ "version": "1.0.1629211"
109
109
  }