Path: blob/main/src/vs/platform/actionWidget/browser/actionWidgetDropdown.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 { IActionWidgetService } from './actionWidget.js';6import { IAction } from '../../../base/common/actions.js';7import { BaseDropdown, IActionProvider, IBaseDropdownOptions } from '../../../base/browser/ui/dropdown/dropdown.js';8import { ActionListItemKind, IActionListDelegate, IActionListItem } from './actionList.js';9import { ThemeIcon } from '../../../base/common/themables.js';10import { Codicon } from '../../../base/common/codicons.js';11import { getActiveElement, isHTMLElement } from '../../../base/browser/dom.js';12import { IKeybindingService } from '../../keybinding/common/keybinding.js';13import { IListAccessibilityProvider } from '../../../base/browser/ui/list/listWidget.js';1415export interface IActionWidgetDropdownAction extends IAction {16category?: { label: string; order: number };17icon?: ThemeIcon;18description?: string;19}2021// TODO @lramos15 - Should we just make IActionProvider templated?22export interface IActionWidgetDropdownActionProvider {23getActions(): IActionWidgetDropdownAction[];24}2526export interface IActionWidgetDropdownOptions extends IBaseDropdownOptions {27// These are the actions that are shown in the action widget split up by category28readonly actions?: IActionWidgetDropdownAction[];29readonly actionProvider?: IActionWidgetDropdownActionProvider;3031// These actions are those shown at the bottom of the action widget32readonly actionBarActions?: IAction[];33readonly actionBarActionProvider?: IActionProvider;34readonly showItemKeybindings?: boolean;35}3637/**38* Action widget dropdown is a dropdown that uses the action widget under the hood to simulate a native dropdown menu39* The benefits of this include non native features such as headers, descriptions, icons, and button bar40*/41export class ActionWidgetDropdown extends BaseDropdown {42constructor(43container: HTMLElement,44private readonly _options: IActionWidgetDropdownOptions,45@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,46@IKeybindingService private readonly keybindingService: IKeybindingService,47) {48super(container, _options);49}5051override show(): void {52let actionBarActions = this._options.actionBarActions ?? this._options.actionBarActionProvider?.getActions() ?? [];53const actions = this._options.actions ?? this._options.actionProvider?.getActions() ?? [];54const actionWidgetItems: IActionListItem<IActionWidgetDropdownAction>[] = [];5556const actionsByCategory = new Map<string, IActionWidgetDropdownAction[]>();57for (const action of actions) {58let category = action.category;59if (!category) {60category = { label: '', order: Number.MIN_SAFE_INTEGER };61}62if (!actionsByCategory.has(category.label)) {63actionsByCategory.set(category.label, []);64}65actionsByCategory.get(category.label)!.push(action);66}6768// Sort categories by order69const sortedCategories = Array.from(actionsByCategory.entries())70.sort((a, b) => {71const aOrder = a[1][0]?.category?.order ?? Number.MAX_SAFE_INTEGER;72const bOrder = b[1][0]?.category?.order ?? Number.MAX_SAFE_INTEGER;73return aOrder - bOrder;74});7576for (let i = 0; i < sortedCategories.length; i++) {77const [, categoryActions] = sortedCategories[i];7879// Push actions for each category80for (const action of categoryActions) {81actionWidgetItems.push({82item: action,83tooltip: action.tooltip,84description: action.description,85kind: ActionListItemKind.Action,86canPreview: false,87group: { title: '', icon: action.icon ?? ThemeIcon.fromId(action.checked ? Codicon.check.id : Codicon.blank.id) },88disabled: false,89hideIcon: false,90label: action.label,91keybinding: this._options.showItemKeybindings ?92this.keybindingService.lookupKeybinding(action.id) :93undefined,94});95}9697// Add separator at the end of each category except the last one98if (i < sortedCategories.length - 1) {99actionWidgetItems.push({100label: '',101kind: ActionListItemKind.Separator,102canPreview: false,103disabled: false,104hideIcon: false,105});106}107}108109const previouslyFocusedElement = getActiveElement();110111112const actionWidgetDelegate: IActionListDelegate<IActionWidgetDropdownAction> = {113onSelect: (action, preview) => {114this.actionWidgetService.hide();115action.run();116},117onHide: () => {118if (isHTMLElement(previouslyFocusedElement)) {119previouslyFocusedElement.focus();120}121}122};123124actionBarActions = actionBarActions.map(action => ({125...action,126run: async (...args: any[]) => {127this.actionWidgetService.hide();128return action.run(...args);129}130}));131132const accessibilityProvider: Partial<IListAccessibilityProvider<IActionListItem<IActionWidgetDropdownAction>>> = {133isChecked(element) {134return element.kind === ActionListItemKind.Action && !!element?.item?.checked;135},136getRole: (e) => {137switch (e.kind) {138case ActionListItemKind.Action:139return 'menuitemcheckbox';140case ActionListItemKind.Separator:141return 'separator';142default:143return 'separator';144}145},146getWidgetRole: () => 'menu',147};148149this.actionWidgetService.show<IActionWidgetDropdownAction>(150this._options.label ?? '',151false,152actionWidgetItems,153actionWidgetDelegate,154this.element,155undefined,156actionBarActions,157accessibilityProvider158);159}160}161162163