chrome-devtools-frontend
Version:
Chrome DevTools UI
856 lines (813 loc) • 30.2 kB
text/typescript
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no-lit-render-outside-of-view, rulesdir/inject-checkbox-styles */
import '../../../../ui/legacy/legacy.js';
import * as i18n from '../../../../core/i18n/i18n.js';
import * as Platform from '../../../../core/platform/platform.js';
import type * as Protocol from '../../../../generated/protocol.js';
import * as Buttons from '../../../../ui/components/buttons/buttons.js';
import * as Input from '../../../../ui/components/input/input.js';
import type * as UI from '../../../../ui/legacy/legacy.js';
import * as Lit from '../../../../ui/lit/lit.js';
import * as VisualLogging from '../../../../ui/visual_logging/visual_logging.js';
import * as EmulationUtils from '../utils/utils.js';
import userAgentClientHintsFormStyles from './userAgentClientHintsForm.css.js';
const {html} = Lit;
const UIStrings = {
/**
* @description Title for user agent client hints form
*/
title: 'User agent client hints',
/**
* @description Heading for user agent section.
* Brands here relate to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
*/
useragent: 'User agent (Sec-CH-UA)',
/**
* @description Heading for full-version-list section.
*/
fullVersionList: 'Full version list (Sec-CH-UA-Full-Version-List)',
/**
* @description ARIA label for a form with properties for a single brand in a brand list. The form includes a brand name input field, a version
* input field and a delete icon. Brand refer to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
*/
brandProperties: 'User agent properties',
/**
* @description Input field placeholder for brands browser name.
* Brands here relate to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
*/
brandName: 'Brand',
/**
* @description Aria label for brands browser name input field.
* Brands here relate to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
* @example {index} PH1
*/
brandNameAriaLabel: 'Brand {PH1}',
/**
* @description Input field placeholder for significant brand version.
* Brands here relate to different browser brands/vendors like Google Chrome (v89), Microsoft Edge (v92) etc.
*/
significantBrandVersionPlaceholder: 'Significant version (e.g. 87)',
/**
* @description Input field placeholder for brand version.
* Brands here relate to different browser brands/vendors like Google Chrome (v89), Microsoft Edge (v92) etc.
*/
brandVersionPlaceholder: 'Version (e.g. 87.0.4280.88)',
/**
* @description Aria label for brands browser version input field.
* Brands here relate to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
* @example {index} PH1
*/
brandVersionAriaLabel: 'Version {PH1}',
/**
* @description Button title for adding another brand in brands section to client hints.
* Brands here relate to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
*/
addBrand: 'Add Brand',
/**
* @description Tooltip and aria label for delete icon for deleting browser brand from brands user agent section.
* Brands here relate to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
*/
brandUserAgentDelete: 'Delete brand from user agent section',
/**
* @description Tooltip and aria label for delete icon for deleting user agent from brands full version list.
* Brands here relate to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
*/
brandFullVersionListDelete: 'Delete brand from full version list',
/**
* @description Label for full browser version input field.
*/
fullBrowserVersion: 'Full browser version (Sec-CH-UA-Full-Browser-Version)',
/**
* @description Placeholder for full browser version input field.
*/
fullBrowserVersionPlaceholder: 'Full browser version (e.g. 87.0.4280.88)',
/**
* @description Label for platform heading section, platform relates to OS like Android, Windows etc.
*/
platformLabel: 'Platform (Sec-CH-UA-Platform / Sec-CH-UA-Platform-Version)',
/**
* @description Platform row, including platform name and platform version input field.
*/
platformProperties: 'Platform properties',
/**
* @description Version for platform input field, platform relates to OS like Android, Windows etc.
*/
platformVersion: 'Platform version',
/**
* @description Placeholder for platform name input field, platform relates to OS like Android, Windows etc.
*/
platformPlaceholder: 'Platform (e.g. Android)',
/**
* @description Label for architecture (Eg: x86, x64, arm) input field.
*/
architecture: 'Architecture (Sec-CH-UA-Arch)',
/**
* @description Placeholder for architecture (Eg: x86, x64, arm) input field.
*/
architecturePlaceholder: 'Architecture (e.g. x86)',
/**
* @description Device model row, including device model input field and mobile checkbox
*/
deviceProperties: 'Device properties',
/**
* @description Label for Device Model input field.
*/
deviceModel: 'Device model (Sec-CH-UA-Model)',
/**
* @description Label for Mobile phone checkbox.
*/
mobileCheckboxLabel: 'Mobile',
/**
* @description Label for button to submit client hints form in DevTools.
*/
update: 'Update',
/**
*@description Field Error message in the Device settings pane that shows that the entered value has characters that can't be represented in the corresponding User Agent Client Hints
*/
notRepresentable: 'Not representable as structured headers string.',
/**
* @description Hover text for info icon which explains user agent client hints.
*/
userAgentClientHintsInfo:
'User agent client hints are an alternative to the user agent string that identify the browser and the device in a more structured way with better privacy accounting.',
/**
* @description Success message when brand row is successfully added in client hints form.
* Brands here relate to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
*/
addedBrand: 'Added brand row',
/**
* @description Success message when brand row is successfully deleted in client hints form.
* Brands here relate to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
*/
deletedBrand: 'Deleted brand row',
/**
*@description Text that is usually a hyperlink to more documentation
*/
learnMore: 'Learn more',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/settings/emulation/components/UserAgentClientHintsForm.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class ClientHintsChangeEvent extends Event {
static readonly eventName = 'clienthintschange';
constructor() {
super(ClientHintsChangeEvent.eventName);
}
}
export class ClientHintsSubmitEvent extends Event {
static readonly eventName = 'clienthintssubmit';
detail: {value: Protocol.Emulation.UserAgentMetadata};
constructor(value: Protocol.Emulation.UserAgentMetadata) {
super(ClientHintsSubmitEvent.eventName);
this.detail = {value};
}
}
export interface UserAgentClientHintsFormData {
metaData?: Protocol.Emulation.UserAgentMetadata;
showMobileCheckbox?: boolean;
showSubmitButton?: boolean;
}
const DEFAULT_METADATA = {
brands: [
{
brand: '',
version: '',
},
],
fullVersionList: [
{
brand: '',
version: '',
},
],
fullVersion: '',
platform: '',
platformVersion: '',
architecture: '',
model: '',
mobile: false,
};
/**
* Component for user agent client hints form, it is used in device settings panel
* and network conditions panel. It is customizable through showMobileCheckbox and showSubmitButton.
*/
export class UserAgentClientHintsForm extends HTMLElement {
readonly #shadow = this.attachShadow({mode: 'open'});
#isFormOpened = false;
#isFormDisabled = false;
#metaData: Protocol.Emulation.UserAgentMetadata = DEFAULT_METADATA;
#showMobileCheckbox = false;
#showSubmitButton = false;
#useragentModifiedAriaMessage = '';
set value(data: UserAgentClientHintsFormData) {
const {metaData = DEFAULT_METADATA, showMobileCheckbox = false, showSubmitButton = false} = data;
this.#metaData = {
...this.#metaData,
...metaData,
};
this.#showMobileCheckbox = showMobileCheckbox;
this.#showSubmitButton = showSubmitButton;
this.#render();
}
get value(): UserAgentClientHintsFormData {
return {
metaData: this.#metaData,
};
}
set disabled(disableForm: boolean) {
this.#isFormDisabled = disableForm;
this.#isFormOpened = false;
this.#render();
}
get disabled(): boolean {
return this.#isFormDisabled;
}
#handleTreeExpand = (event: KeyboardEvent): void => {
if (event.code === 'Space' || event.code === 'Enter' || event.code === 'ArrowLeft' || event.code === 'ArrowRight') {
event.consume(true);
this.#handleTreeClick(event.code);
}
};
#handleTreeClick = (key: string): void => {
if (this.#isFormDisabled) {
return;
}
if ((key === 'ArrowLeft' && !this.#isFormOpened) || (key === 'ArrowRight' && this.#isFormOpened)) {
return;
}
this.#isFormOpened = !this.#isFormOpened;
this.#render();
};
#handleUseragentInputChange = (value: string, index: number, brandInputType: 'brandName'|'brandVersion'): void => {
const updatedUseragent = this.#metaData.brands?.map((browserBrand, brandIndex) => {
if (brandIndex === index) {
const {brand, version} = browserBrand;
if (brandInputType === 'brandName') {
return {
brand: value,
version,
};
}
return {
brand,
version: value,
};
}
return browserBrand;
});
this.#metaData = {
...this.#metaData,
brands: updatedUseragent,
};
this.dispatchEvent(new ClientHintsChangeEvent());
this.#render();
};
#handleFullVersionListInputChange =
(value: string, index: number, brandInputType: 'brandName'|'brandVersion'): void => {
const fullVersionList = this.#metaData.fullVersionList?.map((browserBrand, brandIndex) => {
if (brandIndex === index) {
const {brand, version} = browserBrand;
if (brandInputType === 'brandName') {
return {
brand: value,
version,
};
}
return {
brand,
version: value,
};
}
return browserBrand;
});
this.#metaData = {
...this.#metaData,
fullVersionList,
};
this.dispatchEvent(new ClientHintsChangeEvent());
this.#render();
};
#handleUseragentDelete = (index: number): void => {
const {brands = []} = this.#metaData;
brands.splice(index, 1);
this.#metaData = {
...this.#metaData,
brands,
};
this.dispatchEvent(new ClientHintsChangeEvent());
this.#useragentModifiedAriaMessage = i18nString(UIStrings.deletedBrand);
this.#render();
// after deleting a brand row, focus on next Brand input if available,
// otherwise focus on the "Add Brand" button
let nextFocusElement = this.shadowRoot?.getElementById(`ua-brand-${index + 1}-input`);
if (!nextFocusElement) {
nextFocusElement = this.shadowRoot?.getElementById('add-brand-button');
}
(nextFocusElement as HTMLElement)?.focus();
};
#handleFullVersionListDelete = (index: number): void => {
const {fullVersionList = []} = this.#metaData;
fullVersionList.splice(index, 1);
this.#metaData = {
...this.#metaData,
fullVersionList,
};
this.dispatchEvent(new ClientHintsChangeEvent());
this.#useragentModifiedAriaMessage = i18nString(UIStrings.deletedBrand);
this.#render();
// after deleting a brand row, focus on next Brand input if available,
// otherwise focus on the "Add Brand" button
let nextFocusElement = this.shadowRoot?.getElementById(`fvl-brand-${index + 1}-input`);
if (!nextFocusElement) {
nextFocusElement = this.shadowRoot?.getElementById('add-fvl-brand-button');
}
(nextFocusElement as HTMLElement)?.focus();
};
#handleAddUseragentBrandClick = (): void => {
const {brands} = this.#metaData;
this.#metaData = {
...this.#metaData,
brands: [
...(Array.isArray(brands) ? brands : []),
{
brand: '',
version: '',
},
],
};
this.dispatchEvent(new ClientHintsChangeEvent());
this.#useragentModifiedAriaMessage = i18nString(UIStrings.addedBrand);
this.#render();
const brandInputElements = this.shadowRoot?.querySelectorAll('.ua-brand-name-input');
if (brandInputElements) {
const lastBrandInputElement = Array.from(brandInputElements).pop();
if (lastBrandInputElement) {
(lastBrandInputElement as HTMLInputElement).focus();
}
}
};
#handleAddUseragentBrandKeyPress = (event: KeyboardEvent): void => {
if (event.code === 'Space' || event.code === 'Enter') {
event.preventDefault();
this.#handleAddUseragentBrandClick();
}
};
#handleAddFullVersionListBrandClick = (): void => {
const {fullVersionList} = this.#metaData;
this.#metaData = {
...this.#metaData,
fullVersionList: [
...(Array.isArray(fullVersionList) ? fullVersionList : []),
{
brand: '',
version: '',
},
],
};
this.dispatchEvent(new ClientHintsChangeEvent());
this.#useragentModifiedAriaMessage = i18nString(UIStrings.addedBrand);
this.#render();
const brandInputElements = this.shadowRoot?.querySelectorAll('.fvl-brand-name-input');
if (brandInputElements) {
const lastBrandInputElement = Array.from(brandInputElements).pop();
if (lastBrandInputElement) {
(lastBrandInputElement as HTMLInputElement).focus();
}
}
};
#handleAddFullVersionListBrandKeyPress = (event: KeyboardEvent): void => {
if (event.code === 'Space' || event.code === 'Enter') {
event.preventDefault();
this.#handleAddFullVersionListBrandClick();
}
};
#handleInputChange = (stateKey: keyof Protocol.Emulation.UserAgentMetadata, value: string|boolean): void => {
if (stateKey in this.#metaData) {
this.#metaData = {
...this.#metaData,
[stateKey]: value,
};
this.#render();
}
this.dispatchEvent(new ClientHintsChangeEvent());
};
#handleLinkPress = (event: KeyboardEvent): void => {
if (event.code === 'Space' || event.code === 'Enter') {
event.preventDefault();
(event.target as HTMLAnchorElement).click();
}
};
#handleSubmit = (event: Event): void => {
event.preventDefault();
if (this.#showSubmitButton) {
this.dispatchEvent(new ClientHintsSubmitEvent(this.#metaData));
this.#render();
}
};
#renderInputWithLabel(
label: string, placeholder: string, value: string,
stateKey: keyof Protocol.Emulation.UserAgentMetadata): Lit.TemplateResult {
const handleInputChange = (event: KeyboardEvent): void => {
const value = (event.target as HTMLInputElement).value;
this.#handleInputChange(stateKey, value);
};
return html`
<label class="full-row label input-field-label-container">
${label}
<input
class="input-field"
type="text"
@input=${handleInputChange}
.value=${value}
placeholder=${placeholder}
jslog=${
VisualLogging.textField().track({change: true}).context(Platform.StringUtilities.toKebabCase(stateKey))}
/>
</label>
`;
}
#renderPlatformSection(): Lit.TemplateResult {
const {platform, platformVersion} = this.#metaData;
const handlePlatformNameChange = (event: KeyboardEvent): void => {
const value = (event.target as HTMLInputElement).value;
this.#handleInputChange('platform', value);
};
const handlePlatformVersionChange = (event: KeyboardEvent): void => {
const value = (event.target as HTMLInputElement).value;
this.#handleInputChange('platformVersion', value);
};
return html`
<span class="full-row label">${i18nString(UIStrings.platformLabel)}</span>
<div class="full-row brand-row" aria-label=${i18nString(UIStrings.platformProperties)} role="group">
<input
class="input-field half-row"
type="text"
@input=${handlePlatformNameChange}
.value=${platform}
placeholder=${i18nString(UIStrings.platformPlaceholder)}
aria-label=${i18nString(UIStrings.platformLabel)}
jslog=${VisualLogging.textField('platform').track({
change: true,
})}
/>
<input
class="input-field half-row"
type="text"
@input=${handlePlatformVersionChange}
.value=${platformVersion}
placeholder=${i18nString(UIStrings.platformVersion)}
aria-label=${i18nString(UIStrings.platformVersion)}
jslog=${VisualLogging.textField('platform-version').track({
change: true,
})}
/>
</div>
`;
}
#renderDeviceModelSection(): Lit.TemplateResult {
const {model, mobile} = this.#metaData;
const handleDeviceModelChange = (event: KeyboardEvent): void => {
const value = (event.target as HTMLInputElement).value;
this.#handleInputChange('model', value);
};
const handleMobileChange = (event: KeyboardEvent): void => {
const value = (event.target as HTMLInputElement).checked;
this.#handleInputChange('mobile', value);
};
const mobileCheckboxInput = this.#showMobileCheckbox ? html`
<label class="mobile-checkbox-container">
<input type="checkbox" @input=${handleMobileChange} .checked=${mobile}
jslog=${VisualLogging.toggle('mobile').track({
click: true,
})}
/>
${i18nString(UIStrings.mobileCheckboxLabel)}
</label>
` :
html``;
return html`
<span class="full-row label">${i18nString(UIStrings.deviceModel)}</span>
<div class="full-row brand-row" aria-label=${i18nString(UIStrings.deviceProperties)} role="group">
<input
class="input-field ${this.#showMobileCheckbox ? 'device-model-input' : 'full-row'}"
type="text"
@input=${handleDeviceModelChange}
.value=${model}
placeholder=${i18nString(UIStrings.deviceModel)}
jslog=${VisualLogging.textField('model').track({
change: true,
})}
/>
${mobileCheckboxInput}
</div>
`;
}
#renderUseragent(): Lit.TemplateResult {
const {
brands =
[
{
brand: '',
version: '',
},
],
} = this.#metaData;
const brandElements = brands.map((brandRow, index) => {
const {brand, version} = brandRow;
const handleDeleteClick = (): void => {
this.#handleUseragentDelete(index);
};
const handleKeyPress = (event: KeyboardEvent): void => {
if (event.code === 'Space' || event.code === 'Enter') {
event.preventDefault();
handleDeleteClick();
}
};
const handleBrandChange = (event: KeyboardEvent): void => {
const value = (event.target as HTMLInputElement).value;
this.#handleUseragentInputChange(value, index, 'brandName');
};
const handleVersionChange = (event: KeyboardEvent): void => {
const value = (event.target as HTMLInputElement).value;
this.#handleUseragentInputChange(value, index, 'brandVersion');
};
return html`
<div class="full-row brand-row" aria-label=${i18nString(UIStrings.brandProperties)} role="group">
<input
class="input-field ua-brand-name-input"
type="text"
@input=${handleBrandChange}
.value=${brand}
id="ua-brand-${index + 1}-input"
placeholder=${i18nString(UIStrings.brandName)}
aria-label=${i18nString(UIStrings.brandNameAriaLabel, {
PH1: index + 1,
})}
jslog=${VisualLogging.textField('brand-name').track({
change: true,
})}
/>
<input
class="input-field"
type="text"
@input=${handleVersionChange}
.value=${version}
placeholder=${i18nString(UIStrings.significantBrandVersionPlaceholder)}
aria-label=${i18nString(UIStrings.brandVersionAriaLabel, {
PH1: index + 1,
})}
jslog=${VisualLogging.textField('brand-version').track({
change: true,
})}
/>
<devtools-icon
.data=${{
color: 'var(--icon-default)', iconName: 'bin', width: '16px', height: '16px',
}
}
title=${i18nString(UIStrings.brandUserAgentDelete)}
class="delete-icon"
tabindex="0"
role="button"
@click=${handleDeleteClick}
@keypress=${handleKeyPress}
aria-label=${i18nString(UIStrings.brandUserAgentDelete)}
>
</devtools-icon>
</div>
`;
});
return html`
<span class="full-row label">${i18nString(UIStrings.useragent)}</span>
${brandElements}
<div
class="add-container full-row"
role="button"
tabindex="0"
id="add-brand-button"
aria-label=${i18nString(UIStrings.addBrand)}
@click=${this.#handleAddUseragentBrandClick}
@keypress=${this.#handleAddUseragentBrandKeyPress}
>
<devtools-icon
aria-hidden="true"
.data=${{
color: 'var(--icon-default)', iconName: 'plus', width: '16px',
}
}
>
</devtools-icon>
${i18nString(UIStrings.addBrand)}
</div>
`;
}
#renderFullVersionList(): Lit.TemplateResult {
const {
fullVersionList =
[
{
brand: '',
version: '',
},
],
} = this.#metaData;
const elements = fullVersionList.map((brandRow, index) => {
const {brand, version} = brandRow;
const handleDeleteClick = (): void => {
this.#handleFullVersionListDelete(index);
};
const handleKeyPress = (event: KeyboardEvent): void => {
if (event.code === 'Space' || event.code === 'Enter') {
event.preventDefault();
handleDeleteClick();
}
};
const handleBrandChange = (event: KeyboardEvent): void => {
const value = (event.target as HTMLInputElement).value;
this.#handleFullVersionListInputChange(value, index, 'brandName');
};
const handleVersionChange = (event: KeyboardEvent): void => {
const value = (event.target as HTMLInputElement).value;
this.#handleFullVersionListInputChange(value, index, 'brandVersion');
};
return html`
<div
class="full-row brand-row"
aria-label=${i18nString(UIStrings.brandProperties)}
jslog=${VisualLogging.section('full-version')}
role="group">
<input
class="input-field fvl-brand-name-input"
type="text"
@input=${handleBrandChange}
.value=${brand}
id="fvl-brand-${index + 1}-input"
placeholder=${i18nString(UIStrings.brandName)}
aria-label=${i18nString(UIStrings.brandNameAriaLabel, {
PH1: index + 1,
})}
jslog=${VisualLogging.textField('brand-name').track({
change: true,
})}
/>
<input
class="input-field"
type="text"
@input=${handleVersionChange}
.value=${version}
placeholder=${i18nString(UIStrings.brandVersionPlaceholder)}
aria-label=${i18nString(UIStrings.brandVersionAriaLabel, {
PH1: index + 1,
})}
jslog=${VisualLogging.textField('brand-version').track({
change: true,
})}
/>
<devtools-icon
.data=${{
color: 'var(--icon-default)', iconName: 'bin', width: '16px', height: '16px',
}
}
title=${i18nString(UIStrings.brandFullVersionListDelete)}
class="delete-icon"
tabindex="0"
role="button"
@click=${handleDeleteClick}
@keypress=${handleKeyPress}
aria-label=${i18nString(UIStrings.brandFullVersionListDelete)}
>
</devtools-icon>
</div>
`;
});
return html`
<span class="full-row label">${i18nString(UIStrings.fullVersionList)}</span>
${elements}
<div
class="add-container full-row"
role="button"
tabindex="0"
id="add-fvl-brand-button"
aria-label=${i18nString(UIStrings.addBrand)}
@click=${this.#handleAddFullVersionListBrandClick}
@keypress=${this.#handleAddFullVersionListBrandKeyPress}
>
<devtools-icon
aria-hidden="true"
.data=${{
color: 'var(--icon-default)', iconName: 'plus', width: '16px',
}
}
>
</devtools-icon>
${i18nString(UIStrings.addBrand)}
</div>
`;
}
#render(): void {
const {fullVersion, architecture} = this.#metaData;
const useragentSection = this.#renderUseragent();
const fullVersionListSection = this.#renderFullVersionList();
const fullBrowserInput = this.#renderInputWithLabel(
i18nString(UIStrings.fullBrowserVersion), i18nString(UIStrings.fullBrowserVersionPlaceholder),
fullVersion || '', 'fullVersion');
const platformSection = this.#renderPlatformSection();
const architectureInput = this.#renderInputWithLabel(
i18nString(UIStrings.architecture), i18nString(UIStrings.architecturePlaceholder), architecture,
'architecture');
const deviceModelSection = this.#renderDeviceModelSection();
// clang-format off
const submitButton = this.#showSubmitButton ? html`
<devtools-button
.variant=${Buttons.Button.Variant.OUTLINED}
.type=${'submit'}
>
${i18nString(UIStrings.update)}
</devtools-button>
` : Lit.nothing;
// clang-format on
// clang-format off
const output = html`
<style>${Input.checkboxStyles}</style>
<style>${userAgentClientHintsFormStyles}</style>
<section class="root">
<div class="tree-title">
<div
role=button
@click=${this.#handleTreeClick}
tabindex=${this.#isFormDisabled ? '-1' : '0'}
@keydown=${this.#handleTreeExpand}
aria-expanded=${this.#isFormOpened}
aria-controls=form-container
aria-disabled=${this.#isFormDisabled}
aria-label=${i18nString(UIStrings.title)}
jslog=${VisualLogging.toggleSubpane().track({click: true})}>
<devtools-icon name=triangle-right></devtools-icon>
<devtools-icon name=triangle-down></devtools-icon>
${i18nString(UIStrings.title)}
</div>
<devtools-icon class=info-icon name=info title=${i18nString(UIStrings.userAgentClientHintsInfo)}></devtools-icon>
<x-link
tabindex=${this.#isFormDisabled ? '-1' : '0'}
href="https://web.dev/user-agent-client-hints/"
target="_blank"
class="link"
@keypress=${this.#handleLinkPress}
aria-label=${i18nString(UIStrings.userAgentClientHintsInfo)}
jslog=${VisualLogging.link('learn-more').track({click: true})}
>
${i18nString(UIStrings.learnMore)}
</x-link>
</div>
<form
id="form-container"
class="form-container ${this.#isFormOpened ? '' : 'hide-container'}"
@submit=${this.#handleSubmit}
>
${useragentSection}
${fullVersionListSection}
${fullBrowserInput}
${platformSection}
${architectureInput}
${deviceModelSection}
${submitButton}
</form>
<div aria-live="polite" aria-label=${this.#useragentModifiedAriaMessage}></div>
</section>
`;
// clang-format on
Lit.render(output, this.#shadow, {host: this});
}
validate = (): UI.ListWidget.ValidatorResult => {
for (const [metaDataKey, metaDataValue] of Object.entries(this.#metaData)) {
if (metaDataKey === 'brands' || metaDataKey === 'fullVersionList') {
// for sturctured fields, check each individual brand/version
const isBrandValid = this.#metaData.brands?.every(({brand, version}) => {
const brandNameResult = EmulationUtils.UserAgentMetadata.validateAsStructuredHeadersString(
brand, i18nString(UIStrings.notRepresentable));
const brandVersionResult = EmulationUtils.UserAgentMetadata.validateAsStructuredHeadersString(
version, i18nString(UIStrings.notRepresentable));
return brandNameResult.valid && brandVersionResult.valid;
});
if (!isBrandValid) {
return {valid: false, errorMessage: i18nString(UIStrings.notRepresentable)};
}
} else {
// otherwise, validate the value as a string
const metaDataError = EmulationUtils.UserAgentMetadata.validateAsStructuredHeadersString(
metaDataValue, i18nString(UIStrings.notRepresentable));
if (!metaDataError.valid) {
return metaDataError;
}
}
}
return {valid: true};
};
}
customElements.define('devtools-user-agent-client-hints-form', UserAgentClientHintsForm);
declare global {
interface HTMLElementTagNameMap {
'devtools-user-agent-client-hints-form': UserAgentClientHintsForm;
}
}