Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts
5220 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, IActionListItemHover } 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
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
16
17
export interface IActionWidgetDropdownAction extends IAction {
18
category?: { label: string; order: number; showHeader?: boolean };
19
icon?: ThemeIcon;
20
description?: string;
21
/**
22
* Optional flyout hover configuration shown when focusing/hovering over the action.
23
*/
24
hover?: IActionListItemHover;
25
/**
26
* Optional toolbar actions shown when the item is focused or hovered.
27
*/
28
toolbarActions?: IAction[];
29
}
30
31
// TODO @lramos15 - Should we just make IActionProvider templated?
32
export interface IActionWidgetDropdownActionProvider {
33
getActions(): IActionWidgetDropdownAction[];
34
}
35
36
export interface IActionWidgetDropdownOptions extends IBaseDropdownOptions {
37
// These are the actions that are shown in the action widget split up by category
38
readonly actions?: IActionWidgetDropdownAction[];
39
readonly actionProvider?: IActionWidgetDropdownActionProvider;
40
41
// These actions are those shown at the bottom of the action widget
42
readonly actionBarActions?: IAction[];
43
readonly actionBarActionProvider?: IActionProvider;
44
readonly showItemKeybindings?: boolean;
45
46
// Function that returns the anchor element for the dropdown
47
getAnchor?: () => HTMLElement;
48
49
/**
50
* Telemetry reporter configuration used when the dropdown closes. The `id` field is required
51
* and is used as the telemetry identifier; `name` is optional additional context. If not
52
* provided, no telemetry will be sent.
53
*/
54
readonly reporter?: { id: string; name?: string; includeOptions?: boolean };
55
}
56
57
/**
58
* Action widget dropdown is a dropdown that uses the action widget under the hood to simulate a native dropdown menu
59
* The benefits of this include non native features such as headers, descriptions, icons, and button bar
60
*/
61
export class ActionWidgetDropdown extends BaseDropdown {
62
63
private _enabled: boolean = true;
64
65
constructor(
66
container: HTMLElement,
67
private readonly _options: IActionWidgetDropdownOptions,
68
@IActionWidgetService private readonly actionWidgetService: IActionWidgetService,
69
@IKeybindingService private readonly keybindingService: IKeybindingService,
70
@ITelemetryService private readonly telemetryService: ITelemetryService,
71
) {
72
super(container, _options);
73
}
74
75
override show(): void {
76
if (!this._enabled) {
77
return;
78
}
79
80
let actionBarActions = this._options.actionBarActions ?? this._options.actionBarActionProvider?.getActions() ?? [];
81
const actions = this._options.actions ?? this._options.actionProvider?.getActions() ?? [];
82
83
// Track the currently selected option before opening
84
const optionBeforeOpen: IActionWidgetDropdownAction | undefined = actions.find(a => a.checked);
85
let selectedOption: IActionWidgetDropdownAction | undefined = optionBeforeOpen;
86
87
const actionWidgetItems: IActionListItem<IActionWidgetDropdownAction>[] = [];
88
89
const actionsByCategory = new Map<string, IActionWidgetDropdownAction[]>();
90
for (const action of actions) {
91
let category = action.category;
92
if (!category) {
93
category = { label: '', order: Number.MIN_SAFE_INTEGER };
94
}
95
if (!actionsByCategory.has(category.label)) {
96
actionsByCategory.set(category.label, []);
97
}
98
actionsByCategory.get(category.label)!.push(action);
99
}
100
101
// Sort categories by order
102
const sortedCategories = Array.from(actionsByCategory.entries())
103
.sort((a, b) => {
104
const aOrder = a[1][0]?.category?.order ?? Number.MAX_SAFE_INTEGER;
105
const bOrder = b[1][0]?.category?.order ?? Number.MAX_SAFE_INTEGER;
106
return aOrder - bOrder;
107
});
108
109
for (let i = 0; i < sortedCategories.length; i++) {
110
const [categoryLabel, categoryActions] = sortedCategories[i];
111
const showHeader = categoryActions[0]?.category?.showHeader ?? false;
112
if (showHeader && categoryLabel) {
113
actionWidgetItems.push({
114
kind: ActionListItemKind.Header,
115
label: categoryLabel,
116
canPreview: false,
117
disabled: false,
118
hideIcon: false,
119
});
120
}
121
122
// Push actions for each category
123
for (const action of categoryActions) {
124
actionWidgetItems.push({
125
item: action,
126
tooltip: action.tooltip,
127
description: action.description,
128
hover: action.hover,
129
toolbarActions: action.toolbarActions,
130
kind: ActionListItemKind.Action,
131
canPreview: false,
132
group: { title: '', icon: action.icon ?? ThemeIcon.fromId(action.checked ? Codicon.check.id : Codicon.blank.id) },
133
disabled: !action.enabled,
134
hideIcon: false,
135
label: action.label,
136
keybinding: this._options.showItemKeybindings ?
137
this.keybindingService.lookupKeybinding(action.id) :
138
undefined,
139
});
140
}
141
142
// Add separator after each category except the last one
143
if (i < sortedCategories.length - 1) {
144
actionWidgetItems.push({
145
label: '',
146
kind: ActionListItemKind.Separator,
147
canPreview: false,
148
disabled: false,
149
hideIcon: false,
150
});
151
}
152
}
153
154
const previouslyFocusedElement = getActiveElement();
155
156
157
const actionWidgetDelegate: IActionListDelegate<IActionWidgetDropdownAction> = {
158
onSelect: (action, preview) => {
159
selectedOption = action;
160
this.actionWidgetService.hide();
161
action.run();
162
},
163
onHide: () => {
164
if (isHTMLElement(previouslyFocusedElement)) {
165
previouslyFocusedElement.focus();
166
}
167
this._emitCloseEvent(optionBeforeOpen, selectedOption);
168
}
169
};
170
171
actionBarActions = actionBarActions.map(action => ({
172
...action,
173
run: async (...args: unknown[]) => {
174
this.actionWidgetService.hide();
175
return action.run(...args);
176
}
177
}));
178
179
const accessibilityProvider: Partial<IListAccessibilityProvider<IActionListItem<IActionWidgetDropdownAction>>> = {
180
isChecked(element) {
181
return element.kind === ActionListItemKind.Action && !!element?.item?.checked;
182
},
183
getRole: (e) => {
184
switch (e.kind) {
185
case ActionListItemKind.Action:
186
return 'menuitemcheckbox';
187
case ActionListItemKind.Separator:
188
return 'separator';
189
default:
190
return 'separator';
191
}
192
},
193
getWidgetRole: () => 'menu',
194
};
195
196
this.actionWidgetService.show<IActionWidgetDropdownAction>(
197
this._options.label ?? '',
198
false,
199
actionWidgetItems,
200
actionWidgetDelegate,
201
this._options.getAnchor?.() ?? this.element,
202
undefined,
203
actionBarActions,
204
accessibilityProvider
205
);
206
}
207
208
setEnabled(enabled: boolean): void {
209
this._enabled = enabled;
210
}
211
212
private _emitCloseEvent(optionBeforeOpen: IActionWidgetDropdownAction | undefined, selectedOption: IActionWidgetDropdownAction | undefined): void {
213
const optionBefore = optionBeforeOpen;
214
const optionAfter = selectedOption;
215
216
if (this._options.reporter) {
217
this.telemetryService.publicLog2<ActionWidgetDropdownClosedEvent, ActionWidgetDropdownClosedClassification>(
218
'actionWidgetDropdownClosed',
219
{
220
id: this._options.reporter.id,
221
name: this._options.reporter.name,
222
selectionChanged: optionBefore?.id !== optionAfter?.id,
223
optionIdBefore: this._options.reporter.includeOptions ? optionBefore?.id : undefined,
224
optionIdAfter: this._options.reporter.includeOptions ? optionAfter?.id : undefined,
225
optionLabelBefore: this._options.reporter.includeOptions ? optionBefore?.label : undefined,
226
optionLabelAfter: this._options.reporter.includeOptions ? optionAfter?.label : undefined,
227
}
228
);
229
}
230
}
231
}
232
233
type ActionWidgetDropdownClosedEvent = {
234
id: string;
235
name: string | undefined;
236
selectionChanged: boolean;
237
optionIdBefore: string | undefined;
238
optionIdAfter: string | undefined;
239
optionLabelBefore: string | undefined;
240
optionLabelAfter: string | undefined;
241
};
242
243
type ActionWidgetDropdownClosedClassification = {
244
id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The telemetry id of the dropdown picker.' };
245
name: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The telemetry name of the dropdown picker.' };
246
selectionChanged: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the user changed the selected option.' };
247
optionIdBefore: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The option configured before opening the dropdown.' };
248
optionIdAfter: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The option configured after closing the dropdown.' };
249
optionLabelBefore: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the option configured before opening the dropdown.' };
250
optionLabelAfter: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The label of the option configured after closing the dropdown.' };
251
owner: 'benibenj';
252
comment: 'Tracks action widget dropdown usage and selection changes.';
253
};
254
255