Path: blob/main/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts
5262 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'));75this.setAriaLabelAttributes(this.element);76return this.renderLabel(this.element);77};7879const isActionsArray = Array.isArray(this.menuActionsOrProvider);80const options: IDropdownMenuOptions = {81contextMenuProvider: this.contextMenuProvider,82labelRenderer: labelRenderer,83menuAsChild: this.options.menuAsChild,84actions: isActionsArray ? this.menuActionsOrProvider as IAction[] : undefined,85actionProvider: isActionsArray ? undefined : this.menuActionsOrProvider as IActionProvider,86skipTelemetry: this.options.skipTelemetry87};8889this.dropdownMenu = this._register(new DropdownMenu(container, options));90this._register(this.dropdownMenu.onDidChangeVisibility(visible => {91this.element?.setAttribute('aria-expanded', `${visible}`);92this._onDidChangeVisibility.fire(visible);93}));9495this.dropdownMenu.menuOptions = {96actionViewItemProvider: this.options.actionViewItemProvider,97actionRunner: this.actionRunner,98getKeyBinding: this.options.keybindingProvider,99context: this._context100};101102if (this.options.anchorAlignmentProvider) {103const that = this;104105this.dropdownMenu.menuOptions = {106...this.dropdownMenu.menuOptions,107get anchorAlignment(): AnchorAlignment {108return that.options.anchorAlignmentProvider!();109}110};111}112113this.updateTooltip();114this.updateEnabled();115}116117protected renderLabel(element: HTMLElement): IDisposable | null {118let classNames: string[] = [];119120if (typeof this.options.classNames === 'string') {121classNames = this.options.classNames.split(/\s+/g).filter(s => !!s);122} else if (this.options.classNames) {123classNames = this.options.classNames;124}125126// todo@aeschli: remove codicon, should come through `this.options.classNames`127if (!classNames.find(c => c === 'icon')) {128classNames.push('codicon');129}130131element.classList.add(...classNames);132133if (this._action.label) {134this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), element, this._action.label));135}136137return null;138}139140protected setAriaLabelAttributes(element: HTMLElement): void {141element.setAttribute('role', 'button');142element.setAttribute('aria-haspopup', 'true');143element.setAttribute('aria-expanded', 'false');144element.ariaLabel = this._action.label || '';145}146147protected override getTooltip(): string | undefined {148let title: string | null = null;149150if (this.action.tooltip) {151title = this.action.tooltip;152} else if (this.action.label) {153title = this.action.label;154}155156return title ?? undefined;157}158159override setActionContext(newContext: unknown): void {160super.setActionContext(newContext);161162if (this.dropdownMenu) {163if (this.dropdownMenu.menuOptions) {164this.dropdownMenu.menuOptions.context = newContext;165} else {166this.dropdownMenu.menuOptions = { context: newContext };167}168}169}170171show(): void {172this.dropdownMenu?.show();173}174175protected override updateEnabled(): void {176const disabled = !this.action.enabled;177this.actionItem?.classList.toggle('disabled', disabled);178this.element?.classList.toggle('disabled', disabled);179}180}181182export interface IActionWithDropdownActionViewItemOptions extends IActionViewItemOptions {183readonly menuActionsOrProvider: readonly IAction[] | IActionProvider;184readonly menuActionClassNames?: string[];185}186187export class ActionWithDropdownActionViewItem extends ActionViewItem {188189protected dropdownMenuActionViewItem: DropdownMenuActionViewItem | undefined;190191constructor(192context: unknown,193action: IAction,194options: IActionWithDropdownActionViewItemOptions,195private readonly contextMenuProvider: IContextMenuProvider196) {197super(context, action, options);198}199200override render(container: HTMLElement): void {201super.render(container);202if (this.element) {203this.element.classList.add('action-dropdown-item');204const menuActionsProvider = {205getActions: () => {206const actionsProvider = (<IActionWithDropdownActionViewItemOptions>this.options).menuActionsOrProvider;207return Array.isArray(actionsProvider) ? actionsProvider : (actionsProvider as IActionProvider).getActions(); // TODO: microsoft/TypeScript#42768208}209};210211const menuActionClassNames = (<IActionWithDropdownActionViewItemOptions>this.options).menuActionClassNames || [];212const separator = h('div.action-dropdown-item-separator', [h('div', {})]).root;213separator.classList.toggle('prominent', menuActionClassNames.includes('prominent'));214append(this.element, separator);215216this.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 }));217this.dropdownMenuActionViewItem.render(this.element);218219this._register(addDisposableListener(this.element, EventType.KEY_DOWN, e => {220// If we don't have any actions then the dropdown is hidden so don't try to focus it #164050221if (menuActionsProvider.getActions().length === 0) {222return;223}224const event = new StandardKeyboardEvent(e);225let handled: boolean = false;226if (this.dropdownMenuActionViewItem?.isFocused() && event.equals(KeyCode.LeftArrow)) {227handled = true;228this.dropdownMenuActionViewItem?.blur();229this.focus();230} else if (this.isFocused() && event.equals(KeyCode.RightArrow)) {231handled = true;232this.blur();233this.dropdownMenuActionViewItem?.focus();234}235if (handled) {236event.preventDefault();237event.stopPropagation();238}239}));240}241}242243override blur(): void {244super.blur();245this.dropdownMenuActionViewItem?.blur();246}247248override setFocusable(focusable: boolean): void {249super.setFocusable(focusable);250this.dropdownMenuActionViewItem?.setFocusable(focusable);251}252}253254255