chrome-devtools-frontend 1.0.1516909 → 1.0.1518653

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 (94) hide show
  1. package/docs/checklist/README.md +2 -2
  2. package/docs/checklist/javascript.md +1 -1
  3. package/docs/contributing/README.md +1 -1
  4. package/docs/contributing/settings-experiments-features.md +9 -8
  5. package/docs/cookbook/devtools_on_devtools.md +2 -2
  6. package/docs/cookbook/localization.md +10 -10
  7. package/docs/devtools-protocol.md +9 -8
  8. package/docs/ecosystem/automatic_workspace_folders.md +3 -3
  9. package/docs/get_the_code.md +0 -2
  10. package/docs/styleguide/ux/components.md +166 -85
  11. package/docs/styleguide/ux/numbers.md +3 -4
  12. package/front_end/core/common/README.md +13 -12
  13. package/front_end/core/host/GdpClient.ts +16 -1
  14. package/front_end/core/host/UserMetrics.ts +4 -2
  15. package/front_end/core/root/Runtime.ts +13 -0
  16. package/front_end/core/sdk/CSSMatchedStyles.ts +5 -1
  17. package/front_end/entrypoints/main/MainImpl.ts +6 -3
  18. package/front_end/generated/InspectorBackendCommands.js +10 -7
  19. package/front_end/generated/SupportedCSSProperties.js +21 -7
  20. package/front_end/generated/protocol-mapping.d.ts +16 -1
  21. package/front_end/generated/protocol-proxy-api.d.ts +13 -1
  22. package/front_end/generated/protocol.ts +95 -0
  23. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +166 -49
  24. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.snapshot.txt +14 -181
  25. package/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts +13 -315
  26. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.snapshot.txt +224 -50
  27. package/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts +310 -11
  28. package/front_end/models/ai_assistance/performance/AIContext.ts +15 -2
  29. package/front_end/models/ai_code_completion/AiCodeCompletion.ts +17 -11
  30. package/front_end/models/badges/Badge.ts +8 -3
  31. package/front_end/models/badges/CodeWhispererBadge.ts +2 -4
  32. package/front_end/models/badges/StarterBadge.ts +2 -2
  33. package/front_end/models/badges/UserBadges.ts +21 -3
  34. package/front_end/models/javascript_metadata/NativeFunctions.js +1 -1
  35. package/front_end/models/trace/README.md +28 -1
  36. package/front_end/models/trace/handlers/UserTimingsHandler.ts +1 -1
  37. package/front_end/models/trace/helpers/Trace.ts +99 -43
  38. package/front_end/models/trace/types/TraceEvents.ts +9 -0
  39. package/front_end/panels/accessibility/ARIAAttributesView.ts +113 -191
  40. package/front_end/panels/accessibility/AccessibilityNodeView.ts +9 -9
  41. package/front_end/panels/accessibility/AccessibilitySubPane.ts +6 -4
  42. package/front_end/panels/accessibility/accessibilityProperties.css +2 -0
  43. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +16 -2
  44. package/front_end/panels/ai_assistance/components/ChatView.ts +9 -10
  45. package/front_end/panels/ai_assistance/components/PerformanceAgentMarkdownRenderer.ts +42 -0
  46. package/front_end/panels/common/AiCodeCompletionDisclaimer.ts +32 -9
  47. package/front_end/panels/common/AiCodeCompletionSummaryToolbar.ts +7 -1
  48. package/front_end/panels/common/BadgeNotification.ts +21 -5
  49. package/front_end/panels/common/GdpSignUpDialog.ts +18 -9
  50. package/front_end/panels/console/ConsolePrompt.ts +1 -1
  51. package/front_end/panels/console/ConsoleView.ts +6 -2
  52. package/front_end/panels/elements/ElementsPanel.ts +4 -0
  53. package/front_end/panels/elements/ElementsTreeElement.ts +18 -0
  54. package/front_end/panels/elements/ElementsTreeOutline.ts +13 -0
  55. package/front_end/panels/elements/StylePropertyTreeElement.ts +21 -6
  56. package/front_end/panels/media/TickingFlameChart.ts +1 -1
  57. package/front_end/panels/profiler/HeapSnapshotView.ts +34 -19
  58. package/front_end/panels/search/SearchResultsPane.ts +124 -128
  59. package/front_end/panels/search/SearchView.ts +24 -17
  60. package/front_end/panels/settings/components/SyncSection.ts +16 -8
  61. package/front_end/panels/sources/AiCodeCompletionPlugin.ts +6 -1
  62. package/front_end/panels/sources/SourcesPanel.ts +3 -0
  63. package/front_end/panels/timeline/AppenderUtils.ts +2 -2
  64. package/front_end/panels/timeline/ExtensionTrackAppender.ts +13 -4
  65. package/front_end/panels/timeline/GPUTrackAppender.ts +2 -1
  66. package/front_end/panels/timeline/InteractionsTrackAppender.ts +5 -1
  67. package/front_end/panels/timeline/LayoutShiftsTrackAppender.ts +2 -1
  68. package/front_end/panels/timeline/ThreadAppender.ts +12 -3
  69. package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +9 -4
  70. package/front_end/panels/timeline/TimelinePanel.ts +3 -2
  71. package/front_end/panels/timeline/TimelineUIUtils.ts +5 -4
  72. package/front_end/panels/timeline/TimingsTrackAppender.ts +6 -1
  73. package/front_end/panels/timeline/components/CPUThrottlingSelector.ts +95 -82
  74. package/front_end/panels/timeline/components/LiveMetricsView.ts +2 -2
  75. package/front_end/panels/timeline/components/cpuThrottlingSelector.css +17 -15
  76. package/front_end/panels/timeline/components/insights/BaseInsightComponent.ts +3 -0
  77. package/front_end/third_party/chromium/README.chromium +1 -1
  78. package/front_end/third_party/codemirror.next/chunk/codemirror.js +1 -1
  79. package/front_end/third_party/codemirror.next/chunk/codemirror.js.map +1 -1
  80. package/front_end/third_party/codemirror.next/codemirror.next.d.ts +6 -9
  81. package/front_end/third_party/codemirror.next/package.json +2 -1
  82. package/front_end/third_party/diff/README.chromium +1 -0
  83. package/front_end/ui/components/text_editor/config.ts +6 -7
  84. package/front_end/ui/components/tooltips/Tooltip.ts +70 -31
  85. package/front_end/ui/legacy/README.md +33 -24
  86. package/front_end/ui/legacy/SearchableView.ts +19 -26
  87. package/front_end/ui/legacy/TextPrompt.ts +166 -1
  88. package/front_end/ui/legacy/Treeoutline.ts +16 -2
  89. package/front_end/ui/legacy/UIUtils.ts +15 -2
  90. package/front_end/ui/legacy/XElement.ts +0 -43
  91. package/front_end/ui/legacy/components/perf_ui/FlameChart.ts +20 -4
  92. package/front_end/ui/visual_logging/KnownContextValues.ts +19 -6
  93. package/front_end/ui/visual_logging/README.md +43 -27
  94. package/package.json +1 -1
