debug-server-next
Version:
Dev server for hippy-core.
538 lines (537 loc) • 20.6 kB
JavaScript
// 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.
import * as i18n from '../../../core/i18n/i18n.js';
import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
import * as LitHtml from '../../../ui/lit-html/lit-html.js';
import userAgentClientHintsFormStyles from './userAgentClientHintsForm.css.js';
import * as IconButton from '../../../ui/components/icon_button/icon_button.js';
import * as EmulationUtils from '../utils/utils.js';
const UIStrings = {
/**
* @description Title for user agent client hints form
*/
title: 'User agent client hints',
/**
* @description Heading for brands section.
* Brands here relate to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
*/
brands: 'Brands',
/**
* @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: 'Brand 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 brands version.
* Brands here relate to different browser brands/vendors like Google Chrome (v89), Microsoft Edge (v92) etc.
*/
version: 'Version',
/**
* @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 for delete icon for deleting browser brand in brands section.
* Brands here relate to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
*/
deleteTooltip: 'Delete',
/**
* @description Aria label for delete icon for deleting browser brand in brands section.
* Brands here relate to different browser brands/vendors like Google Chrome, Microsoft Edge etc.
* @example {index} PH1
*/
brandDeleteAriaLabel: 'Delete {PH1}',
/**
* @description Label for full browser version input field.
*/
fullBrowserVersion: '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',
/**
* @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',
/**
* @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',
/**
* @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 Aria label for information link in user agent client hints form.
*/
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. Click the button to learn more.',
};
const str_ = i18n.i18n.registerUIStrings('panels/emulation/components/UserAgentClientHintsForm.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class ClientHintsChangeEvent extends Event {
constructor() {
super('clienthintschange');
}
}
export class ClientHintsSubmitEvent extends Event {
detail;
constructor(value) {
super('clienthintssubmit');
this.detail = { value };
}
}
const DEFAULT_METADATA = {
brands: [
{
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 {
static litTagName = LitHtml.literal `devtools-user-agent-client-hints-form`;
shadow = this.attachShadow({ mode: 'open' });
isFormOpened = false;
isFormDisabled = false;
metaData = DEFAULT_METADATA;
showMobileCheckbox = false;
showSubmitButton = false;
connectedCallback() {
this.shadow.adoptedStyleSheets = [userAgentClientHintsFormStyles];
}
set value(data) {
const { metaData = DEFAULT_METADATA, showMobileCheckbox = false, showSubmitButton = false } = data;
this.metaData = {
...this.metaData,
...metaData,
};
this.showMobileCheckbox = showMobileCheckbox;
this.showSubmitButton = showSubmitButton;
this.render();
}
get value() {
return {
metaData: this.metaData,
};
}
set disabled(disableForm) {
this.isFormDisabled = disableForm;
this.isFormOpened = false;
this.render();
}
get disabled() {
return this.isFormDisabled;
}
handleTreeExpand = (event) => {
if (event.code === 'Space' || event.code === 'Enter') {
event.preventDefault();
this.handleTreeClick();
}
};
handleTreeClick = () => {
if (this.isFormDisabled) {
return;
}
this.isFormOpened = !this.isFormOpened;
this.render();
};
handleBrandInputChange = (value, index, brandInputType) => {
const updatedBrands = 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: updatedBrands,
};
this.dispatchEvent(new ClientHintsChangeEvent());
this.render();
};
handleBrandDelete = (index) => {
const { brands = [] } = this.metaData;
brands.splice(index, 1);
this.metaData = {
...this.metaData,
brands,
};
this.dispatchEvent(new ClientHintsChangeEvent());
this.render();
};
handleAddBrandClick = () => {
const { brands } = this.metaData;
this.metaData = {
...this.metaData,
brands: [
...(Array.isArray(brands) ? brands : []),
{
brand: '',
version: '',
},
],
};
this.dispatchEvent(new ClientHintsChangeEvent());
this.render();
const brandInputElements = this.shadowRoot?.querySelectorAll('.brand-name-input');
if (brandInputElements) {
const lastBrandInputElement = Array.from(brandInputElements).pop();
if (lastBrandInputElement) {
lastBrandInputElement.focus();
}
}
};
handleAddBrandKeyPress = (event) => {
if (event.code === 'Space' || event.code === 'Enter') {
event.preventDefault();
this.handleAddBrandClick();
}
};
handleInputChange = (stateKey, value) => {
if (stateKey in this.metaData) {
this.metaData = {
...this.metaData,
[stateKey]: value,
};
this.render();
}
this.dispatchEvent(new ClientHintsChangeEvent());
};
handleLinkPress = (event) => {
if (event.code === 'Space' || event.code === 'Enter') {
event.preventDefault();
event.target.click();
}
};
handleSubmit = (event) => {
event.preventDefault();
if (this.showSubmitButton) {
this.dispatchEvent(new ClientHintsSubmitEvent(this.metaData));
this.render();
}
};
renderInputWithLabel(label, placeholder, value, stateKey) {
const handleInputChange = (event) => {
const value = event.target.value;
this.handleInputChange(stateKey, value);
};
return LitHtml.html `
<label class="full-row label input-field-label-container">
${label}
<input
class="input-field"
type="text"
="${handleInputChange}"
.value="${value}"
placeholder="${placeholder}"
/>
</label>
`;
}
renderPlatformSection() {
const { platform, platformVersion } = this.metaData;
const handlePlatformNameChange = (event) => {
const value = event.target.value;
this.handleInputChange('platform', value);
};
const handlePlatformVersionChange = (event) => {
const value = event.target.value;
this.handleInputChange('platformVersion', value);
};
return LitHtml.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"
="${handlePlatformNameChange}"
.value="${platform}"
placeholder="${i18nString(UIStrings.platformPlaceholder)}"
aria-label="${i18nString(UIStrings.platformLabel)}"
/>
<input
class="input-field half-row"
type="text"
="${handlePlatformVersionChange}"
.value="${platformVersion}"
placeholder="${i18nString(UIStrings.platformVersion)}"
aria-label="${i18nString(UIStrings.platformVersion)}"
/>
</div>
`;
}
renderDeviceModelSection() {
const { model, mobile } = this.metaData;
const handleDeviceModelChange = (event) => {
const value = event.target.value;
this.handleInputChange('model', value);
};
const handleMobileChange = (event) => {
const value = event.target.checked;
this.handleInputChange('mobile', value);
};
const mobileCheckboxInput = this.showMobileCheckbox ? LitHtml.html `
<label class="mobile-checkbox-container">
<input type="checkbox" ="${handleMobileChange}" .checked="${mobile}" />
${i18nString(UIStrings.mobileCheckboxLabel)}
</label>
` :
LitHtml.html ``;
return LitHtml.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"
="${handleDeviceModelChange}"
.value="${model}"
placeholder="${i18nString(UIStrings.deviceModel)}"
/>
${mobileCheckboxInput}
</div>
`;
}
renderBrands() {
const { brands = [
{
brand: '',
version: '',
},
], } = this.metaData;
const brandElements = brands.map((brandRow, index) => {
const { brand, version } = brandRow;
const handleDeleteClick = () => {
this.handleBrandDelete(index);
};
const handleKeyPress = (event) => {
if (event.code === 'Space' || event.code === 'Enter') {
event.preventDefault();
handleDeleteClick();
}
};
const handleBrandBrowserChange = (event) => {
const value = event.target.value;
this.handleBrandInputChange(value, index, 'brandName');
};
const handleBrandVersionChange = (event) => {
const value = event.target.value;
this.handleBrandInputChange(value, index, 'brandVersion');
};
return LitHtml.html `
<div class="full-row brand-row" aria-label="${i18nString(UIStrings.brandProperties)}" role="group">
<input
class="input-field brand-name-input"
type="text"
="${handleBrandBrowserChange}"
.value="${brand}"
placeholder="${i18nString(UIStrings.brandName)}"
aria-label="${i18nString(UIStrings.brandNameAriaLabel, {
PH1: index + 1,
})}"
/>
<input
class="input-field"
type="text"
="${handleBrandVersionChange}"
.value="${version}"
placeholder="${i18nString(UIStrings.version)}"
aria-label="${i18nString(UIStrings.brandVersionAriaLabel, {
PH1: index + 1,
})}"
/>
<${IconButton.Icon.Icon.litTagName}
.data=${{ color: 'var(--client-hints-form-icon-color)', iconName: 'trash_bin_icon', width: '10px', height: '14px' }}
title="${i18nString(UIStrings.deleteTooltip)}"
class="delete-icon"
tabindex="0"
role="button"
="${handleDeleteClick}"
="${handleKeyPress}"
aria-label="${i18nString(UIStrings.brandDeleteAriaLabel, {
PH1: index + 1,
})}"
>
</${IconButton.Icon.Icon.litTagName}>
</div>
`;
});
return LitHtml.html `
<span class="full-row label">${i18nString(UIStrings.brands)}</span>
${brandElements}
<div
class="add-container full-row"
role="button"
tabindex="0"
="${this.handleAddBrandClick}"
="${this.handleAddBrandKeyPress}"
>
<${IconButton.Icon.Icon.litTagName}
aria-hidden="true"
.data=${{ color: 'var(--client-hints-form-icon-color)', iconName: 'add-icon', width: '10px' }}
>
</${IconButton.Icon.Icon.litTagName}>
${i18nString(UIStrings.addBrand)}
</div>
`;
}
render() {
const { fullVersion, architecture } = this.metaData;
const brandSection = this.renderBrands();
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();
const submitButton = this.showSubmitButton ? LitHtml.html `
<button
type="submit"
class="submit-button full-row"
>
${i18nString(UIStrings.update)}
</button>
` :
LitHtml.html ``;
const output = LitHtml.html `
<section class="root">
<div
class="tree-title"
role="button"
="${this.handleTreeClick}"
tabindex="0"
="${this.handleTreeExpand}"
aria-expanded="${this.isFormOpened}"
aria-controls="form-container"
="${this.isFormDisabled}"
aria-disabled="${this.isFormDisabled}"
>
<${IconButton.Icon.Icon.litTagName}
class="${this.isFormOpened ? '' : 'rotate-icon'}"
.data=${{ color: 'var(--client-hints-form-icon-color)', iconName: 'chromeSelect', width: '20px' }}
>
</${IconButton.Icon.Icon.litTagName}>
${i18nString(UIStrings.title)}
<x-link
tabindex="0"
href="https://web.dev/user-agent-client-hints/"
target="_blank"
class="info-link"
="${this.handleLinkPress}"
aria-label="${i18nString(UIStrings.userAgentClientHintsInfo)}"
>
<${IconButton.Icon.Icon.litTagName}
.data=${{ color: 'var(--client-hints-form-icon-color)', iconName: 'ic_info_black_18dp', width: '14px' }}
>
</${IconButton.Icon.Icon.litTagName}>
</x-link>
</div>
<form
id="form-container"
class="form-container ${this.isFormOpened ? '' : 'hide-container'}"
="${this.handleSubmit}"
>
${brandSection}
${fullBrowserInput}
${platformSection}
${architectureInput}
${deviceModelSection}
${submitButton}
</form>
</section>
`;
// clang-format off
LitHtml.render(output, this.shadow);
// clang-format on
}
validate = () => {
for (const [metaDataKey, metaDataValue] of Object.entries(this.metaData)) {
if (metaDataKey === 'brands') {
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 {
const metaDataError = EmulationUtils.UserAgentMetadata.validateAsStructuredHeadersString(metaDataValue, i18nString(UIStrings.notRepresentable));
if (!metaDataError.valid) {
return metaDataError;
}
}
}
return { valid: true };
};
}
ComponentHelpers.CustomElements.defineComponent('devtools-user-agent-client-hints-form', UserAgentClientHintsForm);