UNPKG

chrome-devtools-frontend

Version:
373 lines (344 loc) • 13.3 kB
// Copyright 2023 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 '../../../ui/components/icon_button/icon_button.js'; import './ControlButton.js'; import * as i18n from '../../../core/i18n/i18n.js'; import * as Buttons from '../../../ui/components/buttons/buttons.js'; import * as Input from '../../../ui/components/input/input.js'; import * as Lit from '../../../ui/lit/lit.js'; import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js'; import * as Models from '../models/models.js'; import * as Actions from '../recorder-actions/recorder-actions.js'; import createRecordingViewStyles from './createRecordingView.css.js'; const {html, Directives: {ifDefined}} = Lit; const UIStrings = { /** * @description The label for the input where the user enters a name for the new recording. */ recordingName: 'Recording name', /** * @description The button that start the recording with selected options. */ startRecording: 'Start recording', /** * @description The title of the page that contains the form for creating a new recording. */ createRecording: 'Create a new recording', /** * @description The error message that is shown if the user tries to create a recording without a name. */ recordingNameIsRequired: 'Recording name is required', /** * @description The label for the input where the user enters an attribute to be used for selector generation. */ selectorAttribute: 'Selector attribute', /** * @description The title for the close button where the user cancels a recording and returns back to previous view. */ cancelRecording: 'Cancel recording', /** * @description Label indicating a CSS (Cascading Style Sheets) selector type * (https://developer.mozilla.org/en-US/docs/Web/CSS). The label is used on a * checkbox which users can tick if they are interesting in recording CSS * selectors. */ selectorTypeCSS: 'CSS', /** * @description Label indicating a piercing CSS (Cascading Style Sheets) * selector type * (https://pptr.dev/guides/query-selectors#pierce-selectors-pierce). These * type of selectors behave like CSS selectors, but can pierce through * ShadowDOM. The label is used on a checkbox which users can tick if they are * interesting in recording CSS selectors. */ selectorTypePierce: 'Pierce', /** * @description Label indicating a ARIA (Accessible Rich Internet * Applications) selector type * (https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA). The * label is used on a checkbox which users can tick if they are interesting in * recording ARIA selectors. */ selectorTypeARIA: 'ARIA', /** * @description Label indicating a text selector type. The label is used on a * checkbox which users can tick if they are interesting in recording text * selectors. */ selectorTypeText: 'Text', /** * @description Label indicating a XPath (XML Path Language) selector type * (https://en.wikipedia.org/wiki/XPath). The label is used on a checkbox * which users can tick if they are interesting in recording text selectors. */ selectorTypeXPath: 'XPath', /** * @description The label for the input that allows specifying selector types * that should be used during the recordering. */ selectorTypes: 'Selector types to record', /** * @description The error message that shows up if the user turns off * necessary selectors. */ includeNecessarySelectors: 'You must choose CSS, Pierce, or XPath as one of your options. Only these selectors are guaranteed to be recorded since ARIA and text selectors may not be unique.', /** * @description Title of a link to the developer documentation. */ learnMore: 'Learn more', } as const; const str_ = i18n.i18n.registerUIStrings( 'panels/recorder/components/CreateRecordingView.ts', UIStrings, ); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); declare global { interface HTMLElementTagNameMap { 'devtools-create-recording-view': CreateRecordingView; } interface HTMLElementEventMap { recordingstarted: RecordingStartedEvent; recordingcancelled: RecordingCancelledEvent; } } export class RecordingStartedEvent extends Event { static readonly eventName = 'recordingstarted'; name: string; selectorAttribute?: string; selectorTypesToRecord: Models.Schema.SelectorType[]; constructor( name: string, selectorTypesToRecord: Models.Schema.SelectorType[], selectorAttribute?: string, ) { super(RecordingStartedEvent.eventName, {}); this.name = name; this.selectorAttribute = selectorAttribute || undefined; this.selectorTypesToRecord = selectorTypesToRecord; } } export class RecordingCancelledEvent extends Event { static readonly eventName = 'recordingcancelled'; constructor() { super(RecordingCancelledEvent.eventName); } } export interface CreateRecordingViewData { recorderSettings: Models.RecorderSettings.RecorderSettings; } export class CreateRecordingView extends HTMLElement { readonly #shadow = this.attachShadow({mode: 'open'}); #defaultRecordingName = ''; #error?: Error; #recorderSettings?: Models.RecorderSettings.RecorderSettings; constructor() { super(); this.setAttribute('jslog', `${VisualLogging.section('create-recording-view')}`); } connectedCallback(): void { this.#render(); this.#shadow.querySelector('input')?.focus(); } set data(data: CreateRecordingViewData) { this.#recorderSettings = data.recorderSettings; this.#defaultRecordingName = this.#recorderSettings.defaultTitle; } #onKeyDown(event: Event): void { if (this.#error) { this.#error = undefined; this.#render(); } const keyboardEvent = event as KeyboardEvent; if (keyboardEvent.key === 'Enter') { this.startRecording(); event.stopPropagation(); event.preventDefault(); } } startRecording(): void { const nameInput = this.#shadow.querySelector<HTMLInputElement>('#user-flow-name'); if (!nameInput) { throw new Error('input#user-flow-name not found'); } if (!this.#recorderSettings) { throw new Error('settings not set'); } if (!nameInput.value.trim()) { this.#error = new Error(i18nString(UIStrings.recordingNameIsRequired)); this.#render(); return; } const selectorTypeElements = this.#shadow.querySelectorAll( '.selector-type input[type=checkbox]', ); const selectorTypesToRecord: Models.Schema.SelectorType[] = []; for (const selectorType of selectorTypeElements) { const checkbox = selectorType as HTMLInputElement; const checkboxValue = checkbox.value as Models.Schema.SelectorType; if (checkbox.checked) { selectorTypesToRecord.push(checkboxValue); } } if (!selectorTypesToRecord.includes(Models.Schema.SelectorType.CSS) && !selectorTypesToRecord.includes(Models.Schema.SelectorType.XPath) && !selectorTypesToRecord.includes(Models.Schema.SelectorType.Pierce)) { this.#error = new Error(i18nString(UIStrings.includeNecessarySelectors)); this.#render(); return; } for (const selectorType of Object.values(Models.Schema.SelectorType)) { this.#recorderSettings.setSelectorByType( selectorType, selectorTypesToRecord.includes(selectorType), ); } const selectorAttributeEl = this.#shadow.querySelector( '#selector-attribute', ) as HTMLInputElement; const selectorAttribute = selectorAttributeEl.value.trim(); this.#recorderSettings.selectorAttribute = selectorAttribute; this.dispatchEvent( new RecordingStartedEvent( nameInput.value.trim(), selectorTypesToRecord, selectorAttribute, ), ); } #dispatchRecordingCancelled(): void { this.dispatchEvent(new RecordingCancelledEvent()); } #onInputFocus = (): void => { (this.#shadow.querySelector('#user-flow-name') as HTMLInputElement)?.select(); }; #render(): void { const selectorTypeToLabel = new Map([ [Models.Schema.SelectorType.ARIA, i18nString(UIStrings.selectorTypeARIA)], [Models.Schema.SelectorType.CSS, i18nString(UIStrings.selectorTypeCSS)], [Models.Schema.SelectorType.Text, i18nString(UIStrings.selectorTypeText)], [ Models.Schema.SelectorType.XPath, i18nString(UIStrings.selectorTypeXPath), ], [ Models.Schema.SelectorType.Pierce, i18nString(UIStrings.selectorTypePierce), ], ]); // clang-format off Lit.render( html` <style>${createRecordingViewStyles}</style> <style>${Input.textInputStyles}</style> <style>${Input.checkboxStyles}</style> <div class="wrapper"> <div class="header-wrapper"> <h1>${i18nString(UIStrings.createRecording)}</h1> <devtools-button title=${i18nString(UIStrings.cancelRecording)} jslog=${VisualLogging.close().track({click: true})} .data=${ { variant: Buttons.Button.Variant.ICON, size: Buttons.Button.Size.SMALL, iconName: 'cross', } as Buttons.Button.ButtonData } @click=${this.#dispatchRecordingCancelled} ></devtools-button> </div> <label class="row-label" for="user-flow-name">${i18nString( UIStrings.recordingName, )}</label> <input value=${this.#defaultRecordingName} @focus=${this.#onInputFocus} @keydown=${this.#onKeyDown} jslog=${VisualLogging.textField('user-flow-name').track({change: true})} class="devtools-text-input" id="user-flow-name" /> <label class="row-label" for="selector-attribute"> <span>${i18nString(UIStrings.selectorAttribute)}</span> <x-link class="link" href="https://g.co/devtools/recorder#selector" title=${i18nString(UIStrings.learnMore)} jslog=${VisualLogging.link('recorder-selector-help').track({click: true})}> <devtools-icon name="help"> </devtools-icon> </x-link> </label> <input value=${ifDefined(this.#recorderSettings?.selectorAttribute)} placeholder="data-testid" @keydown=${this.#onKeyDown} jslog=${VisualLogging.textField('selector-attribute').track({change: true})} class="devtools-text-input" id="selector-attribute" /> <label class="row-label"> <span>${i18nString(UIStrings.selectorTypes)}</span> <x-link class="link" href="https://g.co/devtools/recorder#selector" title=${i18nString(UIStrings.learnMore)} jslog=${VisualLogging.link('recorder-selector-help').track({click: true})}> <devtools-icon name="help"> </devtools-icon> </x-link> </label> <div class="checkbox-container"> ${Object.values(Models.Schema.SelectorType).map(selectorType => { const checked = this.#recorderSettings?.getSelectorByType(selectorType); return html` <label class="checkbox-label selector-type"> <input @keydown=${this.#onKeyDown} .value=${selectorType} jslog=${VisualLogging.toggle().track({click: true}).context(`selector-${selectorType}`)} ?checked=${checked} type="checkbox" /> ${selectorTypeToLabel.get(selectorType) || selectorType} </label> `; })} </div> ${ this.#error && html` <div class="error" role="alert"> ${this.#error.message} </div> ` } </div> <div class="footer"> <div class="controls"> <devtools-control-button @click=${this.startRecording} .label=${i18nString(UIStrings.startRecording)} .shape=${'circle'} jslog=${VisualLogging.action(Actions.RecorderActions.START_RECORDING).track({click: true})} title=${Models.Tooltip.getTooltipForActions( i18nString(UIStrings.startRecording), Actions.RecorderActions.START_RECORDING, )} ></devtools-control-button> </div> </div> `, this.#shadow, { host: this }, ); // clang-format on } } customElements.define( 'devtools-create-recording-view', CreateRecordingView, );