@@ -31,7 +31,7 @@ This folder contains the new trace engine that was first implemented for the Per
31
31
 
32
32
 
33
33
  ┌──────────────────▼─────────────────┐
34
- │const data = model.parsedTrace()│
34
+ │const data = model.parsedTrace()
35
35
  └────────────────────────────────────┘
36
36
  ```
37
37
 
@@ -98,3 +98,30 @@ The object returned from `parsedTrace()` is an object of key-value pairs where e
98
98
  // and so on for each enabled Handler
99
99
  }
100
100
  ```
101
+
102
+ ## Pairing begin & end events
103
+
104
+ Note: this detail is not useful if you are using the Trace Engine, but it is if you are working on it.
105
+
106
+ Trace events are often emitted as `begin` & `end` events to represent the lifetime of the event. These have the `b` and `e` phase.
107
+
108
+ When we find these events, we often try to pair them into what we call a "Synthetic" event. This is a trace event that doesn't exist in the raw trace, but one that we create to make it easier to deal with. This means we can represent a `b` & `e` pair as a single event rather than pass two events around.
109
+
110
+ When we pair these events, we look for an ID. Some events have a top level `id` field, others have a nested `id2.local` field (this is for various historical reasons). `getSyntheticId` in `Trace.Helpers` takes care of this, and tries to account for potential collisions by appending a few other pieces of metadata onto the ID.
111
+
112
+ This approach worked well until July 2025 when an upstream change in Perfetto [https://chromium.googlesource.com/external/github.com/google/perfetto.git/+/aef636b27ffbf379fd722e7798030da2c5c4d699] meant that Perfetto will try to minimise the amount of unique IDs it uses. A consequence of this change is that IDs can be reused by consecutive, non-overlapping events.
113
+
114
+ For example, take the following set of events:
115
+
116
+ ```
117
+ === E1 === === E2 ===
118
+ ```
119
+
120
+ These could have the same ID, because they do not overlap.
121
+
122
+ Whereas these events will have different IDs because otherwise you cannot reliably pair them up:
123
+
124
+ ```
125
+ === E1 ===
126
+ === E2 ===
127
+ ```
@@ -223,9 +223,9 @@ export async function finalize(): Promise<void> {
223
223
 
224
224
  export function data(): UserTimingsData {
225
225
  return {
226
+ consoleTimings: syntheticEvents.filter(e => e.cat === 'blink.console') as Types.Events.SyntheticConsoleTimingPair[],
226
227
  performanceMeasures: syntheticEvents.filter(e => e.cat === 'blink.user_timing') as
227
228
  Types.Events.SyntheticUserTimingPair[],
228
- consoleTimings: syntheticEvents.filter(e => e.cat === 'blink.console') as Types.Events.SyntheticConsoleTimingPair[],
229
229
  performanceMarks: performanceMarkEvents,
230
230
  timestampEvents,
231
231
  measureTraceByTraceId,
@@ -12,6 +12,7 @@ import {SyntheticEventsManager} from './SyntheticEvents.js';
12
12
  import {eventTimingsMicroSeconds} from './Timing.js';
13
13
 
14
14
  interface MatchingPairableAsyncEvents {
15
+ syntheticId: string;
15
16
  begin: Types.Events.PairableAsyncBegin|null;
16
17
  end: Types.Events.PairableAsyncEnd|null;
17
18
  instant?: Types.Events.PairableAsyncInstant[];
@@ -290,62 +291,113 @@ export function makeProfileCall(
290
291
  }
291
292
 
292
293
  /**
293
- * Matches beginning events with PairableAsyncEnd and PairableAsyncInstant (ASYNC_NESTABLE_INSTANT)
294
- * if provided, though currently only coming from Animations. Traces may contain multiple instant events so we need to
295
- * account for that.
294
+ * Matches beginning events with PairableAsyncEnd and PairableAsyncInstant
295
+ * if provided. Traces may contain multiple instant events so we need to
296
+ * account for that. Additionally we have seen cases where we might only have a
297
+ * begin event & instant event(s), with no end event. So we account for that
298
+ * situation also.
296
299
  *
297
- * @returns Map of the animation's ID to it's matching events.
300
+ * You might also like to read the models/trace/README.md which has some
301
+ * documentation on trace IDs. This is important as Perfetto will reuse trace
302
+ * IDs when emitting events (if they do not overlap). This means it's not as
303
+ * simple as grouping events by IDs. Instead, we group begin & instant events
304
+ * by ID as we find them. When we find end events, we then pop any matching
305
+ * begin/instant events off the stack and group those. That way, if we meet the
306
+ * same ID later on it doesn't cause us collisions.
307
+ *
308
+ * @returns An array of all the matched event groups, along with their ID. Note
309
+ * that two event groups can have the same ID if they were non-overlapping
310
+ * events. You cannot rely on ID being unique across a trace. The returned set
311
+ * of groups are NOT SORTED in any order.
298
312
  */
299
- export function matchEvents(unpairedEvents: Types.Events.PairableAsync[]): Map<string, MatchingPairableAsyncEvents> {
313
+ function matchEvents(unpairedEvents: Types.Events.PairableAsync[]): MatchingPairableAsyncEvents[] {
314
+ sortTraceEventsInPlace(unpairedEvents);
300
315
  // map to store begin and end of the event
301
- const matchedPairs = new Map<string, MatchingPairableAsyncEvents>();
316
+ const matches: MatchingPairableAsyncEvents[] = [];
302
317
 
303
- // looking for start and end
318
+ const beginEventsById = new Map<string, Types.Events.PairableAsyncBegin[]>();
319
+ const instantEventsById = new Map<string, Types.Events.PairableAsyncInstant[]>();
304
320
  for (const event of unpairedEvents) {
305
- const syntheticId = getSyntheticId(event);
306
- if (syntheticId === undefined) {
321
+ const id = getSyntheticId(event);
322
+ if (id === undefined) {
307
323
  continue;
308
324
  }
309
- // Create a synthetic id to prevent collisions across categories.
310
- // Console timings can be dispatched with the same id, so use the
311
- // event name as well to generate unique ids.
312
- const otherEventsWithID = Platform.MapUtilities.getWithDefault(matchedPairs, syntheticId, () => {
313
- return {begin: null, end: null, instant: []};
314
- });
315
-
316
- const isStartEvent = event.ph === Types.Events.Phase.ASYNC_NESTABLE_START;
317
- const isEndEvent = event.ph === Types.Events.Phase.ASYNC_NESTABLE_END;
318
- const isInstantEvent = event.ph === Types.Events.Phase.ASYNC_NESTABLE_INSTANT;
319
-
320
- if (isStartEvent) {
321
- otherEventsWithID.begin = event as Types.Events.PairableAsyncBegin;
322
- } else if (isEndEvent) {
323
- otherEventsWithID.end = event as Types.Events.PairableAsyncEnd;
324
- } else if (isInstantEvent) {
325
- if (!otherEventsWithID.instant) {
326
- otherEventsWithID.instant = [];
325
+ if (Types.Events.isPairableAsyncBegin(event)) {
326
+ const existingEvents = beginEventsById.get(id) ?? [];
327
+ existingEvents.push(event);
328
+ beginEventsById.set(id, existingEvents);
329
+ } else if (Types.Events.isPairableAsyncInstant(event)) {
330
+ const existingEvents = instantEventsById.get(id) ?? [];
331
+ existingEvents.push(event);
332
+ instantEventsById.set(id, existingEvents);
333
+ } else if (Types.Events.isPairableAsyncEnd(event)) {
334
+ // Find matching begin event by ID
335
+ const beginEventsWithMatchingId = beginEventsById.get(id) ?? [];
336
+ const beginEvent = beginEventsWithMatchingId.pop();
337
+ if (!beginEvent) {
338
+ continue;
327
339
  }
328
- otherEventsWithID.instant.push(event as Types.Events.PairableAsyncInstant);
340
+ const instantEventsWithMatchingId = instantEventsById.get(id) ?? [];
341
+ // Find all instant events after the begin event ts.
342
+ const instantEventsForThisGroup: Types.Events.PairableAsyncInstant[] = [];
343
+ while (instantEventsWithMatchingId.length > 0) {
344
+ if (instantEventsWithMatchingId[0].ts >= beginEvent.ts) {
345
+ const event = instantEventsWithMatchingId.pop();
346
+ if (event) {
347
+ instantEventsForThisGroup.push(event);
348
+ }
349
+ } else {
350
+ break;
351
+ }
352
+ }
353
+ const matchingGroup: MatchingPairableAsyncEvents = {
354
+ begin: beginEvent,
355
+ end: event,
356
+ instant: instantEventsForThisGroup,
357
+ syntheticId: id,
358
+ };
359
+ matches.push(matchingGroup);
329
360
  }
330
361
  }
331
- return matchedPairs;
362
+
363
+ // At this point we know we have paired up all the Begin & End & Instant
364
+ // events. But it is possible to see only begin & instant events with the
365
+ // same ID, and no end event. So now we do a second pass through our begin
366
+ // events to find any that did not have an end event. If we find some
367
+ // instant events for the begin event, we create a new group.
368
+ // Also, because there were no end events, we know that the IDs will be
369
+ // unique now; e.g. each key in the map should have no more than one item in
370
+ // it.
371
+ for (const [id, beginEvents] of beginEventsById) {
372
+ const beginEvent = beginEvents.pop();
373
+ if (!beginEvent) {
374
+ continue;
375
+ }
376
+ const matchingInstantEvents = instantEventsById.get(id);
377
+ if (matchingInstantEvents?.length) {
378
+ matches.push({
379
+ syntheticId: id,
380
+ begin: beginEvent,
381
+ end: null,
382
+ instant: matchingInstantEvents,
383
+ });
384
+ }
385
+ }
386
+
387
+ return matches;
332
388
  }
333
389
 
334
- function getSyntheticId(event: Types.Events.PairableAsync): string|undefined {
390
+ export function getSyntheticId(event: Types.Events.PairableAsync): string|undefined {
335
391
  const id = extractId(event);
336
392
  return id && `${event.cat}:${id}:${event.name}`;
337
393
  }
338
394
 
339
- export function createSortedSyntheticEvents<T extends Types.Events.PairableAsync>(
340
- matchedPairs: Map<string, {
341
- begin: Types.Events.PairableAsyncBegin | null,
342
- end: Types.Events.PairableAsyncEnd | null,
343
- instant?: Types.Events.PairableAsyncInstant[],
344
- }>,
345
- syntheticEventCallback?: (syntheticEvent: Types.Events.SyntheticEventPair<T>) => void,
395
+ function createSortedSyntheticEvents<T extends Types.Events.PairableAsync>(
396
+ matchedPairs: MatchingPairableAsyncEvents[],
346
397
  ): Array<Types.Events.SyntheticEventPair<T>> {
347
398
  const syntheticEvents: Array<Types.Events.SyntheticEventPair<T>> = [];
348
- for (const [id, eventsTriplet] of matchedPairs.entries()) {
399
+ for (const eventsTriplet of matchedPairs) {
400
+ const id = eventsTriplet.syntheticId;
349
401
  const beginEvent = eventsTriplet.begin;
350
402
  const endEvent = eventsTriplet.end;
351
403
  const instantEvents = eventsTriplet.instant;
@@ -399,17 +451,21 @@ export function createSortedSyntheticEvents<T extends Types.Events.PairableAsync
399
451
  // crbug.com/1472375
400
452
  continue;
401
453
  }
402
- syntheticEventCallback?.(event);
403
454
  syntheticEvents.push(event);
404
455
  }
405
- return syntheticEvents.sort((a, b) => a.ts - b.ts);
456
+ sortTraceEventsInPlace(syntheticEvents);
457
+ return syntheticEvents;
406
458
  }
407
459
 
408
- export function createMatchedSortedSyntheticEvents<T extends Types.Events.PairableAsync>(
409
- unpairedAsyncEvents: T[], syntheticEventCallback?: (syntheticEvent: Types.Events.SyntheticEventPair<T>) => void):
460
+ /**
461
+ * Groups up sets of async events into synthetic events.
462
+ * @param unpairedAsyncEvents the raw array of begin, end and async instant
463
+ * events. These MUST be sorted in timestamp ASC order.
464
+ */
465
+ export function createMatchedSortedSyntheticEvents<T extends Types.Events.PairableAsync>(unpairedAsyncEvents: T[]):
410
466
  Array<Types.Events.SyntheticEventPair<T>> {
411
467
  const matchedPairs = matchEvents(unpairedAsyncEvents);
412
- const syntheticEvents = createSortedSyntheticEvents<T>(matchedPairs, syntheticEventCallback);
468
+ const syntheticEvents = createSortedSyntheticEvents<T>(matchedPairs);
413
469
  return syntheticEvents;
414
470
  }
415
471
 
@@ -1358,6 +1358,15 @@ export interface PairableAsyncInstant extends PairableAsync {
1358
1358
  export interface PairableAsyncEnd extends PairableAsync {
1359
1359
  ph: Phase.ASYNC_NESTABLE_END;
1360
1360
  }
1361
+ export function isPairableAsyncBegin(e: Event): e is PairableAsyncBegin {
1362
+ return e.ph === Phase.ASYNC_NESTABLE_START;
1363
+ }
1364
+ export function isPairableAsyncEnd(e: Event): e is PairableAsyncEnd {
1365
+ return e.ph === Phase.ASYNC_NESTABLE_END;
1366
+ }
1367
+ export function isPairableAsyncInstant(e: Event): e is PairableAsyncInstant {
1368
+ return e.ph === Phase.ASYNC_NESTABLE_INSTANT;
1369
+ }
1361
1370
 
1362
1371
  export interface AnimationFrame extends PairableAsync {
1363
1372
  name: Name.ANIMATION_FRAME;
@@ -1,14 +1,15 @@
1
1
  // Copyright 2016 The Chromium Authors
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
- /* eslint-disable rulesdir/no-imperative-dom-api */
5
4
 
6
5
  import * as i18n from '../../core/i18n/i18n.js';
7
6
  import * as Platform from '../../core/platform/platform.js';
8
7
  import * as SDK from '../../core/sdk/sdk.js';
9
8
  import * as UI from '../../ui/legacy/legacy.js';
9
+ import * as Lit from '../../ui/lit/lit.js';
10
10
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
11
11
 
12
+ import accessibilityPropertiesStyles from './accessibilityProperties.css.js';
12
13
  import {AccessibilitySubPane} from './AccessibilitySubPane.js';
13
14
  import {ariaMetadata} from './ARIAMetadata.js';
14
15
 
@@ -24,214 +25,135 @@ const UIStrings = {
24
25
  } as const;
25
26
  const str_ = i18n.i18n.registerUIStrings('panels/accessibility/ARIAAttributesView.ts', UIStrings);
26
27
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
28
+ const {render, html} = Lit;
29
+
30
+ interface ViewInput {
31
+ propertyCompletions: Map<SDK.DOMModel.Attribute, string[]>;
32
+ onStartEditing: (attribute: SDK.DOMModel.Attribute) => void;
33
+ onCommitEditing: (attribute: SDK.DOMModel.Attribute, result: string) => void;
34
+ onCancelEditing: (attribute: SDK.DOMModel.Attribute) => void;
35
+ attributeBeingEdited: SDK.DOMModel.Attribute|null;
36
+ attributes: SDK.DOMModel.Attribute[];
37
+ }
38
+
39
+ type View = (input: ViewInput, output: object, target: HTMLElement) => void;
40
+ export const DEFAULT_VIEW: View = (input, output, target) => {
41
+ const MAX_CONTENT_LENGTH = 10000;
42
+
43
+ const onStartEditing = (attribute: SDK.DOMModel.Attribute, e: MouseEvent): void => {
44
+ e.consume(true);
45
+ input.onStartEditing(attribute);
46
+ };
47
+
48
+ const propertyCompletions = (attribute: SDK.DOMModel.Attribute): Lit.LitTemplate => {
49
+ const values = input.propertyCompletions.get(attribute);
50
+ if (!values?.length) {
51
+ return Lit.nothing;
52
+ }
53
+
54
+ return html`<datalist id=completions>
55
+ ${values.map(value => html`<option>${value}</option>`)}
56
+ </datalist>`;
57
+ };
58
+
59
+ render(
60
+ // clang-format off
61
+ input.attributes.length === 0 ?
62
+ html`
63
+ <style>${accessibilityPropertiesStyles}</style>
64
+ <devtools-widget
65
+ .widgetConfig=${UI.Widget.widgetConfig(UI.EmptyWidget.EmptyWidget,
66
+ {text: i18nString(UIStrings.noAriaAttributes)})}
67
+ class="gray-info-message info-message-overflow"></devtools-widget>` :
68
+ html`<devtools-tree
69
+ hide-overflow
70
+ .template=${html`
71
+ <ul role="tree">
72
+ ${input.attributes?.map(attribute => html`
73
+ <li role="treeitem">
74
+ <style>${accessibilityPropertiesStyles}</style>
75
+ <span class="ax-name monospace" @mousedown=${onStartEditing.bind(null, attribute)}>
76
+ ${attribute.name}
77
+ </span>
78
+ <span class="separator" @mousedown=${onStartEditing.bind(null, attribute)}>${':\xA0'}</span>
79
+ <devtools-prompt
80
+ completions=completions
81
+ class="monospace"
82
+ @mousedown=${onStartEditing.bind(null, attribute)}
83
+ .completionTimeout=${0}
84
+ ?editing=${input.attributeBeingEdited === attribute}
85
+ @commit=${(e: UI.TextPrompt.TextPromptElement.CommitEvent) =>
86
+ input.onCommitEditing(attribute, e.detail)}
87
+ @cancel=${() => input.onCancelEditing(attribute)}>
88
+ ${Platform.StringUtilities.trimMiddle(attribute.value, MAX_CONTENT_LENGTH)}
89
+ ${propertyCompletions(attribute)}
90
+ </devtools-prompt>
91
+ </li>`)}
92
+ </ul>
93
+ `}></devtools-tree>`,
94
+ // clang-format on
95
+ target);
96
+ };
97
+
27
98
  export class ARIAAttributesPane extends AccessibilitySubPane {
28
- private readonly noPropertiesInfo: Element;
29
- private readonly treeOutline: UI.TreeOutline.TreeOutline;
99
+ readonly #view: View;
100
+ #attributeBeingEdited: SDK.DOMModel.Attribute|null = null;
30
101
 
31
- constructor() {
102
+ constructor(view = DEFAULT_VIEW) {
32
103
  super({
33
104
  title: i18nString(UIStrings.ariaAttributes),
34
105
  viewId: 'aria-attributes',
35
106
  jslog: `${VisualLogging.section('aria-attributes')}`,
36
107
  });
37
108
 
38
- this.noPropertiesInfo = this.createInfo(i18nString(UIStrings.noAriaAttributes));
39
- this.treeOutline = this.createTreeOutline();
109
+ this.#view = view;
40
110
  }
41
111
 
42
112
  override setNode(node: SDK.DOMModel.DOMNode|null): void {
43
113
  super.setNode(node);
44
- this.treeOutline.removeChildren();
45
- if (!node) {
46
- return;
47
- }
48
- const target = node.domModel().target();
49
- const attributes = node.attributes();
50
- for (let i = 0; i < attributes.length; ++i) {
51
- const attribute = attributes[i];
52
- if (!this.isARIAAttribute(attribute)) {
53
- continue;
54
- }
114
+ this.requestUpdate();
115
+ }
55
116
 
56
- this.treeOutline.appendChild(new ARIAAttributesTreeElement(this, attribute, target));
57
- }
117
+ override performUpdate(): void {
118
+ const onStartEditing = (attribute: SDK.DOMModel.Attribute): void => {
119
+ this.#attributeBeingEdited = attribute;
120
+ this.requestUpdate();
121
+ };
122
+ const onCancelEditing = (attribute: SDK.DOMModel.Attribute): void => {
123
+ if (attribute === this.#attributeBeingEdited) {
124
+ this.#attributeBeingEdited = null;
125
+ }
126
+ this.requestUpdate();
127
+ };
128
+
129
+ const onCommitEditing = (attribute: SDK.DOMModel.Attribute, result: string): void => {
130
+ // Make the changes to the attribute
131
+ const node = this.node();
132
+ if (node && attribute.value !== result) {
133
+ node.setAttributeValue(attribute.name, result);
134
+ }
135
+ if (attribute === this.#attributeBeingEdited) {
136
+ this.#attributeBeingEdited = null;
137
+ }
138
+ this.requestUpdate();
139
+ };
58
140
 
59
- const foundAttributes = (this.treeOutline.rootElement().childCount() !== 0);
60
- this.noPropertiesInfo.classList.toggle('hidden', foundAttributes);
61
- this.treeOutline.element.classList.toggle('hidden', !foundAttributes);
62
- }
141
+ const attributes = this.node()?.attributes()?.filter(attribute => this.isARIAAttribute(attribute)) ?? [];
142
+ const propertyCompletions =
143
+ new Map(attributes.map(attribute => [attribute, ariaMetadata().valuesForProperty(attribute.name)]));
63
144
 
64
- getTreeOutlineForTesting(): Readonly<UI.TreeOutline.TreeOutline>|undefined {
65
- return this.treeOutline;
145
+ const input: ViewInput = {
146
+ attributeBeingEdited: this.#attributeBeingEdited,
147
+ attributes,
148
+ onStartEditing,
149
+ onCommitEditing,
150
+ onCancelEditing,
151
+ propertyCompletions,
152
+ };
153
+ this.#view(input, {}, this.contentElement);
66
154
  }
67
155
 
68
156
  private isARIAAttribute(attribute: SDK.DOMModel.Attribute): boolean {
69
157
  return SDK.DOMModel.ARIA_ATTRIBUTES.has(attribute.name);
70
158
  }
71
159
  }
72
-
73
- export class ARIAAttributesTreeElement extends UI.TreeOutline.TreeElement {
74
- private readonly parentPane: ARIAAttributesPane;
75
- private readonly attribute: SDK.DOMModel.Attribute;
76
- private nameElement?: HTMLSpanElement;
77
- private valueElement?: Element;
78
- private prompt?: ARIAAttributePrompt;
79
-
80
- constructor(parentPane: ARIAAttributesPane, attribute: SDK.DOMModel.Attribute, _target: SDK.Target.Target) {
81
- super('');
82
-
83
- this.parentPane = parentPane;
84
- this.attribute = attribute;
85
-
86
- this.selectable = false;
87
- }
88
-
89
- static createARIAValueElement(value: string): Element {
90
- const valueElement = document.createElement('span');
91
- valueElement.classList.add('monospace');
92
- // TODO(aboxhall): quotation marks?
93
- valueElement.setTextContentTruncatedIfNeeded(value || '');
94
- return valueElement;
95
- }
96
-
97
- override onattach(): void {
98
- this.populateListItem();
99
- this.listItemElement.addEventListener('click', this.mouseClick.bind(this));
100
- }
101
-
102
- getPromptForTesting(): Readonly<ARIAAttributePrompt>|undefined {
103
- return this.prompt;
104
- }
105
-
106
- private populateListItem(): void {
107
- this.listItemElement.removeChildren();
108
- this.appendNameElement(this.attribute.name);
109
- this.listItemElement.createChild('span', 'separator').textContent = ':\xA0';
110
- this.appendAttributeValueElement(this.attribute.value);
111
- }
112
-
113
- appendNameElement(name: string): void {
114
- this.nameElement = document.createElement('span');
115
- this.nameElement.textContent = name;
116
- this.nameElement.classList.add('ax-name');
117
- this.nameElement.classList.add('monospace');
118
- this.listItemElement.appendChild(this.nameElement);
119
- }
120
-
121
- appendAttributeValueElement(value: string): void {
122
- this.valueElement = ARIAAttributesTreeElement.createARIAValueElement(value);
123
- this.listItemElement.appendChild(this.valueElement);
124
- }
125
-
126
- private mouseClick(event: Event): void {
127
- if (event.target === this.listItemElement) {
128
- return;
129
- }
130
-
131
- event.consume(true);
132
-
133
- this.startEditing();
134
- }
135
-
136
- private startEditing(): void {
137
- const valueElement = this.valueElement;
138
-
139
- if (!valueElement || UI.UIUtils.isBeingEdited(valueElement)) {
140
- return;
141
- }
142
-
143
- const previousContent = valueElement.textContent || '';
144
-
145
- function blurListener(this: ARIAAttributesTreeElement, previousContent: string, event: Event): void {
146
- const target = event.target as HTMLElement;
147
- const text = target.textContent || '';
148
- this.editingCommitted(text, previousContent);
149
- }
150
-
151
- const attributeName = (this.nameElement as HTMLSpanElement).textContent || '';
152
- this.prompt = new ARIAAttributePrompt(ariaMetadata().valuesForProperty(attributeName));
153
- this.prompt.setAutocompletionTimeout(0);
154
- const proxyElement =
155
- this.prompt.attachAndStartEditing(valueElement, blurListener.bind(this, previousContent)) as HTMLElement;
156
-
157
- proxyElement.addEventListener('keydown', event => this.editingValueKeyDown(previousContent, event), false);
158
-
159
- const selection = valueElement.getComponentSelection();
160
- if (selection) {
161
- selection.selectAllChildren(valueElement);
162
- }
163
- }
164
-
165
- private removePrompt(): void {
166
- if (!this.prompt) {
167
- return;
168
- }
169
- this.prompt.detach();
170
- delete this.prompt;
171
- }
172
-
173
- private editingCommitted(userInput: string, previousContent: string): void {
174
- this.removePrompt();
175
-
176
- // Make the changes to the attribute
177
- if (userInput !== previousContent) {
178
- const node = this.parentPane.node() as SDK.DOMModel.DOMNode;
179
- node.setAttributeValue(this.attribute.name, userInput);
180
- }
181
- }
182
-
183
- private editingCancelled(): void {
184
- this.removePrompt();
185
- this.populateListItem();
186
- }
187
-
188
- private editingValueKeyDown(previousContent: string, event: KeyboardEvent): void {
189
- if (event.handled) {
190
- return;
191
- }
192
-
193
- if (event.key === 'Enter') {
194
- const target = event.target as HTMLElement;
195
- this.editingCommitted(target.textContent || '', previousContent);
196
- event.consume();
197
- return;
198
- }
199
-
200
- if (Platform.KeyboardUtilities.isEscKey(event)) {
201
- this.editingCancelled();
202
- event.consume();
203
- return;
204
- }
205
- }
206
- }
207
-
208
- export class ARIAAttributePrompt extends UI.TextPrompt.TextPrompt {
209
- private readonly ariaCompletions: string[];
210
- constructor(ariaCompletions: string[]) {
211
- super();
212
- this.initialize(this.buildPropertyCompletions.bind(this));
213
-
214
- this.ariaCompletions = ariaCompletions;
215
- }
216
-
217
- private async buildPropertyCompletions(expression: string, prefix: string, force?: boolean):
218
- Promise<UI.SuggestBox.Suggestions> {
219
- prefix = prefix.toLowerCase();
220
- if (!prefix && !force && expression) {
221
- return [];
222
- }
223
- return this.ariaCompletions.filter(value => value.startsWith(prefix)).map(c => {
224
- return {
225
- text: c,
226
- title: undefined,
227
- subtitle: undefined,
228
- priority: undefined,
229
- isSecondary: undefined,
230
- subtitleRenderer: undefined,
231
- selectionRange: undefined,
232
- hideGhostText: undefined,
233
- iconElement: undefined,
234
- };
235
- });
236
- }
237
- }
@@ -120,8 +120,8 @@ const str_ = i18n.i18n.registerUIStrings('panels/accessibility/AccessibilityNode
120
120
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
121
121
  export class AXNodeSubPane extends AccessibilitySubPane {
122
122
  override axNode: SDK.AccessibilityModel.AccessibilityNode|null;
123
- private readonly noNodeInfo: Element;
124
- private readonly ignoredInfo: Element;
123
+ private readonly noNodeInfo: UI.Widget.Widget;
124
+ private readonly ignoredInfo: UI.Widget.Widget;
125
125
  private readonly treeOutline: UI.TreeOutline.TreeOutline;
126
126
  private readonly ignoredReasonsTree: UI.TreeOutline.TreeOutline;
127
127
  constructor() {
@@ -137,7 +137,7 @@ export class AXNodeSubPane extends AccessibilitySubPane {
137
137
  this.contentElement.classList.add('ax-subpane');
138
138
 
139
139
  this.noNodeInfo = this.createInfo(i18nString(UIStrings.noAccessibilityNode));
140
- this.ignoredInfo = this.createInfo(i18nString(UIStrings.accessibilityNodeNotExposed), 'ax-ignored-info hidden');
140
+ this.ignoredInfo = this.createInfo(i18nString(UIStrings.accessibilityNodeNotExposed), 'ax-ignored-info', 'hidden');
141
141
 
142
142
  this.treeOutline = this.createTreeOutline();
143
143
  this.ignoredReasonsTree = this.createTreeOutline();
@@ -160,21 +160,21 @@ export class AXNodeSubPane extends AccessibilitySubPane {
160
160
 
161
161
  if (!axNode) {
162
162
  treeOutline.element.classList.add('hidden');
163
- this.ignoredInfo.classList.add('hidden');
163
+ this.ignoredInfo.element.classList.add('hidden');
164
164
  ignoredReasons.element.classList.add('hidden');
165
165
 
166
- this.noNodeInfo.classList.remove('hidden');
166
+ this.noNodeInfo.element.classList.remove('hidden');
167
167
  this.element.classList.add('ax-ignored-node-pane');
168
168
 
169
169
  return;
170
170
  }
171
171
 
172
172
  if (axNode.ignored()) {
173
- this.noNodeInfo.classList.add('hidden');
173
+ this.noNodeInfo.element.classList.add('hidden');
174
174
  treeOutline.element.classList.add('hidden');
175
175
  this.element.classList.add('ax-ignored-node-pane');
176
176
 
177
- this.ignoredInfo.classList.remove('hidden');
177
+ this.ignoredInfo.element.classList.remove('hidden');
178
178
  ignoredReasons.element.classList.remove('hidden');
179
179
  function addIgnoredReason(property: Protocol.Accessibility.AXProperty): void {
180
180
  ignoredReasons.appendChild(
@@ -191,9 +191,9 @@ export class AXNodeSubPane extends AccessibilitySubPane {
191
191
  }
192
192
  this.element.classList.remove('ax-ignored-node-pane');
193
193
 
194
- this.ignoredInfo.classList.add('hidden');
194
+ this.ignoredInfo.element.classList.add('hidden');
195
195
  ignoredReasons.element.classList.add('hidden');
196
- this.noNodeInfo.classList.add('hidden');
196
+ this.noNodeInfo.element.classList.add('hidden');
197
197
 
198
198
  treeOutline.element.classList.remove('hidden');
199
199
 
@@ -33,10 +33,12 @@ export class AccessibilitySubPane extends UI.View.SimpleView {
33
33
  this.nodeInternal = node;
34
34
  }
35
35
 
36
- createInfo(textContent: string, className?: string): Element {
37
- const info = this.element.createChild('div', className || 'gray-info-message');
38
- info.classList.add('info-message-overflow');
39
- info.textContent = textContent;
36
+ createInfo(textContent: string, ...classNames: string[]): UI.Widget.Widget {
37
+ const info = new UI.EmptyWidget.EmptyWidget(textContent);
38
+ if (classNames.length === 0) {
39
+ classNames.push('gray-info-message');
40
+ }
41
+ info.element.classList.add(...classNames, 'info-message-overflow');
40
42
  return info;
41
43
  }
42
44