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
5262 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
this.setAriaLabelAttributes(this.element);
77
return this.renderLabel(this.element);
78
};
79
80
const isActionsArray = Array.isArray(this.menuActionsOrProvider);
81
const options: IDropdownMenuOptions = {
82
contextMenuProvider: this.contextMenuProvider,
83
labelRenderer: labelRenderer,
84
menuAsChild: this.options.menuAsChild,
85
actions: isActionsArray ? this.menuActionsOrProvider as IAction[] : undefined,
86
actionProvider: isActionsArray ? undefined : this.menuActionsOrProvider as IActionProvider,
87
skipTelemetry: this.options.skipTelemetry
88
};
89
90
this.dropdownMenu = this._register(new DropdownMenu(container, options));
91
this._register(this.dropdownMenu.onDidChangeVisibility(visible => {
92
this.element?.setAttribute('aria-expanded', `${visible}`);
93
this._onDidChangeVisibility.fire(visible);
94
}));
95
96
this.dropdownMenu.menuOptions = {
97
actionViewItemProvider: this.options.actionViewItemProvider,
98
actionRunner: this.actionRunner,
99
getKeyBinding: this.options.keybindingProvider,
100
context: this._context
101
};
102
103
if (this.options.anchorAlignmentProvider) {
104
const that = this;
105
106
this.dropdownMenu.menuOptions = {
107
...this.dropdownMenu.menuOptions,
108
get anchorAlignment(): AnchorAlignment {
109
return that.options.anchorAlignmentProvider!();
110
}
111
};
112
}
113
114
this.updateTooltip();
115
this.updateEnabled();
116
}
117
118
protected renderLabel(element: HTMLElement): IDisposable | null {
119
let classNames: string[] = [];
120
121
if (typeof this.options.classNames === 'string') {
122
classNames = this.options.classNames.split(/\s+/g).filter(s => !!s);
123
} else if (this.options.classNames) {
124
classNames = this.options.classNames;
125
}
126
127
// todo@aeschli: remove codicon, should come through `this.options.classNames`
128
if (!classNames.find(c => c === 'icon')) {
129
classNames.push('codicon');
130
}
131
132
element.classList.add(...classNames);
133
134
if (this._action.label) {
135
this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), element, this._action.label));
136
}
137
138
return null;
139
}
140
141
protected setAriaLabelAttributes(element: HTMLElement): void {
142
element.setAttribute('role', 'button');
143
element.setAttribute('aria-haspopup', 'true');
144
element.setAttribute('aria-expanded', 'false');
145
element.ariaLabel = this._action.label || '';
146
}
147
148
protected override getTooltip(): string | undefined {
149
let title: string | null = null;
150
151
if (this.action.tooltip) {
152
title = this.action.tooltip;
153
} else if (this.action.label) {
154
title = this.action.label;
155
}
156
157
return title ?? undefined;
158
}
159
160
override setActionContext(newContext: unknown): void {
161
super.setActionContext(newContext);
162
163
if (this.dropdownMenu) {
164
if (this.dropdownMenu.menuOptions) {
165
this.dropdownMenu.menuOptions.context = newContext;
166
} else {
167
this.dropdownMenu.menuOptions = { context: newContext };
168
}
169
}
170
}
171
172
show(): void {
173
this.dropdownMenu?.show();
174
}
175
176
protected override updateEnabled(): void {
177
const disabled = !this.action.enabled;
178
this.actionItem?.classList.toggle('disabled', disabled);
179
this.element?.classList.toggle('disabled', disabled);
180
}
181
}
182
183
export interface IActionWithDropdownActionViewItemOptions extends IActionViewItemOptions {
184
readonly menuActionsOrProvider: readonly IAction[] | IActionProvider;
185
readonly menuActionClassNames?: string[];
186
}
187
188
export class ActionWithDropdownActionViewItem extends ActionViewItem {
189
190
protected dropdownMenuActionViewItem: DropdownMenuActionViewItem | undefined;
191
192
constructor(
193
context: unknown,
194
action: IAction,
195
options: IActionWithDropdownActionViewItemOptions,
196
private readonly contextMenuProvider: IContextMenuProvider
197
) {
198
super(context, action, options);
199
}
200
201
override render(container: HTMLElement): void {
202
super.render(container);
203
if (this.element) {
204
this.element.classList.add('action-dropdown-item');
205
const menuActionsProvider = {
206
getActions: () => {
207
const actionsProvider = (<IActionWithDropdownActionViewItemOptions>this.options).menuActionsOrProvider;
208
return Array.isArray(actionsProvider) ? actionsProvider : (actionsProvider as IActionProvider).getActions(); // TODO: microsoft/TypeScript#42768
209
}
210
};
211
212
const menuActionClassNames = (<IActionWithDropdownActionViewItemOptions>this.options).menuActionClassNames || [];
213
const separator = h('div.action-dropdown-item-separator', [h('div', {})]).root;
214
separator.classList.toggle('prominent', menuActionClassNames.includes('prominent'));
215
append(this.element, separator);
216
217
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 }));
218
this.dropdownMenuActionViewItem.render(this.element);
219
220
this._register(addDisposableListener(this.element, EventType.KEY_DOWN, e => {
221
// If we don't have any actions then the dropdown is hidden so don't try to focus it #164050
222
if (menuActionsProvider.getActions().length === 0) {
223
return;
224
}
225
const event = new StandardKeyboardEvent(e);
226
let handled: boolean = false;
227
if (this.dropdownMenuActionViewItem?.isFocused() && event.equals(KeyCode.LeftArrow)) {
228
handled = true;
229
this.dropdownMenuActionViewItem?.blur();
230
this.focus();
231
} else if (this.isFocused() && event.equals(KeyCode.RightArrow)) {
232
handled = true;
233
this.blur();
234
this.dropdownMenuActionViewItem?.focus();
235
}
236
if (handled) {
237
event.preventDefault();
238
event.stopPropagation();
239
}
240
}));
241
}
242
}
243
244
override blur(): void {
245
super.blur();
246
this.dropdownMenuActionViewItem?.blur();
247
}
248
249
override setFocusable(focusable: boolean): void {
250
super.setFocusable(focusable);
251
this.dropdownMenuActionViewItem?.setFocusable(focusable);
252
}
253
}
254
255