Path: blob/main/src/vs/workbench/browser/parts/banner/bannerPart.ts
5334 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/bannerpart.css';6import { localize, localize2 } from '../../../../nls.js';7import { $, addDisposableListener, append, clearNode, EventType, isHTMLElement } from '../../../../base/browser/dom.js';8import { asCSSUrl } from '../../../../base/browser/cssValue.js';9import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';10import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';11import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';12import { IStorageService } from '../../../../platform/storage/common/storage.js';13import { IThemeService } from '../../../../platform/theme/common/themeService.js';14import { ThemeIcon } from '../../../../base/common/themables.js';15import { Part } from '../../part.js';16import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js';17import { Action } from '../../../../base/common/actions.js';18import { Link } from '../../../../platform/opener/browser/link.js';19import { MarkdownString } from '../../../../base/common/htmlContent.js';20import { Emitter } from '../../../../base/common/event.js';21import { IBannerItem, IBannerService } from '../../../services/banner/browser/bannerService.js';22import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';23import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';24import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';25import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';26import { KeyCode } from '../../../../base/common/keyCodes.js';27import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';28import { URI } from '../../../../base/common/uri.js';29import { widgetClose } from '../../../../platform/theme/common/iconRegistry.js';30import { BannerFocused } from '../../../common/contextkeys.js';3132// Banner Part3334export class BannerPart extends Part implements IBannerService {3536declare readonly _serviceBrand: undefined;3738// #region IView3940readonly height: number = 26;41readonly minimumWidth: number = 0;42readonly maximumWidth: number = Number.POSITIVE_INFINITY;4344get minimumHeight(): number {45return this.visible ? this.height : 0;46}4748get maximumHeight(): number {49return this.visible ? this.height : 0;50}5152private _onDidChangeSize = this._register(new Emitter<{ width: number; height: number } | undefined>());53override get onDidChange() { return this._onDidChangeSize.event; }5455//#endregion5657private item: IBannerItem | undefined;58private visible = false;5960private actionBar: ActionBar | undefined;61private messageActionsContainer: HTMLElement | undefined;62private focusedActionIndex: number = -1;6364constructor(65@IThemeService themeService: IThemeService,66@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,67@IStorageService storageService: IStorageService,68@IContextKeyService private readonly contextKeyService: IContextKeyService,69@IInstantiationService private readonly instantiationService: IInstantiationService,70@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,71) {72super(Parts.BANNER_PART, { hasTitle: false }, themeService, storageService, layoutService);73}7475protected override createContentArea(parent: HTMLElement): HTMLElement {76this.element = parent;77this.element.tabIndex = 0;7879// Restore focused action if needed80this._register(addDisposableListener(this.element, EventType.FOCUS, () => {81if (this.focusedActionIndex !== -1) {82this.focusActionLink();83}84}));8586// Track focus87const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element));88BannerFocused.bindTo(scopedContextKeyService).set(true);8990return this.element;91}9293private close(item: IBannerItem): void {94// Hide banner95this.setVisibility(false);9697// Remove from document98clearNode(this.element);99100// Remember choice101if (typeof item.onClose === 'function') {102item.onClose();103}104105this.item = undefined;106}107108private focusActionLink(): void {109const length = this.item?.actions?.length ?? 0;110111if (this.focusedActionIndex < length) {112const actionLink = this.messageActionsContainer?.children[this.focusedActionIndex];113if (isHTMLElement(actionLink)) {114this.actionBar?.setFocusable(false);115actionLink.focus();116}117} else {118this.actionBar?.focus(0);119}120}121122private getAriaLabel(item: IBannerItem): string | undefined {123if (item.ariaLabel) {124return item.ariaLabel;125}126if (typeof item.message === 'string') {127return item.message;128}129130return undefined;131}132133private getBannerMessage(message: MarkdownString | string): HTMLElement {134if (typeof message === 'string') {135const element = $('span');136element.textContent = message;137return element;138}139140return this.markdownRendererService.render(message).element;141}142143private setVisibility(visible: boolean): void {144if (visible !== this.visible) {145this.visible = visible;146this.focusedActionIndex = -1;147148this.layoutService.setPartHidden(!visible, Parts.BANNER_PART);149this._onDidChangeSize.fire(undefined);150}151}152153focus(): void {154this.focusedActionIndex = -1;155this.element.focus();156}157158focusNextAction(): void {159const length = this.item?.actions?.length ?? 0;160this.focusedActionIndex = this.focusedActionIndex < length ? this.focusedActionIndex + 1 : 0;161162this.focusActionLink();163}164165focusPreviousAction(): void {166const length = this.item?.actions?.length ?? 0;167this.focusedActionIndex = this.focusedActionIndex > 0 ? this.focusedActionIndex - 1 : length;168169this.focusActionLink();170}171172hide(id: string): void {173if (this.item?.id !== id) {174return;175}176177this.setVisibility(false);178}179180show(item: IBannerItem): void {181if (item.id === this.item?.id) {182this.setVisibility(true);183return;184}185186// Clear previous item187clearNode(this.element);188189// Banner aria label190const ariaLabel = this.getAriaLabel(item);191if (ariaLabel) {192this.element.setAttribute('aria-label', ariaLabel);193}194195// Icon196const iconContainer = append(this.element, $('div.icon-container'));197iconContainer.setAttribute('aria-hidden', 'true');198199if (ThemeIcon.isThemeIcon(item.icon)) {200iconContainer.appendChild($(`div${ThemeIcon.asCSSSelector(item.icon)}`));201} else {202iconContainer.classList.add('custom-icon');203204if (URI.isUri(item.icon)) {205iconContainer.style.backgroundImage = asCSSUrl(item.icon);206}207}208209// Message210const messageContainer = append(this.element, $('div.message-container'));211messageContainer.setAttribute('aria-hidden', 'true');212messageContainer.appendChild(this.getBannerMessage(item.message));213214// Message Actions215this.messageActionsContainer = append(this.element, $('div.message-actions-container'));216if (item.actions) {217for (const action of item.actions) {218this._register(this.instantiationService.createInstance(Link, this.messageActionsContainer, { ...action, tabIndex: -1 }, {}));219}220}221222// Action223const actionBarContainer = append(this.element, $('div.action-container'));224this.actionBar = this._register(new ActionBar(actionBarContainer));225const label = item.closeLabel ?? localize('closeBanner', "Close Banner");226const closeAction = this._register(new Action('banner.close', label, ThemeIcon.asClassName(widgetClose), true, () => this.close(item)));227this.actionBar.push(closeAction, { icon: true, label: false });228this.actionBar.setFocusable(false);229230this.setVisibility(true);231this.item = item;232}233234toJSON(): object {235return {236type: Parts.BANNER_PART237};238}239}240241registerSingleton(IBannerService, BannerPart, InstantiationType.Eager);242243244// Keybindings245246KeybindingsRegistry.registerCommandAndKeybindingRule({247id: 'workbench.banner.focusBanner',248weight: KeybindingWeight.WorkbenchContrib,249primary: KeyCode.Escape,250when: BannerFocused,251handler: (accessor: ServicesAccessor) => {252const bannerService = accessor.get(IBannerService);253bannerService.focus();254}255});256257KeybindingsRegistry.registerCommandAndKeybindingRule({258id: 'workbench.banner.focusNextAction',259weight: KeybindingWeight.WorkbenchContrib,260primary: KeyCode.RightArrow,261secondary: [KeyCode.DownArrow],262when: BannerFocused,263handler: (accessor: ServicesAccessor) => {264const bannerService = accessor.get(IBannerService);265bannerService.focusNextAction();266}267});268269KeybindingsRegistry.registerCommandAndKeybindingRule({270id: 'workbench.banner.focusPreviousAction',271weight: KeybindingWeight.WorkbenchContrib,272primary: KeyCode.LeftArrow,273secondary: [KeyCode.UpArrow],274when: BannerFocused,275handler: (accessor: ServicesAccessor) => {276const bannerService = accessor.get(IBannerService);277bannerService.focusPreviousAction();278}279});280281282// Actions283284class FocusBannerAction extends Action2 {285286static readonly ID = 'workbench.action.focusBanner';287static readonly LABEL = localize2('focusBanner', "Focus Banner");288289constructor() {290super({291id: FocusBannerAction.ID,292title: FocusBannerAction.LABEL,293category: Categories.View,294f1: true295});296}297298async run(accessor: ServicesAccessor): Promise<void> {299const layoutService = accessor.get(IWorkbenchLayoutService);300layoutService.focusPart(Parts.BANNER_PART);301}302}303304registerAction2(FocusBannerAction);305306307