| // Copyright 2016 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-imperative-dom-api */ |
| |
| import * as i18n from '../../core/i18n/i18n.js'; |
| import * as Platform from '../../core/platform/platform.js'; |
| import type * as SDK from '../../core/sdk/sdk.js'; |
| import * as UI from '../../ui/legacy/legacy.js'; |
| import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; |
| |
| import {AccessibilitySubPane} from './AccessibilitySubPane.js'; |
| import {ariaMetadata} from './ARIAMetadata.js'; |
| |
| const UIStrings = { |
| /** |
| *@description Text in ARIAAttributes View of the Accessibility panel |
| */ |
| ariaAttributes: 'ARIA Attributes', |
| /** |
| *@description Text in ARIAAttributes View of the Accessibility panel |
| */ |
| noAriaAttributes: 'No ARIA attributes', |
| } as const; |
| const str_ = i18n.i18n.registerUIStrings('panels/accessibility/ARIAAttributesView.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| export class ARIAAttributesPane extends AccessibilitySubPane { |
| private readonly noPropertiesInfo: Element; |
| private readonly treeOutline: UI.TreeOutline.TreeOutline; |
| constructor() { |
| super(i18nString(UIStrings.ariaAttributes)); |
| |
| this.noPropertiesInfo = this.createInfo(i18nString(UIStrings.noAriaAttributes)); |
| this.treeOutline = this.createTreeOutline(); |
| this.element.setAttribute('jslog', `${VisualLogging.section('aria-attributes')}`); |
| } |
| |
| override setNode(node: SDK.DOMModel.DOMNode|null): void { |
| super.setNode(node); |
| this.treeOutline.removeChildren(); |
| if (!node) { |
| return; |
| } |
| const target = node.domModel().target(); |
| const attributes = node.attributes(); |
| for (let i = 0; i < attributes.length; ++i) { |
| const attribute = attributes[i]; |
| if (!this.isARIAAttribute(attribute)) { |
| continue; |
| } |
| |
| this.treeOutline.appendChild(new ARIAAttributesTreeElement(this, attribute, target)); |
| } |
| |
| const foundAttributes = (this.treeOutline.rootElement().childCount() !== 0); |
| this.noPropertiesInfo.classList.toggle('hidden', foundAttributes); |
| this.treeOutline.element.classList.toggle('hidden', !foundAttributes); |
| } |
| |
| private isARIAAttribute(attribute: SDK.DOMModel.Attribute): boolean { |
| return ATTRIBUTES.has(attribute.name); |
| } |
| } |
| |
| export class ARIAAttributesTreeElement extends UI.TreeOutline.TreeElement { |
| private readonly parentPane: ARIAAttributesPane; |
| private readonly attribute: SDK.DOMModel.Attribute; |
| private nameElement?: HTMLSpanElement; |
| private valueElement?: Element; |
| private prompt?: ARIAAttributePrompt; |
| |
| constructor(parentPane: ARIAAttributesPane, attribute: SDK.DOMModel.Attribute, _target: SDK.Target.Target) { |
| super(''); |
| |
| this.parentPane = parentPane; |
| this.attribute = attribute; |
| |
| this.selectable = false; |
| } |
| |
| static createARIAValueElement(value: string): Element { |
| const valueElement = document.createElement('span'); |
| valueElement.classList.add('monospace'); |
| // TODO(aboxhall): quotation marks? |
| valueElement.setTextContentTruncatedIfNeeded(value || ''); |
| return valueElement; |
| } |
| |
| override onattach(): void { |
| this.populateListItem(); |
| this.listItemElement.addEventListener('click', this.mouseClick.bind(this)); |
| } |
| |
| private populateListItem(): void { |
| this.listItemElement.removeChildren(); |
| this.appendNameElement(this.attribute.name); |
| this.listItemElement.createChild('span', 'separator').textContent = ':\xA0'; |
| this.appendAttributeValueElement(this.attribute.value); |
| } |
| |
| appendNameElement(name: string): void { |
| this.nameElement = document.createElement('span'); |
| this.nameElement.textContent = name; |
| this.nameElement.classList.add('ax-name'); |
| this.nameElement.classList.add('monospace'); |
| this.listItemElement.appendChild(this.nameElement); |
| } |
| |
| appendAttributeValueElement(value: string): void { |
| this.valueElement = ARIAAttributesTreeElement.createARIAValueElement(value); |
| this.listItemElement.appendChild(this.valueElement); |
| } |
| |
| private mouseClick(event: Event): void { |
| if (event.target === this.listItemElement) { |
| return; |
| } |
| |
| event.consume(true); |
| |
| this.startEditing(); |
| } |
| |
| private startEditing(): void { |
| const valueElement = this.valueElement; |
| |
| if (!valueElement || UI.UIUtils.isBeingEdited(valueElement)) { |
| return; |
| } |
| |
| const previousContent = valueElement.textContent || ''; |
| |
| function blurListener(this: ARIAAttributesTreeElement, previousContent: string, event: Event): void { |
| const target = event.target as HTMLElement; |
| const text = target.textContent || ''; |
| this.editingCommitted(text, previousContent); |
| } |
| |
| const attributeName = (this.nameElement as HTMLSpanElement).textContent || ''; |
| this.prompt = new ARIAAttributePrompt(ariaMetadata().valuesForProperty(attributeName)); |
| this.prompt.setAutocompletionTimeout(0); |
| const proxyElement = |
| this.prompt.attachAndStartEditing(valueElement, blurListener.bind(this, previousContent)) as HTMLElement; |
| |
| proxyElement.addEventListener('keydown', event => this.editingValueKeyDown(previousContent, event), false); |
| |
| const selection = valueElement.getComponentSelection(); |
| if (selection) { |
| selection.selectAllChildren(valueElement); |
| } |
| } |
| |
| private removePrompt(): void { |
| if (!this.prompt) { |
| return; |
| } |
| this.prompt.detach(); |
| delete this.prompt; |
| } |
| |
| private editingCommitted(userInput: string, previousContent: string): void { |
| this.removePrompt(); |
| |
| // Make the changes to the attribute |
| if (userInput !== previousContent) { |
| const node = this.parentPane.node() as SDK.DOMModel.DOMNode; |
| node.setAttributeValue(this.attribute.name, userInput); |
| } |
| } |
| |
| private editingCancelled(): void { |
| this.removePrompt(); |
| this.populateListItem(); |
| } |
| |
| private editingValueKeyDown(previousContent: string, event: KeyboardEvent): void { |
| if (event.handled) { |
| return; |
| } |
| |
| if (event.key === 'Enter') { |
| const target = event.target as HTMLElement; |
| this.editingCommitted(target.textContent || '', previousContent); |
| event.consume(); |
| return; |
| } |
| |
| if (Platform.KeyboardUtilities.isEscKey(event)) { |
| this.editingCancelled(); |
| event.consume(); |
| return; |
| } |
| } |
| } |
| |
| export class ARIAAttributePrompt extends UI.TextPrompt.TextPrompt { |
| private readonly ariaCompletions: string[]; |
| constructor(ariaCompletions: string[]) { |
| super(); |
| this.initialize(this.buildPropertyCompletions.bind(this)); |
| |
| this.ariaCompletions = ariaCompletions; |
| } |
| |
| private async buildPropertyCompletions(expression: string, prefix: string, force?: boolean): |
| Promise<UI.SuggestBox.Suggestions> { |
| prefix = prefix.toLowerCase(); |
| if (!prefix && !force && expression) { |
| return []; |
| } |
| return this.ariaCompletions.filter(value => value.startsWith(prefix)).map(c => { |
| return { |
| text: c, |
| title: undefined, |
| subtitle: undefined, |
| priority: undefined, |
| isSecondary: undefined, |
| subtitleRenderer: undefined, |
| selectionRange: undefined, |
| hideGhostText: undefined, |
| iconElement: undefined, |
| }; |
| }); |
| } |
| } |
| |
| // Keep this list in sync with https://w3c.github.io/aria/#state_prop_def |
| const ATTRIBUTES = new Set<string>([ |
| 'role', |
| 'aria-activedescendant', |
| 'aria-atomic', |
| 'aria-autocomplete', |
| 'aria-braillelabel', |
| 'aria-brailleroledescription', |
| 'aria-busy', |
| 'aria-checked', |
| 'aria-colcount', |
| 'aria-colindex', |
| 'aria-colindextext', |
| 'aria-colspan', |
| 'aria-controls', |
| 'aria-current', |
| 'aria-describedby', |
| 'aria-description', |
| 'aria-details', |
| 'aria-disabled', |
| 'aria-dropeffect', |
| 'aria-errormessage', |
| 'aria-expanded', |
| 'aria-flowto', |
| 'aria-grabbed', |
| 'aria-haspopup', |
| 'aria-hidden', |
| 'aria-invalid', |
| 'aria-keyshortcuts', |
| 'aria-label', |
| 'aria-labelledby', |
| 'aria-level', |
| 'aria-live', |
| 'aria-modal', |
| 'aria-multiline', |
| 'aria-multiselectable', |
| 'aria-orientation', |
| 'aria-owns', |
| 'aria-placeholder', |
| 'aria-posinset', |
| 'aria-pressed', |
| 'aria-readonly', |
| 'aria-relevant', |
| 'aria-required', |
| 'aria-roledescription', |
| 'aria-rowcount', |
| 'aria-rowindex', |
| 'aria-rowindextext', |
| 'aria-rowspan', |
| 'aria-selected', |
| 'aria-setsize', |
| 'aria-sort', |
| 'aria-valuemax', |
| 'aria-valuemin', |
| 'aria-valuenow', |
| 'aria-valuetext', |
| ]); |