Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/actionbar/actionViewItems.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 { isFirefox } from '../../browser.js';
7
import { DataTransfers } from '../../dnd.js';
8
import { addDisposableListener, EventHelper, EventLike, EventType } from '../../dom.js';
9
import { EventType as TouchEventType, Gesture } from '../../touch.js';
10
import { IActionViewItem } from './actionbar.js';
11
import { IContextViewProvider } from '../contextview/contextview.js';
12
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
13
import { IHoverDelegate } from '../hover/hoverDelegate.js';
14
import { ISelectBoxOptions, ISelectBoxStyles, ISelectOptionItem, SelectBox } from '../selectBox/selectBox.js';
15
import { IToggleStyles } from '../toggle/toggle.js';
16
import { Action, ActionRunner, IAction, IActionChangeEvent, IActionRunner, Separator } from '../../../common/actions.js';
17
import { Disposable } from '../../../common/lifecycle.js';
18
import * as platform from '../../../common/platform.js';
19
import * as types from '../../../common/types.js';
20
import './actionbar.css';
21
import * as nls from '../../../../nls.js';
22
import type { IManagedHover, IManagedHoverContent } from '../hover/hover.js';
23
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
24
25
export interface IBaseActionViewItemOptions {
26
readonly draggable?: boolean;
27
readonly isMenu?: boolean;
28
readonly isTabList?: boolean;
29
readonly useEventAsContext?: boolean;
30
readonly hoverDelegate?: IHoverDelegate;
31
}
32
33
export class BaseActionViewItem extends Disposable implements IActionViewItem {
34
35
element: HTMLElement | undefined;
36
37
_context: unknown;
38
readonly _action: IAction;
39
40
private customHover?: IManagedHover;
41
42
get action() {
43
return this._action;
44
}
45
46
private _actionRunner: IActionRunner | undefined;
47
48
constructor(
49
context: unknown,
50
action: IAction,
51
protected readonly options: IBaseActionViewItemOptions = {}
52
) {
53
super();
54
55
this._context = context || this;
56
this._action = action;
57
58
if (action instanceof Action) {
59
this._register(action.onDidChange(event => {
60
if (!this.element) {
61
// we have not been rendered yet, so there
62
// is no point in updating the UI
63
return;
64
}
65
66
this.handleActionChangeEvent(event);
67
}));
68
}
69
}
70
71
private handleActionChangeEvent(event: IActionChangeEvent): void {
72
if (event.enabled !== undefined) {
73
this.updateEnabled();
74
}
75
76
if (event.checked !== undefined) {
77
this.updateChecked();
78
}
79
80
if (event.class !== undefined) {
81
this.updateClass();
82
}
83
84
if (event.label !== undefined) {
85
this.updateLabel();
86
this.updateTooltip();
87
}
88
89
if (event.tooltip !== undefined) {
90
this.updateTooltip();
91
}
92
}
93
94
get actionRunner(): IActionRunner {
95
if (!this._actionRunner) {
96
this._actionRunner = this._register(new ActionRunner());
97
}
98
99
return this._actionRunner;
100
}
101
102
set actionRunner(actionRunner: IActionRunner) {
103
this._actionRunner = actionRunner;
104
}
105
106
isEnabled(): boolean {
107
return this._action.enabled;
108
}
109
110
setActionContext(newContext: unknown): void {
111
this._context = newContext;
112
}
113
114
render(container: HTMLElement): void {
115
const element = this.element = container;
116
this._register(Gesture.addTarget(container));
117
118
const enableDragging = this.options && this.options.draggable;
119
if (enableDragging) {
120
container.draggable = true;
121
122
if (isFirefox) {
123
// Firefox: requires to set a text data transfer to get going
124
this._register(addDisposableListener(container, EventType.DRAG_START, e => e.dataTransfer?.setData(DataTransfers.TEXT, this._action.label)));
125
}
126
}
127
128
this._register(addDisposableListener(element, TouchEventType.Tap, e => this.onClick(e, true))); // Preserve focus on tap #125470
129
130
this._register(addDisposableListener(element, EventType.MOUSE_DOWN, e => {
131
if (!enableDragging) {
132
EventHelper.stop(e, true); // do not run when dragging is on because that would disable it
133
}
134
135
if (this._action.enabled && e.button === 0) {
136
element.classList.add('active');
137
}
138
}));
139
140
if (platform.isMacintosh) {
141
// macOS: allow to trigger the button when holding Ctrl+key and pressing the
142
// main mouse button. This is for scenarios where e.g. some interaction forces
143
// the Ctrl+key to be pressed and hold but the user still wants to interact
144
// with the actions (for example quick access in quick navigation mode).
145
this._register(addDisposableListener(element, EventType.CONTEXT_MENU, e => {
146
if (e.button === 0 && e.ctrlKey === true) {
147
this.onClick(e);
148
}
149
}));
150
}
151
152
this._register(addDisposableListener(element, EventType.CLICK, e => {
153
EventHelper.stop(e, true);
154
155
// menus do not use the click event
156
if (!(this.options && this.options.isMenu)) {
157
this.onClick(e);
158
}
159
}));
160
161
this._register(addDisposableListener(element, EventType.DBLCLICK, e => {
162
EventHelper.stop(e, true);
163
}));
164
165
[EventType.MOUSE_UP, EventType.MOUSE_OUT].forEach(event => {
166
this._register(addDisposableListener(element, event, e => {
167
EventHelper.stop(e);
168
element.classList.remove('active');
169
}));
170
});
171
}
172
173
onClick(event: EventLike, preserveFocus = false): void {
174
EventHelper.stop(event, true);
175
176
const context = types.isUndefinedOrNull(this._context) ? this.options?.useEventAsContext ? event : { preserveFocus } : this._context;
177
this.actionRunner.run(this._action, context);
178
}
179
180
// Only set the tabIndex on the element once it is about to get focused
181
// That way this element wont be a tab stop when it is not needed #106441
182
focus(): void {
183
if (this.element) {
184
this.element.tabIndex = 0;
185
this.element.focus();
186
this.element.classList.add('focused');
187
}
188
}
189
190
isFocused(): boolean {
191
return !!this.element?.classList.contains('focused');
192
}
193
194
blur(): void {
195
if (this.element) {
196
this.element.blur();
197
this.element.tabIndex = -1;
198
this.element.classList.remove('focused');
199
}
200
}
201
202
setFocusable(focusable: boolean): void {
203
if (this.element) {
204
this.element.tabIndex = focusable ? 0 : -1;
205
}
206
}
207
208
get trapsArrowNavigation(): boolean {
209
return false;
210
}
211
212
protected updateEnabled(): void {
213
// implement in subclass
214
}
215
216
protected updateLabel(): void {
217
// implement in subclass
218
}
219
220
protected getClass(): string | undefined {
221
return this.action.class;
222
}
223
224
protected getTooltip(): string | undefined {
225
return this.action.tooltip;
226
}
227
228
protected getHoverContents(): IManagedHoverContent | undefined {
229
return this.getTooltip();
230
}
231
232
protected updateTooltip(): void {
233
if (!this.element) {
234
return;
235
}
236
const title = this.getHoverContents() ?? '';
237
this.updateAriaLabel();
238
239
if (!this.customHover && title !== '') {
240
const hoverDelegate = this.options.hoverDelegate ?? getDefaultHoverDelegate('element');
241
this.customHover = this._store.add(getBaseLayerHoverDelegate().setupManagedHover(hoverDelegate, this.element, title));
242
} else if (this.customHover) {
243
this.customHover.update(title);
244
}
245
}
246
247
protected updateAriaLabel(): void {
248
if (this.element) {
249
const title = this.getTooltip() ?? '';
250
this.element.setAttribute('aria-label', title);
251
}
252
}
253
254
protected updateClass(): void {
255
// implement in subclass
256
}
257
258
protected updateChecked(): void {
259
// implement in subclass
260
}
261
262
override dispose(): void {
263
if (this.element) {
264
this.element.remove();
265
this.element = undefined;
266
}
267
this._context = undefined;
268
super.dispose();
269
}
270
}
271
272
export interface IActionViewItemOptions extends IBaseActionViewItemOptions {
273
icon?: boolean;
274
label?: boolean;
275
readonly keybinding?: string | null;
276
readonly keybindingNotRenderedWithLabel?: boolean;
277
readonly toggleStyles?: IToggleStyles;
278
}
279
280
export class ActionViewItem extends BaseActionViewItem {
281
282
protected label: HTMLElement | undefined;
283
protected override readonly options: IActionViewItemOptions;
284
285
private cssClass?: string;
286
287
constructor(context: unknown, action: IAction, options: IActionViewItemOptions) {
288
options = {
289
...options,
290
icon: options.icon !== undefined ? options.icon : false,
291
label: options.label !== undefined ? options.label : true,
292
};
293
super(context, action, options);
294
295
this.options = options;
296
this.cssClass = '';
297
}
298
299
override render(container: HTMLElement): void {
300
super.render(container);
301
types.assertType(this.element);
302
303
const label = document.createElement('a');
304
label.classList.add('action-label');
305
label.setAttribute('role', this.getDefaultAriaRole());
306
307
this.label = label;
308
this.element.appendChild(label);
309
310
if (this.options.label && this.options.keybinding && !this.options.keybindingNotRenderedWithLabel) {
311
const kbLabel = document.createElement('span');
312
kbLabel.classList.add('keybinding');
313
kbLabel.textContent = this.options.keybinding;
314
this.element.appendChild(kbLabel);
315
}
316
317
this.updateClass();
318
this.updateLabel();
319
this.updateTooltip();
320
this.updateEnabled();
321
this.updateChecked();
322
}
323
324
private getDefaultAriaRole(): 'presentation' | 'menuitem' | 'tab' | 'button' {
325
if (this._action.id === Separator.ID) {
326
return 'presentation'; // A separator is a presentation item
327
} else {
328
if (this.options.isMenu) {
329
return 'menuitem';
330
} else if (this.options.isTabList) {
331
return 'tab';
332
} else {
333
return 'button';
334
}
335
}
336
}
337
338
// Only set the tabIndex on the element once it is about to get focused
339
// That way this element wont be a tab stop when it is not needed #106441
340
override focus(): void {
341
if (this.label) {
342
this.label.tabIndex = 0;
343
this.label.focus();
344
}
345
}
346
347
override isFocused(): boolean {
348
return !!this.label && this.label?.tabIndex === 0;
349
}
350
351
override blur(): void {
352
if (this.label) {
353
this.label.tabIndex = -1;
354
}
355
}
356
357
override setFocusable(focusable: boolean): void {
358
if (this.label) {
359
this.label.tabIndex = focusable ? 0 : -1;
360
}
361
}
362
363
protected override updateLabel(): void {
364
if (this.options.label && this.label) {
365
this.label.textContent = this.action.label;
366
}
367
}
368
369
protected override getTooltip() {
370
let title: string | null = null;
371
372
if (this.action.tooltip) {
373
title = this.action.tooltip;
374
375
} else if (this.action.label) {
376
title = this.action.label;
377
if (this.options.keybinding) {
378
title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding);
379
}
380
}
381
return title ?? undefined;
382
}
383
384
protected override updateClass(): void {
385
if (this.cssClass && this.label) {
386
this.label.classList.remove(...this.cssClass.split(' '));
387
}
388
if (this.options.icon) {
389
this.cssClass = this.getClass();
390
391
if (this.label) {
392
this.label.classList.add('codicon');
393
if (this.cssClass) {
394
this.label.classList.add(...this.cssClass.split(' '));
395
}
396
}
397
398
this.updateEnabled();
399
} else {
400
this.label?.classList.remove('codicon');
401
}
402
}
403
404
protected override updateEnabled(): void {
405
if (this.action.enabled) {
406
if (this.label) {
407
this.label.removeAttribute('aria-disabled');
408
this.label.classList.remove('disabled');
409
}
410
411
this.element?.classList.remove('disabled');
412
} else {
413
if (this.label) {
414
this.label.setAttribute('aria-disabled', 'true');
415
this.label.classList.add('disabled');
416
}
417
418
this.element?.classList.add('disabled');
419
}
420
}
421
422
protected override updateAriaLabel(): void {
423
if (this.label) {
424
const title = this.getTooltip() ?? '';
425
this.label.setAttribute('aria-label', title);
426
}
427
}
428
429
protected override updateChecked(): void {
430
if (this.label) {
431
if (this.action.checked !== undefined) {
432
this.label.classList.toggle('checked', this.action.checked);
433
if (this.options.isTabList) {
434
this.label.setAttribute('aria-selected', this.action.checked ? 'true' : 'false');
435
} else {
436
this.label.setAttribute('aria-checked', this.action.checked ? 'true' : 'false');
437
this.label.setAttribute('role', 'checkbox');
438
}
439
} else {
440
this.label.classList.remove('checked');
441
this.label.removeAttribute(this.options.isTabList ? 'aria-selected' : 'aria-checked');
442
this.label.setAttribute('role', this.getDefaultAriaRole());
443
}
444
}
445
}
446
}
447
448
export class SelectActionViewItem<T = string> extends BaseActionViewItem {
449
protected selectBox: SelectBox;
450
451
constructor(ctx: unknown, action: IAction, options: ISelectOptionItem[], selected: number, contextViewProvider: IContextViewProvider, styles: ISelectBoxStyles, selectBoxOptions?: ISelectBoxOptions) {
452
super(ctx, action);
453
454
this.selectBox = new SelectBox(options, selected, contextViewProvider, styles, selectBoxOptions);
455
this.selectBox.setFocusable(false);
456
457
this._register(this.selectBox);
458
this.registerListeners();
459
}
460
461
setOptions(options: ISelectOptionItem[], selected?: number): void {
462
this.selectBox.setOptions(options, selected);
463
}
464
465
select(index: number): void {
466
this.selectBox.select(index);
467
}
468
469
private registerListeners(): void {
470
this._register(this.selectBox.onDidSelect(e => this.runAction(e.selected, e.index)));
471
}
472
473
protected runAction(option: string, index: number): void {
474
this.actionRunner.run(this._action, this.getActionContext(option, index));
475
}
476
477
protected getActionContext(option: string, index: number): T | string {
478
return option;
479
}
480
481
override setFocusable(focusable: boolean): void {
482
this.selectBox.setFocusable(focusable);
483
}
484
485
override focus(): void {
486
this.selectBox?.focus();
487
}
488
489
override blur(): void {
490
this.selectBox?.blur();
491
}
492
493
override render(container: HTMLElement): void {
494
this.selectBox.render(container);
495
}
496
}
497
498