Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 1 | // Copyright 2020 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
Changhao Han | 5040a44 | 2020-06-24 11:56:05 | [diff] [blame] | 5 | import * as Common from '../common/common.js'; |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 6 | import * as UI from '../ui/ui.js'; |
| 7 | |
Changhao Han | 5040a44 | 2020-06-24 11:56:05 | [diff] [blame] | 8 | const ls = Common.ls; |
| 9 | |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 10 | /** |
| 11 | * @enum {string} |
| 12 | * Use a normal object instead of making it null-prototyped because |
| 13 | * Closure requires enum initialization to be an object literal. |
| 14 | * Will be a proper enum class once this file becomes TypeScript. |
| 15 | */ |
| 16 | export const AdornerCategories = { |
| 17 | Security: 'Security', |
| 18 | Layout: 'Layout', |
| 19 | Default: 'Default', |
| 20 | }; |
| 21 | Object.freeze(AdornerCategories); |
| 22 | |
| 23 | const template = document.createElement('template'); |
| 24 | template.innerHTML = ` |
| 25 | <style> |
| 26 | :host { |
| 27 | display: inline-flex; |
| 28 | } |
| 29 | |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 30 | :host(.hidden) { |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 31 | display: none; |
| 32 | } |
| 33 | |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 34 | :host(.clickable) { |
| 35 | cursor: pointer; |
| 36 | } |
| 37 | |
| 38 | :host(:focus) slot { |
| 39 | border: var(--adorner-border-focus, 1px solid #1a73e8); |
| 40 | } |
| 41 | |
| 42 | :host([aria-pressed=true]) slot { |
| 43 | color: var(--adorner-text-color-active, #ffffff); |
| 44 | background-color: var(--adorner-background-color-active, #1a73e8); |
| 45 | } |
| 46 | |
| 47 | slot { |
Changhao Han | bc887de | 2020-06-22 08:32:05 | [diff] [blame] | 48 | display: inline-flex; |
| 49 | box-sizing: border-box; |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 50 | height: 13px; |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 51 | padding: 0 6px; |
| 52 | font-size: 8.5px; |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 53 | color: var(--adorner-text-color, #3c4043); |
| 54 | background-color: var(--adorner-background-color, #f1f3f4); |
| 55 | border: var(--adorner-border, 1px solid #dadce0); |
| 56 | border-radius: var(--adorner-border-radius, 10px); |
| 57 | } |
| 58 | |
Changhao Han | bc887de | 2020-06-22 08:32:05 | [diff] [blame] | 59 | ::slotted(*) { |
| 60 | height: 10px; |
| 61 | } |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 62 | </style> |
| 63 | <slot name="content"></slot> |
| 64 | `; |
| 65 | |
| 66 | export class Adorner extends HTMLElement { |
| 67 | /** |
| 68 | * |
| 69 | * @param {!HTMLElement} content |
| 70 | * @param {string} name |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 71 | * @param {!{category: (!AdornerCategories|undefined)}} options |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 72 | * @return {!Adorner} |
| 73 | */ |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 74 | // @ts-ignore typedef TODO(changhaohan): properly type options once this is .ts |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 75 | static create(content, name, options = {}) { |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 76 | const {category = AdornerCategories.Default} = options; |
| 77 | |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 78 | const adorner = /** @type {!Adorner} */ (document.createElement('devtools-adorner')); |
| 79 | content.slot = 'content'; |
| 80 | adorner.append(content); |
| 81 | |
| 82 | adorner.name = name; |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 83 | adorner.category = category; |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 84 | |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 85 | return adorner; |
| 86 | } |
| 87 | |
| 88 | constructor() { |
| 89 | super(); |
| 90 | |
| 91 | const shadowRoot = this.attachShadow({mode: 'open'}); |
| 92 | shadowRoot.appendChild(template.content.cloneNode(true)); |
| 93 | |
| 94 | this.name = ''; |
| 95 | this.category = AdornerCategories.Default; |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 96 | this._isToggle = false; |
Changhao Han | 5040a44 | 2020-06-24 11:56:05 | [diff] [blame] | 97 | this._ariaLabelDefault = ls`adorner`; |
| 98 | this._ariaLabelActive = ls`adorner active`; |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 99 | } |
| 100 | |
| 101 | /** |
| 102 | * @override |
| 103 | */ |
| 104 | connectedCallback() { |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 105 | if (!this.getAttribute('aria-label')) { |
Changhao Han | 5040a44 | 2020-06-24 11:56:05 | [diff] [blame] | 106 | UI.ARIAUtils.setAccessibleName(this, ls`${this.name} adorner`); |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 107 | } |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 108 | } |
| 109 | |
Changhao Han | b0f1ce1 | 2020-07-28 07:22:26 | [diff] [blame] | 110 | /** |
| 111 | * @return {boolean} |
| 112 | */ |
| 113 | isActive() { |
| 114 | return this.getAttribute('aria-pressed') === 'true'; |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * Toggle the active state of the adorner. Optionally pass `true` to force-set |
| 119 | * an active state; pass `false` to force-set an inactive state. |
| 120 | * @param {boolean=} forceActiveState |
| 121 | */ |
| 122 | toggle(forceActiveState) { |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 123 | if (!this._isToggle) { |
| 124 | return; |
| 125 | } |
Changhao Han | b0f1ce1 | 2020-07-28 07:22:26 | [diff] [blame] | 126 | const shouldBecomeActive = forceActiveState === undefined ? !this.isActive() : forceActiveState; |
| 127 | UI.ARIAUtils.setPressed(this, shouldBecomeActive); |
| 128 | UI.ARIAUtils.setAccessibleName(this, shouldBecomeActive ? this._ariaLabelActive : this._ariaLabelDefault); |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 129 | } |
| 130 | |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 131 | show() { |
| 132 | this.classList.remove('hidden'); |
| 133 | } |
| 134 | |
| 135 | hide() { |
| 136 | this.classList.add('hidden'); |
| 137 | } |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 138 | |
| 139 | /** |
| 140 | * Make adorner interactive by responding to click events with the provided action |
| 141 | * and simulating ARIA-capable toggle button behavior. |
| 142 | * @param {!EventListener} action |
| 143 | * @param {!{isToggle: (boolean|undefined), shouldPropagateOnKeydown: (boolean|undefined), ariaLabelDefault: (string|undefined), ariaLabelActive: (string|undefined)}} options |
| 144 | */ |
| 145 | // @ts-ignore typedef TODO(changhaohan): properly type options once this is .ts |
| 146 | addInteraction(action, options = {}) { |
| 147 | const {isToggle = false, shouldPropagateOnKeydown = false, ariaLabelDefault, ariaLabelActive} = options; |
| 148 | |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 149 | this._isToggle = isToggle; |
| 150 | |
| 151 | if (ariaLabelDefault) { |
| 152 | this._ariaLabelDefault = ariaLabelDefault; |
| 153 | UI.ARIAUtils.setAccessibleName(this, ariaLabelDefault); |
| 154 | } |
| 155 | |
| 156 | if (isToggle) { |
Changhao Han | b0f1ce1 | 2020-07-28 07:22:26 | [diff] [blame] | 157 | this.addEventListener('click', () => { |
| 158 | this.toggle(); |
| 159 | }); |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 160 | if (ariaLabelActive) { |
| 161 | this._ariaLabelActive = ariaLabelActive; |
| 162 | } |
Changhao Han | b0f1ce1 | 2020-07-28 07:22:26 | [diff] [blame] | 163 | this.toggle(false /* initialize inactive state */); |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 164 | } |
| 165 | |
Changhao Han | 1f94e60 | 2020-08-06 14:57:50 | [diff] [blame] | 166 | this.addEventListener('click', action); |
| 167 | |
Changhao Han | 72f885b | 2020-06-02 13:27:10 | [diff] [blame] | 168 | // Simulate an ARIA-capable toggle button |
| 169 | this.classList.add('clickable'); |
| 170 | UI.ARIAUtils.markAsButton(this); |
| 171 | this.tabIndex = 0; |
| 172 | this.addEventListener('keydown', event => { |
| 173 | if (event.code === 'Enter' || event.code === 'Space') { |
| 174 | this.click(); |
| 175 | if (!shouldPropagateOnKeydown) { |
| 176 | event.stopPropagation(); |
| 177 | } |
| 178 | } |
| 179 | }); |
| 180 | } |
Changhao Han | 006b8c0 | 2020-04-27 14:44:05 | [diff] [blame] | 181 | } |
| 182 | |
| 183 | self.customElements.define('devtools-adorner', Adorner); |