Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IActionWidgetService } from './actionWidget.js';
7
import { IAction } from '../../../base/common/actions.js';
8
import { BaseDropdown, IActionProvider, IBaseDropdownOptions } from '../../../base/browser/ui/dropdown/dropdown.js';
9
import { ActionListItemKind, IActionListDelegate, IActionListItem } from './actionList.js';
10
import { ThemeIcon } from '../../../base/common/themables.js';
11
import { Codicon } from '../../../base/common/codicons.js';
12
import { getActiveElement, isHTMLElement } from '../../../base/browser/dom.js';
13
import { IKeybindingService } from '../../keybinding/common/keybinding.js';
14
import { IListAccessibilityProvider } from '../../../base/browser/ui/list/listWidget.js';
15
16
export interface IActionWidgetDropdownAction extends IAction {
17
category?: { label: string; order: number };
18
icon?: ThemeIcon;
19
description?: string;
20
}
21
22
// TODO @lramos15 - Should we just make IActionProvider templated?
23
export interface IActionWidgetDropdownActionProvider {
24
getActions(): IActionWidgetDropdownAction[];
25
}
26
27
export interface IActionWidgetDropdownOptions extends IBaseDropdownOptions {
28
// These are the actions that are shown in the action widget split up by category
29
readonly actions?: IActionWidgetDropdownAction[];
30
readonly actionProvider?: IActionWidgetDropdownActionProvider;
31
32
// These actions are those shown at the bottom of the action widget
33
readonly actionBarActions?: IAction[];
34
readonly actionBarActionProvider?: IActionProvider;
35
readonly showItemKeybindings?: boolean;
36
}
37
38
/**
39
* Action widget dropdown is a dropdown that uses the action widget under the hood to simulate a native dropdown menu
40
* The benefits of this include non native features such as headers, descriptions, icons, and button bar
41
*/
42
export class ActionWidgetDropdown extends BaseDropdown {
43
constructor(
44
container: HTMLElement,
45
private readonly _options: IActionWidgetDropdownOptions,
46
@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,
47
@IKeybindingService private readonly keybindingService: IKeybindingService,
48
) {
49
super(container, _options);
50
}
51
52
override show(): void {
53
let actionBarActions = this._options.actionBarActions ?? this._options.actionBarActionProvider?.getActions() ?? [];
54
const actions = this._options.actions ?? this._options.actionProvider?.getActions() ?? [];
55
const actionWidgetItems: IActionListItem<IActionWidgetDropdownAction>[] = [];
56
57
const actionsByCategory = new Map<string, IActionWidgetDropdownAction[]>();
58
for (const action of actions) {
59
let category = action.category;
60
if (!category) {
61
category = { label: '', order: Number.MIN_SAFE_INTEGER };
62
}
63
if (!actionsByCategory.has(category.label)) {
64
actionsByCategory.set(category.label, []);
65
}
66
actionsByCategory.get(category.label)!.push(action);
67
}
68
69
// Sort categories by order
70
const sortedCategories = Array.from(actionsByCategory.entries())
71
.sort((a, b) => {
72
const aOrder = a[1][0]?.category?.order ?? Number.MAX_SAFE_INTEGER;
73
const bOrder = b[1][0]?.category?.order ?? Number.MAX_SAFE_INTEGER;
74
return aOrder - bOrder;
75
});
76
77
for (let i = 0; i < sortedCategories.length; i++) {
78
const [, categoryActions] = sortedCategories[i];
79
80
// Push actions for each category
81
for (const action of categoryActions) {
82
actionWidgetItems.push({
83
item: action,
84
tooltip: action.tooltip,
85
description: action.description,
86
kind: ActionListItemKind.Action,
87
canPreview: false,
88
group: { title: '', icon: action.icon ?? ThemeIcon.fromId(action.checked ? Codicon.check.id : Codicon.blank.id) },
89
disabled: false,
90
hideIcon: false,
91
label: action.label,
92
keybinding: this._options.showItemKeybindings ?
93
this.keybindingService.lookupKeybinding(action.id) :
94
undefined,
95
});
96
}
97
98
// Add separator at the end of each category except the last one
99
if (i < sortedCategories.length - 1) {
100
actionWidgetItems.push({
101
label: '',
102
kind: ActionListItemKind.Separator,
103
canPreview: false,
104
disabled: false,
105
hideIcon: false,
106
});
107
}
108
}
109
110
const previouslyFocusedElement = getActiveElement();
111
112
113
const actionWidgetDelegate: IActionListDelegate<IActionWidgetDropdownAction> = {
114
onSelect: (action, preview) => {
115
this.actionWidgetService.hide();
116
action.run();
117
},
118
onHide: () => {
119
if (isHTMLElement(previouslyFocusedElement)) {
120
previouslyFocusedElement.focus();
121
}
122
}
123
};
124
125
actionBarActions = actionBarActions.map(action => ({
126
...action,
127
run: async (...args: any[]) => {
128
this.actionWidgetService.hide();
129
return action.run(...args);
130
}
131
}));
132
133
const accessibilityProvider: Partial<IListAccessibilityProvider<IActionListItem<IActionWidgetDropdownAction>>> = {
134
isChecked(element) {
135
return element.kind === ActionListItemKind.Action && !!element?.item?.checked;
136
},
137
getRole: (e) => {
138
switch (e.kind) {
139
case ActionListItemKind.Action:
140
return 'menuitemcheckbox';
141
case ActionListItemKind.Separator:
142
return 'separator';
143
default:
144
return 'separator';
145
}
146
},
147
getWidgetRole: () => 'menu',
148
};
149
150
this.actionWidgetService.show<IActionWidgetDropdownAction>(
151
this._options.label ?? '',
152
false,
153
actionWidgetItems,
154
actionWidgetDelegate,
155
this.element,
156
undefined,
157
actionBarActions,
158
accessibilityProvider
159
);
160
}
161
}
162
163