Path: blob/main/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts
3296 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 * as nls from '../../../../nls.js';6import { Action, IAction, IActionRunner } from '../../../common/actions.js';7import { Codicon } from '../../../common/codicons.js';8import { Emitter } from '../../../common/event.js';9import { ResolvedKeybinding } from '../../../common/keybindings.js';10import { KeyCode } from '../../../common/keyCodes.js';11import { IDisposable } from '../../../common/lifecycle.js';12import { ThemeIcon } from '../../../common/themables.js';13import { IContextMenuProvider } from '../../contextmenu.js';14import { $, addDisposableListener, append, EventType, h } from '../../dom.js';15import { StandardKeyboardEvent } from '../../keyboardEvent.js';16import { IActionViewItemProvider } from '../actionbar/actionbar.js';17import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions, IBaseActionViewItemOptions } from '../actionbar/actionViewItems.js';18import { AnchorAlignment } from '../contextview/contextview.js';19import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';20import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';21import './dropdown.css';22import { DropdownMenu, IActionProvider, IDropdownMenuOptions, ILabelRenderer } from './dropdown.js';2324export interface IKeybindingProvider {25(action: IAction): ResolvedKeybinding | undefined;26}2728export interface IAnchorAlignmentProvider {29(): AnchorAlignment;30}3132export interface IDropdownMenuActionViewItemOptions extends IBaseActionViewItemOptions {33readonly actionViewItemProvider?: IActionViewItemProvider;34readonly keybindingProvider?: IKeybindingProvider;35readonly actionRunner?: IActionRunner;36readonly classNames?: string[] | string;37readonly anchorAlignmentProvider?: IAnchorAlignmentProvider;38readonly menuAsChild?: boolean;39readonly skipTelemetry?: boolean;40}4142export class DropdownMenuActionViewItem extends BaseActionViewItem {43private menuActionsOrProvider: readonly IAction[] | IActionProvider;44private dropdownMenu: DropdownMenu | undefined;45private contextMenuProvider: IContextMenuProvider;46private actionItem: HTMLElement | null = null;4748private _onDidChangeVisibility = this._register(new Emitter<boolean>());49get onDidChangeVisibility() { return this._onDidChangeVisibility.event; }5051protected override readonly options: IDropdownMenuActionViewItemOptions;5253constructor(54action: IAction,55menuActionsOrProvider: readonly IAction[] | IActionProvider,56contextMenuProvider: IContextMenuProvider,57options: IDropdownMenuActionViewItemOptions = Object.create(null)58) {59super(null, action, options);6061this.menuActionsOrProvider = menuActionsOrProvider;62this.contextMenuProvider = contextMenuProvider;63this.options = options;6465if (this.options.actionRunner) {66this.actionRunner = this.options.actionRunner;67}68}6970override render(container: HTMLElement): void {71this.actionItem = container;7273const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => {74this.element = append(el, $('a.action-label'));75return this.renderLabel(this.element);76};7778const isActionsArray = Array.isArray(this.menuActionsOrProvider);79const options: IDropdownMenuOptions = {80contextMenuProvider: this.contextMenuProvider,81labelRenderer: labelRenderer,82menuAsChild: this.options.menuAsChild,83actions: isActionsArray ? this.menuActionsOrProvider as IAction[] : undefined,84actionProvider: isActionsArray ? undefined : this.menuActionsOrProvider as IActionProvider,85skipTelemetry: this.options.skipTelemetry86};8788this.dropdownMenu = this._register(new DropdownMenu(container, options));89this._register(this.dropdownMenu.onDidChangeVisibility(visible => {90this.element?.setAttribute('aria-expanded', `${visible}`);91this._onDidChangeVisibility.fire(visible);92}));9394this.dropdownMenu.menuOptions = {95actionViewItemProvider: this.options.actionViewItemProvider,96actionRunner: this.actionRunner,97getKeyBinding: this.options.keybindingProvider,98context: this._context99};100101if (this.options.anchorAlignmentProvider) {102const that = this;103104this.dropdownMenu.menuOptions = {105...this.dropdownMenu.menuOptions,106get anchorAlignment(): AnchorAlignment {107return that.options.anchorAlignmentProvider!();108}109};110}111112this.updateTooltip();113this.updateEnabled();114}115116protected renderLabel(element: HTMLElement): IDisposable | null {117let classNames: string[] = [];118119if (typeof this.options.classNames === 'string') {120classNames = this.options.classNames.split(/\s+/g).filter(s => !!s);121} else if (this.options.classNames) {122classNames = this.options.classNames;123}124125// todo@aeschli: remove codicon, should come through `this.options.classNames`126if (!classNames.find(c => c === 'icon')) {127classNames.push('codicon');128}129130element.classList.add(...classNames);131132if (this._action.label) {133this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), element, this._action.label));134}135136return null;137}138139protected setAriaLabelAttributes(element: HTMLElement): void {140element.setAttribute('role', 'button');141element.setAttribute('aria-haspopup', 'true');142element.setAttribute('aria-expanded', 'false');143element.ariaLabel = this._action.label || '';144}145146protected override getTooltip(): string | undefined {147let title: string | null = null;148149if (this.action.tooltip) {150title = this.action.tooltip;151} else if (this.action.label) {152title = this.action.label;153}154155return title ?? undefined;156}157158override setActionContext(newContext: unknown): void {159super.setActionContext(newContext);160161if (this.dropdownMenu) {162if (this.dropdownMenu.menuOptions) {163this.dropdownMenu.menuOptions.context = newContext;164} else {165this.dropdownMenu.menuOptions = { context: newContext };166}167}168}169170show(): void {171this.dropdownMenu?.show();172}173174protected override updateEnabled(): void {175const disabled = !this.action.enabled;176this.actionItem?.classList.toggle('disabled', disabled);177this.element?.classList.toggle('disabled', disabled);178}179}180181export interface IActionWithDropdownActionViewItemOptions extends IActionViewItemOptions {182readonly menuActionsOrProvider: readonly IAction[] | IActionProvider;183readonly menuActionClassNames?: string[];184}185186export class ActionWithDropdownActionViewItem extends ActionViewItem {187188protected dropdownMenuActionViewItem: DropdownMenuActionViewItem | undefined;189190constructor(191context: unknown,192action: IAction,193options: IActionWithDropdownActionViewItemOptions,194private readonly contextMenuProvider: IContextMenuProvider195) {196super(context, action, options);197}198199override render(container: HTMLElement): void {200super.render(container);201if (this.element) {202this.element.classList.add('action-dropdown-item');203const menuActionsProvider = {204getActions: () => {205const actionsProvider = (<IActionWithDropdownActionViewItemOptions>this.options).menuActionsOrProvider;206return Array.isArray(actionsProvider) ? actionsProvider : (actionsProvider as IActionProvider).getActions(); // TODO: microsoft/TypeScript#42768207}208};209210const menuActionClassNames = (<IActionWithDropdownActionViewItemOptions>this.options).menuActionClassNames || [];211const separator = h('div.action-dropdown-item-separator', [h('div', {})]).root;212separator.classList.toggle('prominent', menuActionClassNames.includes('prominent'));213append(this.element, separator);214215this.dropdownMenuActionViewItem = this._register(new DropdownMenuActionViewItem(this._register(new Action('dropdownAction', nls.localize('moreActions', "More Actions..."))), menuActionsProvider, this.contextMenuProvider, { classNames: ['dropdown', ...ThemeIcon.asClassNameArray(Codicon.dropDownButton), ...menuActionClassNames], hoverDelegate: this.options.hoverDelegate }));216this.dropdownMenuActionViewItem.render(this.element);217218this._register(addDisposableListener(this.element, EventType.KEY_DOWN, e => {219// If we don't have any actions then the dropdown is hidden so don't try to focus it #164050220if (menuActionsProvider.getActions().length === 0) {221return;222}223const event = new StandardKeyboardEvent(e);224let handled: boolean = false;225if (this.dropdownMenuActionViewItem?.isFocused() && event.equals(KeyCode.LeftArrow)) {226handled = true;227this.dropdownMenuActionViewItem?.blur();228this.focus();229} else if (this.isFocused() && event.equals(KeyCode.RightArrow)) {230handled = true;231this.blur();232this.dropdownMenuActionViewItem?.focus();233}234if (handled) {235event.preventDefault();236event.stopPropagation();237}238}));239}240}241242override blur(): void {243super.blur();244this.dropdownMenuActionViewItem?.blur();245}246247override setFocusable(focusable: boolean): void {248super.setFocusable(focusable);249this.dropdownMenuActionViewItem?.setFocusable(focusable);250}251}252253254