chrome-devtools-frontend 1.0.1026160 → 1.0.1027447
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.
- package/config/owner/LIGHTHOUSE_OWNERS +1 -1
- package/docs/triage_guidelines.md +1 -122
- package/front_end/core/host/UserMetrics.ts +2 -1
- package/front_end/core/i18n/locales/en-US.json +24 -0
- package/front_end/core/i18n/locales/en-XL.json +24 -0
- package/front_end/core/root/Runtime.ts +1 -0
- package/front_end/core/sdk/NetworkManager.ts +24 -3
- package/front_end/core/sdk/ResourceTreeModel.ts +1 -1
- package/front_end/core/sdk/SourceMap.ts +22 -6
- package/front_end/core/sdk/sdk-meta.ts +7 -0
- package/front_end/devtools_compatibility.js +1 -0
- package/front_end/entrypoints/main/MainImpl.ts +5 -0
- package/front_end/generated/ARIAProperties.js +723 -723
- package/front_end/generated/SupportedCSSProperties.js +2835 -2835
- package/front_end/generated/protocol.ts +4 -0
- package/front_end/legacy_test_runner/axe_core_test_runner/axe_core_test_runner.js +1 -1
- package/front_end/legacy_test_runner/sources_test_runner/DebuggerTestRunner.js +1 -1
- package/front_end/models/bindings/CompilerScriptMapping.ts +1 -1
- package/front_end/models/bindings/DebuggerLanguagePlugins.ts +38 -11
- package/front_end/models/bindings/DebuggerWorkspaceBinding.ts +7 -1
- package/front_end/models/bindings/IgnoreListManager.ts +35 -22
- package/front_end/models/issues_manager/descriptions/clientHintMetaTagAllowListInvalidOrigin.md +1 -1
- package/front_end/models/issues_manager/descriptions/clientHintMetaTagModifiedHTML.md +1 -1
- package/front_end/models/text_utils/TextRange.ts +8 -0
- package/front_end/models/timeline_model/TimelineModel.ts +18 -1
- package/front_end/panels/accessibility/ARIAAttributesView.ts +2 -0
- package/front_end/panels/console/consoleView.css +4 -0
- package/front_end/panels/elements/ElementsTreeOutline.ts +10 -16
- package/front_end/panels/elements/TopLayerContainer.ts +16 -22
- package/front_end/panels/elements/components/ElementsBreadcrumbs.ts +45 -50
- package/front_end/panels/lighthouse/LighthouseController.ts +3 -0
- package/front_end/panels/lighthouse/LighthousePanel.ts +2 -0
- package/front_end/panels/security/SecurityPanel.ts +52 -0
- package/front_end/panels/security/originView.css +1 -1
- package/front_end/panels/settings/FrameworkIgnoreListSettingsTab.ts +16 -0
- package/front_end/panels/sources/CallStackSidebarPane.ts +2 -3
- package/front_end/panels/sources/DebuggerPlugin.ts +8 -2
- package/front_end/panels/sources/navigatorTree.css +3 -3
- package/front_end/panels/timeline/TimelineFlameChartDataProvider.ts +1 -1
- package/front_end/ui/components/docs/building-ui-documentation/CreatingComponents.md +172 -1
- package/front_end/ui/components/panel_feedback/PreviewToggle.ts +15 -16
- package/front_end/ui/components/panel_feedback/previewToggle.css +13 -15
- package/front_end/ui/components/text_editor/TextEditor.ts +3 -0
- package/front_end/ui/legacy/ARIAUtils.ts +1 -75
- package/front_end/ui/legacy/ListControl.ts +4 -0
- package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +13 -3
- package/front_end/ui/legacy/components/utils/JSPresentationUtils.ts +5 -4
- package/front_end/ui/legacy/inspectorCommon.css +0 -9
- package/package.json +1 -1
@@ -329,6 +329,16 @@ const UIStrings = {
|
|
329
329
|
*/
|
330
330
|
cipher: 'Cipher',
|
331
331
|
/**
|
332
|
+
*@description Text in Security Panel that refers to the signature algorithm
|
333
|
+
*used by the server for authenticate in the TLS handshake.
|
334
|
+
*/
|
335
|
+
serverSignature: 'Server signature',
|
336
|
+
/**
|
337
|
+
*@description Text in Security Panel that refers to whether the ClientHello
|
338
|
+
*message in the TLS handshake was encrypted.
|
339
|
+
*/
|
340
|
+
encryptedClientHello: 'Encrypted ClientHello',
|
341
|
+
/**
|
332
342
|
*@description Sct div text content in Security Panel of the Security panel
|
333
343
|
*/
|
334
344
|
certificateTransparency: 'Certificate Transparency',
|
@@ -442,12 +452,42 @@ const UIStrings = {
|
|
442
452
|
*@example {2} PH1
|
443
453
|
*/
|
444
454
|
showMoreSTotal: 'Show more ({PH1} total)',
|
455
|
+
/**
|
456
|
+
*@description Shown when a field refers to an option that is unknown to the frontend.
|
457
|
+
*/
|
458
|
+
unknownField: 'unknown',
|
459
|
+
/**
|
460
|
+
*@description Shown when a field refers to a TLS feature which was enabled.
|
461
|
+
*/
|
462
|
+
enabled: 'enabled',
|
445
463
|
};
|
446
464
|
const str_ = i18n.i18n.registerUIStrings('panels/security/SecurityPanel.ts', UIStrings);
|
447
465
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
448
466
|
|
449
467
|
let securityPanelInstance: SecurityPanel;
|
450
468
|
|
469
|
+
// See https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-signaturescheme
|
470
|
+
// This contains signature schemes supported by Chrome.
|
471
|
+
const SignatureSchemeStrings = new Map([
|
472
|
+
// The full name for these schemes is RSASSA-PKCS1-v1_5, sometimes
|
473
|
+
// "PKCS#1 v1.5", but those are very long, so let "RSA" vs "RSA-PSS"
|
474
|
+
// disambiguate.
|
475
|
+
[0x0201, 'RSA with SHA-1'],
|
476
|
+
[0x0401, 'RSA with SHA-256'],
|
477
|
+
[0x0501, 'RSA with SHA-384'],
|
478
|
+
[0x0601, 'RSA with SHA-512'],
|
479
|
+
|
480
|
+
// We omit the curve from these names because in TLS 1.2 these code points
|
481
|
+
// were not specific to a curve. Saying "P-256" for a server that used a P-384
|
482
|
+
// key with SHA-256 in TLS 1.2 would be confusing.
|
483
|
+
[0x0403, 'ECDSA with SHA-256'],
|
484
|
+
[0x0503, 'ECDSA with SHA-384'],
|
485
|
+
|
486
|
+
[0x0804, 'RSA-PSS with SHA-256'],
|
487
|
+
[0x0805, 'RSA-PSS with SHA-384'],
|
488
|
+
[0x0806, 'RSA-PSS with SHA-512'],
|
489
|
+
]);
|
490
|
+
|
451
491
|
export class SecurityPanel extends UI.Panel.PanelWithSidebar implements
|
452
492
|
SDK.TargetManager.SDKModelObserver<SecurityModel> {
|
453
493
|
private readonly mainView: SecurityMainView;
|
@@ -1466,11 +1506,23 @@ export class SecurityOriginView extends UI.Widget.VBox {
|
|
1466
1506
|
table.addRow(i18nString(UIStrings.keyExchange), originState.securityDetails.keyExchangeGroup);
|
1467
1507
|
}
|
1468
1508
|
|
1509
|
+
if (originState.securityDetails.serverSignatureAlgorithm) {
|
1510
|
+
// See https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-signaturescheme
|
1511
|
+
let sigString = SignatureSchemeStrings.get(originState.securityDetails.serverSignatureAlgorithm);
|
1512
|
+
sigString ??=
|
1513
|
+
i18nString(UIStrings.unknownField) + ' (' + originState.securityDetails.serverSignatureAlgorithm + ')';
|
1514
|
+
table.addRow(i18nString(UIStrings.serverSignature), sigString);
|
1515
|
+
}
|
1516
|
+
|
1469
1517
|
table.addRow(
|
1470
1518
|
i18nString(UIStrings.cipher),
|
1471
1519
|
originState.securityDetails.cipher +
|
1472
1520
|
(originState.securityDetails.mac ? ' with ' + originState.securityDetails.mac : ''));
|
1473
1521
|
|
1522
|
+
if (originState.securityDetails.encryptedClientHello) {
|
1523
|
+
table.addRow(i18nString(UIStrings.encryptedClientHello), i18nString(UIStrings.enabled));
|
1524
|
+
}
|
1525
|
+
|
1474
1526
|
// Create the certificate section outside the callback, so that it appears in the right place.
|
1475
1527
|
const certificateSection = this.element.createChild('div', 'origin-view-section');
|
1476
1528
|
const certificateDiv = certificateSection.createChild('div', 'origin-view-section-title');
|
@@ -26,6 +26,15 @@ const UIStrings = {
|
|
26
26
|
*/
|
27
27
|
ignoreListContentScriptsExtension: 'Add content scripts to ignore list (extension scripts in the page)',
|
28
28
|
/**
|
29
|
+
*@description Text in Framework Ignore List Settings Tab of the Settings
|
30
|
+
*/
|
31
|
+
automaticallyIgnoreListKnownThirdPartyScripts: 'Automatically add known third-party scripts to ignore list',
|
32
|
+
/**
|
33
|
+
*@description Text in Framework Ignore List Settings Tab of the Settings
|
34
|
+
*/
|
35
|
+
automaticallyIgnoreListKnownThirdPartyScriptsTooltip:
|
36
|
+
'Add sources from the `x_google_ignoreList` field from source maps to the ignore list',
|
37
|
+
/**
|
29
38
|
*@description Ignore List label in Framework Ignore List Settings Tab of the Settings
|
30
39
|
*/
|
31
40
|
ignoreList: 'Ignore List',
|
@@ -98,6 +107,13 @@ export class FrameworkIgnoreListSettingsTab extends UI.Widget.VBox implements
|
|
98
107
|
Common.Settings.Settings.instance().moduleSetting('skipContentScripts'), true));
|
99
108
|
UI.Tooltip.Tooltip.install(ignoreListContentScripts, i18nString(UIStrings.ignoreListContentScriptsExtension));
|
100
109
|
|
110
|
+
const automaticallyIgnoreList = this.contentElement.createChild('div', 'automatically-ignore-list');
|
111
|
+
automaticallyIgnoreList.appendChild(UI.SettingsUI.createSettingCheckbox(
|
112
|
+
i18nString(UIStrings.automaticallyIgnoreListKnownThirdPartyScripts),
|
113
|
+
Common.Settings.Settings.instance().moduleSetting('automaticallyIgnoreListKnownThirdPartyScripts'), true));
|
114
|
+
UI.Tooltip.Tooltip.install(
|
115
|
+
automaticallyIgnoreList, i18nString(UIStrings.automaticallyIgnoreListKnownThirdPartyScriptsTooltip));
|
116
|
+
|
101
117
|
this.ignoreListLabel = i18nString(UIStrings.ignoreList);
|
102
118
|
this.disabledLabel = i18nString(UIStrings.disabled);
|
103
119
|
|
@@ -219,13 +219,13 @@ export class CallStackSidebarPane extends UI.View.SimpleView implements UI.Conte
|
|
219
219
|
this.callFrameWarningsElement.classList.add('hidden');
|
220
220
|
|
221
221
|
const details = UI.Context.Context.instance().flavor(SDK.DebuggerModel.DebuggerPausedDetails);
|
222
|
+
this.setSourceMapSubscription(details?.debuggerModel ?? null);
|
222
223
|
if (!details) {
|
223
224
|
this.notPausedMessageElement.classList.remove('hidden');
|
224
225
|
this.ignoreListMessageElement.classList.add('hidden');
|
225
226
|
this.showMoreMessageElement.classList.add('hidden');
|
226
227
|
this.items.replaceAll([]);
|
227
228
|
UI.Context.Context.instance().setFlavor(SDK.DebuggerModel.CallFrame, null);
|
228
|
-
this.setSourceMapSubscription(null);
|
229
229
|
return;
|
230
230
|
}
|
231
231
|
|
@@ -251,7 +251,6 @@ export class CallStackSidebarPane extends UI.View.SimpleView implements UI.Conte
|
|
251
251
|
}
|
252
252
|
|
253
253
|
let debuggerModel = details.debuggerModel;
|
254
|
-
this.setSourceMapSubscription(debuggerModel);
|
255
254
|
let asyncStackTraceId = details.asyncStackTraceId;
|
256
255
|
let asyncStackTrace: Protocol.Runtime.StackTrace|undefined|null = details.asyncStackTrace;
|
257
256
|
let previousStackTrace: Protocol.Runtime.CallFrame[]|SDK.DebuggerModel.CallFrame[] = details.callFrames;
|
@@ -494,7 +493,7 @@ export class CallStackSidebarPane extends UI.View.SimpleView implements UI.Conte
|
|
494
493
|
const canIgnoreList =
|
495
494
|
Bindings.IgnoreListManager.IgnoreListManager.instance().canIgnoreListUISourceCode(uiSourceCode);
|
496
495
|
const isIgnoreListed =
|
497
|
-
Bindings.IgnoreListManager.IgnoreListManager.instance().
|
496
|
+
Bindings.IgnoreListManager.IgnoreListManager.instance().isUserIgnoreListedURL(uiSourceCode.url());
|
498
497
|
const isContentScript = uiSourceCode.project().type() === Workspace.Workspace.projectTypes.ContentScripts;
|
499
498
|
|
500
499
|
const manager = Bindings.IgnoreListManager.IgnoreListManager.instance();
|
@@ -235,6 +235,9 @@ export class DebuggerPlugin extends Plugin {
|
|
235
235
|
Common.Settings.Settings.instance()
|
236
236
|
.moduleSetting('skipContentScripts')
|
237
237
|
.addChangeListener(this.showIgnoreListInfobarIfNeeded, this);
|
238
|
+
Common.Settings.Settings.instance()
|
239
|
+
.moduleSetting('automaticallyIgnoreListKnownThirdPartyScripts')
|
240
|
+
.addChangeListener(this.showIgnoreListInfobarIfNeeded, this);
|
238
241
|
|
239
242
|
UI.Context.Context.instance().addFlavorChangeListener(SDK.DebuggerModel.CallFrame, this.callFrameChanged, this);
|
240
243
|
this.liveLocationPool = new Bindings.LiveLocation.LiveLocationPool();
|
@@ -365,7 +368,7 @@ export class DebuggerPlugin extends Plugin {
|
|
365
368
|
return;
|
366
369
|
}
|
367
370
|
const projectType = uiSourceCode.project().type();
|
368
|
-
if (!Bindings.IgnoreListManager.IgnoreListManager.instance().
|
371
|
+
if (!Bindings.IgnoreListManager.IgnoreListManager.instance().isUserIgnoreListedURL(uiSourceCode.url())) {
|
369
372
|
this.hideIgnoreListInfobar();
|
370
373
|
return;
|
371
374
|
}
|
@@ -513,7 +516,7 @@ export class DebuggerPlugin extends Plugin {
|
|
513
516
|
|
514
517
|
if (this.uiSourceCode.project().type() === Workspace.Workspace.projectTypes.Network &&
|
515
518
|
Common.Settings.Settings.instance().moduleSetting('jsSourceMapsEnabled').get() &&
|
516
|
-
!Bindings.IgnoreListManager.IgnoreListManager.instance().
|
519
|
+
!Bindings.IgnoreListManager.IgnoreListManager.instance().isUserIgnoreListedURL(this.uiSourceCode.url())) {
|
517
520
|
if (this.scriptFileForDebuggerModel.size) {
|
518
521
|
const scriptFile: Bindings.ResourceScriptMapping.ResourceScriptFile =
|
519
522
|
this.scriptFileForDebuggerModel.values().next().value;
|
@@ -1625,6 +1628,9 @@ export class DebuggerPlugin extends Plugin {
|
|
1625
1628
|
Common.Settings.Settings.instance()
|
1626
1629
|
.moduleSetting('skipContentScripts')
|
1627
1630
|
.removeChangeListener(this.showIgnoreListInfobarIfNeeded, this);
|
1631
|
+
Common.Settings.Settings.instance()
|
1632
|
+
.moduleSetting('automaticallyIgnoreListKnownThirdPartyScripts')
|
1633
|
+
.removeChangeListener(this.showIgnoreListInfobarIfNeeded, this);
|
1628
1634
|
super.dispose();
|
1629
1635
|
|
1630
1636
|
UI.Context.Context.instance().removeFlavorChangeListener(SDK.DebuggerModel.CallFrame, this.callFrameChanged, this);
|
@@ -121,9 +121,9 @@
|
|
121
121
|
background: var(--override-image-font-tree-item-color);
|
122
122
|
}
|
123
123
|
|
124
|
-
.navigator-sm-folder-tree-item .tree-element-title,
|
125
|
-
.navigator-sm-script-tree-item .tree-element-title,
|
126
|
-
.navigator-sm-stylesheet-tree-item .tree-element-title {
|
124
|
+
.tree-outline:not(:has(.navigator-deployed-tree-item)) .navigator-sm-folder-tree-item .tree-element-title,
|
125
|
+
.tree-outline:not(:has(.navigator-deployed-tree-item)) .navigator-sm-script-tree-item .tree-element-title,
|
126
|
+
.tree-outline:not(:has(.navigator-deployed-tree-item)) .navigator-sm-stylesheet-tree-item .tree-element-title {
|
127
127
|
font-style: italic;
|
128
128
|
}
|
129
129
|
|
@@ -701,7 +701,7 @@ export class TimelineFlameChartDataProvider extends Common.ObjectWrapper.ObjectW
|
|
701
701
|
}
|
702
702
|
|
703
703
|
private isIgnoreListedURL(url: Platform.DevToolsPath.UrlString): boolean {
|
704
|
-
return Bindings.IgnoreListManager.IgnoreListManager.instance().
|
704
|
+
return Bindings.IgnoreListManager.IgnoreListManager.instance().isUserIgnoreListedURL(url);
|
705
705
|
}
|
706
706
|
|
707
707
|
private appendAsyncEventsGroup(
|
@@ -78,9 +78,11 @@ Each component defines a `#render` method which is responsible for invoking LitH
|
|
78
78
|
The `#render` method calls `LitHtml.render`, building up a template with `LitHtml.html`:
|
79
79
|
|
80
80
|
```ts
|
81
|
-
LitHtml.render(LitHtml.html
|
81
|
+
LitHtml.render(LitHtml.html`<p>hello world!</p>`, this.#shadow, {host: this});
|
82
82
|
```
|
83
83
|
|
84
|
+
The third argument (`{host: this}`) tells LitHtml to automatically bind event listeners to the component. This can save you some painful debugging where event listeners do not have the right `this` reference; so we enforce the use of `{host: this}` via a custom ESLint rule.
|
85
|
+
|
84
86
|
There is unfortunately a [clang-format bug](crbug.com/1079231) which makes its auto-formatting of LitHtml templates very unreadable, so we usually disable clang-format round the call:
|
85
87
|
|
86
88
|
```ts
|
@@ -110,3 +112,172 @@ void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
|
|
110
112
|
```
|
111
113
|
|
112
114
|
> `scheduleRender` returns a promise; we use the `void` keyword to instruct TypeScript that we are purposefully not using `await` to wait for the promise to resolve. When scheduling a render it's most common to "fire and forget".
|
115
|
+
|
116
|
+
To summarise, most components start off life looking like:
|
117
|
+
|
118
|
+
```ts
|
119
|
+
export class ElementsBreadcrumbs extends HTMLElement {
|
120
|
+
static readonly litTagName = LitHtml.literal`devtools-chrome-link`;
|
121
|
+
readonly #shadow = this.attachShadow({mode: 'open'});
|
122
|
+
readonly #boundRender = this.#render.bind(this);
|
123
|
+
|
124
|
+
#render(): void {
|
125
|
+
LitHtml.render(LitHtml.html``, this.#shadow, {host:this});
|
126
|
+
}
|
127
|
+
}
|
128
|
+
```
|
129
|
+
|
130
|
+
## Triggering a render
|
131
|
+
|
132
|
+
One of the most important aspects to understand about our component system is that **rendering does not happen automatically**.
|
133
|
+
|
134
|
+
To ensure we trigger a render once the component is added to the DOM, we can define [`connectedCallback`](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#:~:text=lifecycle.%20For%20example%2C-,connectedCallback,-is%20invoked%20each):
|
135
|
+
|
136
|
+
```ts
|
137
|
+
export class ElementsBreadcrumbs extends HTMLElement {
|
138
|
+
static readonly litTagName = LitHtml.literal`devtools-chrome-link`;
|
139
|
+
readonly #shadow = this.attachShadow({mode: 'open'});
|
140
|
+
readonly #boundRender = this.#render.bind(this);
|
141
|
+
|
142
|
+
connectedCallback(): void {
|
143
|
+
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
|
144
|
+
}
|
145
|
+
|
146
|
+
#render(): void {
|
147
|
+
LitHtml.render(LitHtml.html``, this.#shadow, {host:this});
|
148
|
+
}
|
149
|
+
}
|
150
|
+
```
|
151
|
+
|
152
|
+
## Passing data into a component
|
153
|
+
|
154
|
+
Most of our components will require data that is passed into them. For example, `ElementsBreadcrumbs` takes in an array of `DOMNode` objects.
|
155
|
+
|
156
|
+
To provide a component some data, we define a `data` setter. This setter takes an object with any data the component requires. This object should have a TypeScript interface defined for it:
|
157
|
+
|
158
|
+
```ts
|
159
|
+
export interface ElementsBreadcrumbsData {
|
160
|
+
selectedNode: DOMNode|null;
|
161
|
+
crumbs: DOMNode[];
|
162
|
+
}
|
163
|
+
|
164
|
+
export class ElementsBreadcrumbs extends HTMLElement {
|
165
|
+
// ...
|
166
|
+
#crumbs: DOMNode[] = [];
|
167
|
+
#selectedNode: DOMNode|null = null;
|
168
|
+
|
169
|
+
set data(data: ElementsBreadcrumbsData) {
|
170
|
+
this.#crumbs = data.crumbs;
|
171
|
+
this.#selectedNode = data.selectedNode;
|
172
|
+
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
|
173
|
+
}
|
174
|
+
}
|
175
|
+
```
|
176
|
+
|
177
|
+
## Rendering components
|
178
|
+
|
179
|
+
Now we know how to create components that can take data, let's render them!
|
180
|
+
|
181
|
+
### From a DevTools Widget
|
182
|
+
|
183
|
+
If you are rendering a component from the DevTools widget system, you should instantiate the component, pass any `data` to it, and then append it into the DOM:
|
184
|
+
|
185
|
+
```ts
|
186
|
+
// Within a Widget
|
187
|
+
const breadcrumbs = new ElementsBreadcrumbs();
|
188
|
+
breadcrumbs.data = {selectedNode: node, crumbs: [...]};
|
189
|
+
this.appendChild(breadcrumbs);
|
190
|
+
```
|
191
|
+
|
192
|
+
### From another component
|
193
|
+
|
194
|
+
If you are rendering a component from within another component, render it within the call to `LitHtml.html` and use the static `litTagName` property:
|
195
|
+
|
196
|
+
```ts
|
197
|
+
// Within some component
|
198
|
+
LitHtml.render(LitHtml.html`
|
199
|
+
<${ElementsBreadcrumbs.litTagName}></${ElementsBreadcrumbs>
|
200
|
+
`, this.#shadow, {host: this});
|
201
|
+
```
|
202
|
+
|
203
|
+
To pass data, we use [LitHtml's dot syntax](https://lit.dev/docs/templates/expressions/#property-expressions) to set the `data` property (and invoke our `set data` setter):
|
204
|
+
|
205
|
+
```ts
|
206
|
+
// Within some component
|
207
|
+
LitHtml.render(LitHtml.html`
|
208
|
+
<${ElementsBreadcrumbs.litTagName} .data=${{
|
209
|
+
selectedNode: node,
|
210
|
+
crumbs: [...],
|
211
|
+
}}>
|
212
|
+
</${ElementsBreadcrumbs>
|
213
|
+
`, this.#shadow, {host: this});
|
214
|
+
```
|
215
|
+
|
216
|
+
To enforce some type safety, we also use TypeScript's `as` keyword to force the compiler to type-check the `data` object against the interface:
|
217
|
+
|
218
|
+
```ts
|
219
|
+
// Within some component
|
220
|
+
LitHtml.render(LitHtml.html`
|
221
|
+
<${ElementsBreadcrumbs.litTagName} .data=${{
|
222
|
+
selectedNode: node,
|
223
|
+
crumbs: [...],
|
224
|
+
} as ElementsBreadcrumbsData}>
|
225
|
+
</${ElementsBreadcrumbs>
|
226
|
+
`, this.#shadow, {host: this});
|
227
|
+
```
|
228
|
+
|
229
|
+
This type-checking requirement is enforced by an ESLint rule.
|
230
|
+
|
231
|
+
## Performance concerns with data passing
|
232
|
+
|
233
|
+
The approach of `set data(data)` was chosen because:
|
234
|
+
|
235
|
+
1. It requires few lines of code.
|
236
|
+
2. It provides some form of type safety via the `as FooData` check.
|
237
|
+
3. At the time we didn't have a solution for scheduled and batched renders, and didn't want multiple setters to trigger multiple unnecessary renders.
|
238
|
+
|
239
|
+
However, using `set data(data)` does come with some negative performance costs:
|
240
|
+
|
241
|
+
1. LitHtml will always think the value has changed, because it's an object. If a component renders twice with `.data=${{name: 'Jack'}}`, Lit will think that the value has changed because it's a new object, even though we can see it's holding the same data.
|
242
|
+
2. This approach causes these objects to be created (and subsequently garbage collected) on/after each render.
|
243
|
+
|
244
|
+
For most components in DevTools, these trade-offs are acceptable, and we prefer the type-safety of `set data` and take the usually imperceivable performance hit. However, in rare circumstances, this performance hit is noticeable. A good example of this is in Performance Insights, where we have to constantly re-render components as the user scrolls through their performance timeline. We noticed that this caused a large amount of garbage collection from all the objects being created per-render and then immediately disposed.
|
245
|
+
|
246
|
+
For these situations, we can instead move to an approach where we set properties individually. We still define the interface as before, and then define an individual setter for each property:
|
247
|
+
|
248
|
+
```ts
|
249
|
+
interface ElementsBreadcrumbsData {
|
250
|
+
selectedNode: DOMNode|null;
|
251
|
+
crumbs: DOMNode[];
|
252
|
+
}
|
253
|
+
|
254
|
+
class ElementsBreadcrumbs extends HTMLElement {
|
255
|
+
#crumbs: DOMNode[] = [];
|
256
|
+
#selectedNode: DOMNode|null = null;
|
257
|
+
|
258
|
+
set crumbs(crumbs: ElementsBreadcrumbsData['crumbs']) {
|
259
|
+
this.#crumbs = crumbs;
|
260
|
+
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
|
261
|
+
}
|
262
|
+
|
263
|
+
set selectedNode(selectedNode: ElementsBreadcrumbsData['selectedNode']) {
|
264
|
+
this.#selectedNode = selectedNode;
|
265
|
+
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
|
266
|
+
}
|
267
|
+
}
|
268
|
+
```
|
269
|
+
|
270
|
+
Rendering this component within another Lit component would now be done like so:
|
271
|
+
|
272
|
+
```ts
|
273
|
+
// Within some component
|
274
|
+
LitHtml.render(LitHtml.html`
|
275
|
+
<${ElementsBreadcrumbs.litTagName} .crumbs=${[...]} .selectedNode=${node}>
|
276
|
+
</${ElementsBreadcrumbs>
|
277
|
+
`, this.#shadow, {host: this});
|
278
|
+
```
|
279
|
+
|
280
|
+
This solution is more performant, but less type-safe as TypeScript has no means of checking those values. This is something we may rectify using the `as` pattern, but for now it's preferred to use the `set data` method by default.
|
281
|
+
|
282
|
+
|
283
|
+
|
@@ -72,23 +72,22 @@ export class PreviewToggle extends HTMLElement {
|
|
72
72
|
render(
|
73
73
|
html`
|
74
74
|
<div class="container">
|
75
|
-
<
|
76
|
-
<
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
</div>
|
75
|
+
<label class="experiment-preview">
|
76
|
+
<input type="checkbox" ?checked=${checked} @change=${this.#checkboxChanged} aria-label=${this.#name}/>
|
77
|
+
<${IconButton.Icon.Icon.litTagName} .data=${{
|
78
|
+
iconName: 'ic_preview_feature',
|
79
|
+
width: '16px',
|
80
|
+
height: '16px',
|
81
|
+
color: 'var(--color-text-secondary)',
|
82
|
+
} as IconButton.Icon.IconData}>
|
83
|
+
</${IconButton.Icon.Icon.litTagName}>${this.#name}
|
84
|
+
</label>
|
85
|
+
<div class="spacer"></div>
|
86
|
+
${this.#feedbackURL && !this.#helperText
|
87
|
+
? html`<div class="feedback"><x-link class="x-link" href=${this.#feedbackURL}>${i18nString(UIStrings.shortFeedbackLink)}</x-link></div>`
|
88
|
+
: nothing}
|
90
89
|
${this.#learnMoreURL
|
91
|
-
? html`<x-link class="x-link" href=${this.#learnMoreURL}>${i18nString(UIStrings.learnMoreLink)}</x-link>`
|
90
|
+
? html`<div class="learn-more"><x-link class="x-link" href=${this.#learnMoreURL}>${i18nString(UIStrings.learnMoreLink)}</x-link></div>`
|
92
91
|
: nothing}
|
93
92
|
<div class="helper">
|
94
93
|
${this.#helperText && this.#feedbackURL
|
@@ -8,35 +8,33 @@
|
|
8
8
|
display: block;
|
9
9
|
}
|
10
10
|
|
11
|
-
.
|
11
|
+
.container {
|
12
|
+
display: flex;
|
13
|
+
flex-wrap: wrap;
|
14
|
+
padding: 4px;
|
15
|
+
}
|
16
|
+
|
17
|
+
.experiment-preview,
|
18
|
+
.feedback,
|
19
|
+
.learn-more {
|
12
20
|
display: flex;
|
13
21
|
align-items: center;
|
14
22
|
}
|
15
23
|
|
16
24
|
.helper {
|
25
|
+
flex-basis: 100%;
|
17
26
|
text-align: center;
|
18
27
|
font-style: italic;
|
19
28
|
}
|
20
29
|
|
21
|
-
.
|
22
|
-
|
23
|
-
}
|
24
|
-
|
25
|
-
.checkbox-line {
|
26
|
-
display: flex;
|
27
|
-
justify-content: space-between;
|
28
|
-
align-items: center;
|
29
|
-
flex-wrap: wrap;
|
30
|
-
}
|
31
|
-
|
32
|
-
.container {
|
33
|
-
padding: 4px;
|
30
|
+
.spacer {
|
31
|
+
flex: 1;
|
34
32
|
}
|
35
33
|
|
36
34
|
.x-link {
|
37
35
|
color: var(--color-primary);
|
38
36
|
text-decoration-line: underline;
|
39
|
-
margin: 4px;
|
37
|
+
margin: 0 4px;
|
40
38
|
}
|
41
39
|
|
42
40
|
.feedback .x-link {
|
@@ -105,6 +105,9 @@ export class TextEditor extends HTMLElement {
|
|
105
105
|
connectedCallback(): void {
|
106
106
|
if (!this.#activeEditor) {
|
107
107
|
this.#createEditor();
|
108
|
+
} else {
|
109
|
+
this.#activeEditor.scrollDOM.scrollTop = this.#lastScrollPos.top;
|
110
|
+
this.#activeEditor.scrollDOM.scrollLeft = this.#lastScrollPos.left;
|
108
111
|
}
|
109
112
|
}
|
110
113
|
|
@@ -344,10 +344,6 @@ export function setAccessibleName(element: Element, name: string): void {
|
|
344
344
|
element.setAttribute('aria-label', name);
|
345
345
|
}
|
346
346
|
|
347
|
-
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
|
348
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
349
|
-
const _descriptionMap = new WeakMap<Element, Element>();
|
350
|
-
|
351
347
|
export function setDescription(element: Element, description: string): void {
|
352
348
|
// Nodes in the accessibility tree are made up of a core
|
353
349
|
// triplet of "name", "value", "description"
|
@@ -360,77 +356,7 @@ export function setDescription(element: Element, description: string): void {
|
|
360
356
|
// to appear with the description when the element is hovered.
|
361
357
|
// This is usually fine, except that DevTools has its own styled
|
362
358
|
// tooltips which would interfere with the browser tooltips.
|
363
|
-
|
364
|
-
// In future, the aria-description attribute may be used once it
|
365
|
-
// is unflagged.
|
366
|
-
//
|
367
|
-
// aria-describedby requires that an extra element exist in DOM
|
368
|
-
// that this element can point to. Both elements also have to
|
369
|
-
// be in the same shadow root. This is not trivial to manage.
|
370
|
-
// The rest of DevTools shouldn't have to worry about this,
|
371
|
-
// so there is some unfortunate code below.
|
372
|
-
|
373
|
-
const oldDescription = _descriptionMap.get(element);
|
374
|
-
if (oldDescription) {
|
375
|
-
oldDescription.remove();
|
376
|
-
}
|
377
|
-
element.removeAttribute('data-aria-utils-animation-hack');
|
378
|
-
|
379
|
-
if (!description) {
|
380
|
-
_descriptionMap.delete(element);
|
381
|
-
element.removeAttribute('aria-describedby');
|
382
|
-
return;
|
383
|
-
}
|
384
|
-
|
385
|
-
// We make a hidden element that contains the decsription
|
386
|
-
// and will be pointed to by aria-describedby.
|
387
|
-
const descriptionElement = document.createElement('span');
|
388
|
-
descriptionElement.textContent = description;
|
389
|
-
descriptionElement.style.display = 'none';
|
390
|
-
ensureId(descriptionElement);
|
391
|
-
element.setAttribute('aria-describedby', descriptionElement.id);
|
392
|
-
_descriptionMap.set(element, descriptionElement);
|
393
|
-
|
394
|
-
// Now we have to actually put this description element
|
395
|
-
// somewhere in the DOM so that we can point to it.
|
396
|
-
// It would be nice to just put it in the body, but that
|
397
|
-
// wouldn't work if the main element is in a shadow root.
|
398
|
-
// So the cleanest approach is to add the description element
|
399
|
-
// as a child of the main element. But wait! Some HTML elements
|
400
|
-
// aren't supposed to have children. Blink won't search inside
|
401
|
-
// these elements, and won't find our description element.
|
402
|
-
const contentfulVoidTags = new Set<string>(['INPUT', 'IMG']);
|
403
|
-
if (!contentfulVoidTags.has(element.tagName)) {
|
404
|
-
element.appendChild(descriptionElement);
|
405
|
-
// If we made it here, someone setting .textContent
|
406
|
-
// or removeChildren on the element will blow away
|
407
|
-
// our description. At least we tried our best!
|
408
|
-
return;
|
409
|
-
}
|
410
|
-
|
411
|
-
// We have some special element, like an <input>, where putting the
|
412
|
-
// description element inside it doesn't work.
|
413
|
-
// Lets try the next best thing, and just put the description element
|
414
|
-
// next to it in the DOM.
|
415
|
-
const inserted = element.insertAdjacentElement('afterend', descriptionElement);
|
416
|
-
if (inserted) {
|
417
|
-
return;
|
418
|
-
}
|
419
|
-
|
420
|
-
// Uh oh, the insertion didn't work! That means we aren't currently in the DOM.
|
421
|
-
// How can we find out when the element enters the DOM?
|
422
|
-
// See inspectorCommon.css
|
423
|
-
element.setAttribute('data-aria-utils-animation-hack', 'sorry');
|
424
|
-
element.addEventListener('animationend', () => {
|
425
|
-
// Someone might have made a new description in the meantime.
|
426
|
-
if (_descriptionMap.get(element) !== descriptionElement) {
|
427
|
-
return;
|
428
|
-
}
|
429
|
-
element.removeAttribute('data-aria-utils-animation-hack');
|
430
|
-
|
431
|
-
// Try it again. This time we are in the DOM, so it *should* work.
|
432
|
-
element.insertAdjacentElement('afterend', descriptionElement);
|
433
|
-
}, {once: true});
|
359
|
+
element.setAttribute('aria-description', description);
|
434
360
|
}
|
435
361
|
|
436
362
|
export function setActiveDescendant(element: Element, activedescendant: Element|null): void {
|
@@ -428,6 +428,10 @@ export class ListControl<T> {
|
|
428
428
|
}
|
429
429
|
if (newElement) {
|
430
430
|
ARIAUtils.setSelected(newElement, true);
|
431
|
+
const text = newElement.textContent;
|
432
|
+
if (text) {
|
433
|
+
ARIAUtils.alert(text);
|
434
|
+
}
|
431
435
|
}
|
432
436
|
ARIAUtils.setActiveDescendant(this.element, newElement);
|
433
437
|
}
|
@@ -30,6 +30,7 @@
|
|
30
30
|
|
31
31
|
import * as Common from '../../../../core/common/common.js';
|
32
32
|
import type * as Components from '../utils/utils.js';
|
33
|
+
import * as Root from '../../../../core/root/root.js';
|
33
34
|
import * as Host from '../../../../core/host/host.js';
|
34
35
|
import * as i18n from '../../../../core/i18n/i18n.js';
|
35
36
|
import * as LinearMemoryInspector from '../../../components/linear_memory_inspector/linear_memory_inspector.js';
|
@@ -222,8 +223,12 @@ export class ObjectPropertiesSection extends UI.TreeOutline.TreeOutlineInShadow
|
|
222
223
|
return;
|
223
224
|
}
|
224
225
|
|
226
|
+
const includedWebIdlTypes = webIdlType.includes?.map(className => domPinnedProperties[className]) ?? [];
|
227
|
+
const includedWebIdlProps = includedWebIdlTypes.flatMap(webIdlType => Object.entries(webIdlType.props ?? {}));
|
228
|
+
const webIdlProps = {...webIdlType.props, ...Object.fromEntries(includedWebIdlProps)};
|
229
|
+
|
225
230
|
for (const property of properties) {
|
226
|
-
const webIdlProperty =
|
231
|
+
const webIdlProperty = webIdlProps[property.name];
|
227
232
|
if (webIdlProperty) {
|
228
233
|
property.webIdl = {info: webIdlProperty};
|
229
234
|
}
|
@@ -1118,10 +1123,12 @@ export class ObjectPropertyTreeElement extends UI.TreeOutline.TreeElement {
|
|
1118
1123
|
this.expandedValueElement = this.createExpandedValueElement(this.property.value);
|
1119
1124
|
}
|
1120
1125
|
|
1126
|
+
const experiment = Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.IMPORTANT_DOM_PROPERTIES);
|
1127
|
+
|
1121
1128
|
let adorner: Element|string = '';
|
1122
1129
|
let container: Element;
|
1123
1130
|
|
1124
|
-
if (this.property.webIdl?.applicable) {
|
1131
|
+
if (this.property.webIdl?.applicable && experiment) {
|
1125
1132
|
const icon = new IconButton.Icon.Icon();
|
1126
1133
|
icon.data = {
|
1127
1134
|
iconName: 'star_outline',
|
@@ -1148,7 +1155,10 @@ export class ObjectPropertyTreeElement extends UI.TreeOutline.TreeElement {
|
|
1148
1155
|
this.listItemElement.removeChildren();
|
1149
1156
|
this.rowContainer = (container as HTMLElement);
|
1150
1157
|
this.listItemElement.appendChild(this.rowContainer);
|
1151
|
-
|
1158
|
+
|
1159
|
+
if (experiment) {
|
1160
|
+
this.listItemElement.dataset.webidl = this.property.webIdl?.applicable ? 'true' : 'false';
|
1161
|
+
}
|
1152
1162
|
}
|
1153
1163
|
|
1154
1164
|
private updatePropertyPath(): void {
|