UNPKG

chrome-devtools-frontend

Version:
856 lines (813 loc) • 30.2 kB
// 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; } }