| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import '../../ui/legacy/legacy.js'; |
| |
| import * as i18n from '../../core/i18n/i18n.js'; |
| import * as Buttons from '../../ui/components/buttons/buttons.js'; |
| import * as UI from '../../ui/legacy/legacy.js'; |
| import {Directives, html, render} from '../../ui/lit/lit.js'; |
| import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; |
| |
| import cssOverviewSidebarPanelStyles from './cssOverviewSidebarPanel.css.js'; |
| |
| const {classMap} = Directives; |
| |
| const UIStrings = { |
| /** |
| * @description Label for the 'Clear overview' button in the CSS overview report |
| */ |
| clearOverview: 'Clear overview', |
| /** |
| * @description Accessible label for the CSS overview panel sidebar |
| */ |
| cssOverviewPanelSidebar: 'CSS overview panel sidebar', |
| } as const; |
| const str_ = i18n.i18n.registerUIStrings('panels/css_overview/CSSOverviewSidebarPanel.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| interface ViewInput { |
| items: Array<{name: string, id: string}>; |
| selectedId?: string; |
| onReset: () => void; |
| onItemClick: (id: string) => void; |
| onItemKeyDown: (id: string, key: string) => void; |
| } |
| type View = (input: ViewInput, output: object, target: HTMLElement) => void; |
| |
| export const DEFAULT_VIEW: View = (input, _output, target) => { |
| const onClick = (event: Event): void => { |
| if (event.target instanceof HTMLElement) { |
| const id = event.target.dataset.id; |
| if (id) { |
| input.onItemClick(id); |
| } |
| } |
| }; |
| const onKeyDown = (event: KeyboardEvent): void => { |
| if (event.key !== 'Enter' && event.key !== 'ArrowUp' && event.key !== 'ArrowDown') { |
| return; |
| } |
| if (event.target instanceof HTMLElement) { |
| const id = event.target.dataset.id; |
| if (id) { |
| input.onItemKeyDown(id, event.key); |
| } |
| } |
| |
| event.consume(true); |
| }; |
| |
| // clang-format off |
| render(html` |
| <style>${cssOverviewSidebarPanelStyles}</style> |
| <div class="overview-sidebar-panel" @click=${onClick} @keydown=${onKeyDown} |
| aria-label=${i18nString(UIStrings.cssOverviewPanelSidebar)} role="tree"> |
| <div class="overview-toolbar"> |
| <devtools-toolbar> |
| <devtools-button title=${i18nString(UIStrings.clearOverview)} @click=${input.onReset} |
| .iconName=${'clear'} .variant=${Buttons.Button.Variant.TOOLBAR} |
| .jslogContext=${'css-overview.clear-overview'}></devtools-button> |
| </devtools-toolbar> |
| </div> |
| ${input.items.map(({id, name}) => { |
| const selected = id === input.selectedId; |
| return html` |
| <div class="overview-sidebar-panel-item ${classMap({selected})}" |
| ?autofocus=${selected} |
| role="treeitem" data-id=${id} tabindex="0" |
| jslog=${VisualLogging.item(`css-overview.${id}`) |
| .track({click: true, keydown: 'Enter|ArrowUp|ArrowDown'})}> |
| ${name} |
| </div>`; |
| })} |
| </div>`, |
| target); |
| // clang-format on |
| }; |
| |
| export class CSSOverviewSidebarPanel extends UI.Widget.VBox { |
| #view: View; |
| #items: Array<{name: string, id: string}> = []; |
| #selectedId?: string; |
| #onItemSelected = (_id: string, _shouldFocus: boolean): void => {}; |
| #onReset = (): void => {}; |
| |
| constructor(element?: HTMLElement, view = DEFAULT_VIEW) { |
| super(element, {useShadowDom: true, delegatesFocus: true}); |
| this.#view = view; |
| } |
| |
| override performUpdate(): void { |
| const viewInput = { |
| items: this.#items, |
| selectedId: this.#selectedId, |
| onReset: this.#onReset, |
| onItemClick: this.#onItemClick.bind(this), |
| onItemKeyDown: this.#onItemKeyDown.bind(this) |
| }; |
| this.#view(viewInput, {}, this.contentElement); |
| } |
| |
| set items(items: Array<{name: string, id: string}>) { |
| this.#items = items; |
| this.requestUpdate(); |
| } |
| |
| set selectedId(id: string) { |
| void this.#select(id); |
| } |
| |
| set onItemSelected(callback: (id: string, shouldFocus: boolean) => void) { |
| this.#onItemSelected = callback; |
| this.requestUpdate(); |
| } |
| |
| set onReset(callback: () => void) { |
| this.#onReset = callback; |
| this.requestUpdate(); |
| } |
| |
| #select(id: string, shouldFocus = false): Promise<void> { |
| this.#selectedId = id; |
| this.requestUpdate(); |
| this.#onItemSelected(id, shouldFocus); |
| return this.updateComplete; |
| } |
| |
| #onItemClick(id: string): void { |
| void this.#select(id, false); |
| } |
| |
| #onItemKeyDown(id: string, key: string): void { |
| if (key === 'Enter') { |
| void this.#select(id, true); |
| } else { // arrow up/down key |
| let currItemIndex = -1; |
| for (let idx = 0; idx < this.#items.length; idx++) { |
| if (this.#items[idx].id === id) { |
| currItemIndex = idx; |
| break; |
| } |
| } |
| if (currItemIndex < 0) { |
| return; |
| } |
| |
| const moveTo = (key === 'ArrowDown' ? 1 : -1); |
| const nextItemIndex = (currItemIndex + moveTo) % this.#items.length; |
| const nextItemId = this.#items[nextItemIndex].id; |
| if (!nextItemId) { |
| return; |
| } |
| |
| void this.#select(nextItemId, false).then(() => { |
| this.element.blur(); |
| this.element.focus(); |
| }); |
| } |
| } |
| } |