Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.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 * as nls from '../../../../nls.js';
7
import { Action, IAction, IActionRunner } from '../../../common/actions.js';
8
import { Codicon } from '../../../common/codicons.js';
9
import { Emitter } from '../../../common/event.js';
10
import { ResolvedKeybinding } from '../../../common/keybindings.js';
11
import { KeyCode } from '../../../common/keyCodes.js';
12
import { IDisposable } from '../../../common/lifecycle.js';
13
import { ThemeIcon } from '../../../common/themables.js';
14
import { IContextMenuProvider } from '../../contextmenu.js';
15
import { $, addDisposableListener, append, EventType, h } from '../../dom.js';
16
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
17
import { IActionViewItemProvider } from '../actionbar/actionbar.js';
18
import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions, IBaseActionViewItemOptions } from '../actionbar/actionViewItems.js';
19
import { AnchorAlignment } from '../contextview/contextview.js';
20
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
21
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
22
import './dropdown.css';
23
import { DropdownMenu, IActionProvider, IDropdownMenuOptions, ILabelRenderer } from './dropdown.js';
24
25
export interface IKeybindingProvider {
26
(action: IAction): ResolvedKeybinding | undefined;
27
}
28
29
export interface IAnchorAlignmentProvider {
30
(): AnchorAlignment;
31
}
32
33
export interface IDropdownMenuActionViewItemOptions extends IBaseActionViewItemOptions {
34
readonly actionViewItemProvider?: IActionViewItemProvider;
35
readonly keybindingProvider?: IKeybindingProvider;
36
readonly actionRunner?: IActionRunner;
37
readonly classNames?: string[] | string;
38
readonly anchorAlignmentProvider?: IAnchorAlignmentProvider;
39
readonly menuAsChild?: boolean;
40
readonly skipTelemetry?: boolean;
41
}
42
43
export class DropdownMenuActionViewItem extends BaseActionViewItem {
44
private menuActionsOrProvider: readonly IAction[] | IActionProvider;
45
private dropdownMenu: DropdownMenu | undefined;
46
private contextMenuProvider: IContextMenuProvider;
47
private actionItem: HTMLElement | null = null;
48
49
private _onDidChangeVisibility = this._register(new Emitter<boolean>());
50
get onDidChangeVisibility() { return this._onDidChangeVisibility.event; }
51
52
protected override readonly options: IDropdownMenuActionViewItemOptions;
53
54
constructor(
55
action: IAction,
56
menuActionsOrProvider: readonly IAction[] | IActionProvider,
57
contextMenuProvider: IContextMenuProvider,
58
options: IDropdownMenuActionViewItemOptions = Object.create(null)
59
) {
60
super(null, action, options);
61
62
this.menuActionsOrProvider = menuActionsOrProvider;
63
this.contextMenuProvider = contextMenuProvider;
64
this.options = options;
65
66
if (this.options.actionRunner) {
67
this.actionRunner = this.options.actionRunner;
68
}
69
}
70
71
override render(container: HTMLElement): void {
72
this.actionItem = container;
73
74
const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => {
75
this.element = append(el, $('a.action-label'));
76
return this.renderLabel(this.element);
77
};
78
79
const isActionsArray = Array.isArray(this.menuActionsOrProvider);
80
const options: IDropdownMenuOptions = {
81
contextMenuProvider: this.contextMenuProvider,
82
labelRenderer: labelRenderer,
83
menuAsChild: this.options.menuAsChild,
84
actions: isActionsArray ? this.menuActionsOrProvider as IAction[] : undefined,
85
actionProvider: isActionsArray ? undefined : this.menuActionsOrProvider as IActionProvider,
86
skipTelemetry: this.options.skipTelemetry
87
};
88
89
this.dropdownMenu = this._register(new DropdownMenu(container, options));
90
this._register(this.dropdownMenu.onDidChangeVisibility(visible => {
91
this.element?.setAttribute('aria-expanded', `${visible}`);
92
this._onDidChangeVisibility.fire(visible);
93
}));
94
95
this.dropdownMenu.menuOptions = {
96
actionViewItemProvider: this.options.actionViewItemProvider,
97
actionRunner: this.actionRunner,
98
getKeyBinding: this.options.keybindingProvider,
99
context: this._context
100
};
101
102
if (this.options.anchorAlignmentProvider) {
103
const that = this;
104
105
this.dropdownMenu.menuOptions = {
106
...this.dropdownMenu.menuOptions,
107
get anchorAlignment(): AnchorAlignment {
108
return that.options.anchorAlignmentProvider!();
109
}
110
};
111
}
112
113
this.updateTooltip();
114
this.updateEnabled();
115
}
116
117
protected renderLabel(element: HTMLElement): IDisposable | null {
118
let classNames: string[] = [];
119
120
if (typeof this.options.classNames === 'string') {
121
classNames = this.options.classNames.split(/\s+/g).filter(s => !!s);
122
} else if (this.options.classNames) {
123
classNames = this.options.classNames;
124
}
125
126
// todo@aeschli: remove codicon, should come through `this.options.classNames`
127
if (!classNames.find(c => c === 'icon')) {
128
classNames.push('codicon');
129
}
130
131
element.classList.add(...classNames);
132
133
if (this._action.label) {
134
this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), element, this._action.label));
135
}
136
137
return null;
138
}
139
140
protected setAriaLabelAttributes(element: HTMLElement): void {
141
element.setAttribute('role', 'button');
142
element.setAttribute('aria-haspopup', 'true');
143
element.setAttribute('aria-expanded', 'false');
144
element.ariaLabel = this._action.label || '';
145
}
146
147
protected override getTooltip(): string | undefined {
148
let title: string | null = null;
149
150
if (this.action.tooltip) {
151
title = this.action.tooltip;
152
} else if (this.action.label) {
153
title = this.action.label;
154
}
155
156
return title ?? undefined;
157
}
158
159
override setActionContext(newContext: unknown): void {
160
super.setActionContext(newContext);
161
162
if (this.dropdownMenu) {
163
if (this.dropdownMenu.menuOptions) {
164
this.dropdownMenu.menuOptions.context = newContext;
165
} else {
166
this.dropdownMenu.menuOptions = { context: newContext };
167
}
168
}
169
}
170
171
show(): void {
172
this.dropdownMenu?.show();
173
}
174
175
protected override updateEnabled(): void {
176
const disabled = !this.action.enabled;
177
this.actionItem?.classList.toggle('disabled', disabled);
178
this.element?.classList.toggle('disabled', disabled);
179
}
180
}
181
182
export interface IActionWithDropdownActionViewItemOptions extends IActionViewItemOptions {
183
readonly menuActionsOrProvider: readonly IAction[] | IActionProvider;
184
readonly menuActionClassNames?: string[];
185
}
186
187
export class ActionWithDropdownActionViewItem extends ActionViewItem {
188
189
protected dropdownMenuActionViewItem: DropdownMenuActionViewItem | undefined;
190
191
constructor(
192
context: unknown,
193
action: IAction,
194
options: IActionWithDropdownActionViewItemOptions,
195
private readonly contextMenuProvider: IContextMenuProvider
196
) {
197
super(context, action, options);
198
}
199
200
override render(container: HTMLElement): void {
201
super.render(container);
202
if (this.element) {
203
this.element.classList.add('action-dropdown-item');
204
const menuActionsProvider = {
205
getActions: () => {
206
const actionsProvider = (<IActionWithDropdownActionViewItemOptions>this.options).menuActionsOrProvider;
207
return Array.isArray(actionsProvider) ? actionsProvider : (actionsProvider as IActionProvider).getActions(); // TODO: microsoft/TypeScript#42768
208
}
209
};
210
211
const menuActionClassNames = (<IActionWithDropdownActionViewItemOptions>this.options).menuActionClassNames || [];
212
const separator = h('div.action-dropdown-item-separator', [h('div', {})]).root;
213
separator.classList.toggle('prominent', menuActionClassNames.includes('prominent'));
214
append(this.element, separator);
215
216
this.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 }));
217
this.dropdownMenuActionViewItem.render(this.element);
218
219
this._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 #164050
221
if (menuActionsProvider.getActions().length === 0) {
222
return;
223
}
224
const event = new StandardKeyboardEvent(e);
225
let handled: boolean = false;
226
if (this.dropdownMenuActionViewItem?.isFocused() && event.equals(KeyCode.LeftArrow)) {
227
handled = true;
228
this.dropdownMenuActionViewItem?.blur();
229
this.focus();
230
} else if (this.isFocused() && event.equals(KeyCode.RightArrow)) {
231
handled = true;
232
this.blur();
233
this.dropdownMenuActionViewItem?.focus();
234
}
235
if (handled) {
236
event.preventDefault();
237
event.stopPropagation();
238
}
239
}));
240
}
241
}
242
243
override blur(): void {
244
super.blur();
245
this.dropdownMenuActionViewItem?.blur();
246
}
247
248
override setFocusable(focusable: boolean): void {
249
super.setFocusable(focusable);
250
this.dropdownMenuActionViewItem?.setFocusable(focusable);
251
}
252
}
253
254