Skip to content

Commit 1a61ccb

Browse files
lramos15justschen
andauthored
Add support for separators in the action widget (microsoft#257144)
* Add support for separators in the action widget * add a little padding, no more importants * there were two more importants that we didn't need there because we made the specificity more specific --------- Co-authored-by: Justin Chen <54879025+justschen@users.noreply.github.com> Co-authored-by: Your Name <justchen@microsoft.com>
1 parent 3547642 commit 1a61ccb

File tree

3 files changed

+102
-19
lines changed

3 files changed

+102
-19
lines changed

src/vs/platform/actionWidget/browser/actionList.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ interface IActionMenuTemplateData {
5353

5454
export const enum ActionListItemKind {
5555
Action = 'action',
56-
Header = 'header'
56+
Header = 'header',
57+
Separator = 'separator'
5758
}
5859

5960
interface IHeaderTemplateData {
@@ -83,6 +84,33 @@ class HeaderRenderer<T> implements IListRenderer<IActionListItem<T>, IHeaderTemp
8384
}
8485
}
8586

87+
interface ISeparatorTemplateData {
88+
readonly container: HTMLElement;
89+
readonly text: HTMLElement;
90+
}
91+
92+
class SeparatorRenderer<T> implements IListRenderer<IActionListItem<T>, ISeparatorTemplateData> {
93+
94+
get templateId(): string { return ActionListItemKind.Separator; }
95+
96+
renderTemplate(container: HTMLElement): ISeparatorTemplateData {
97+
container.classList.add('separator');
98+
99+
const text = document.createElement('span');
100+
container.append(text);
101+
102+
return { container, text };
103+
}
104+
105+
renderElement(element: IActionListItem<T>, _index: number, templateData: ISeparatorTemplateData): void {
106+
templateData.text.textContent = element.label ?? '';
107+
}
108+
109+
disposeTemplate(_templateData: ISeparatorTemplateData): void {
110+
// noop
111+
}
112+
}
113+
86114
class ActionItemRenderer<T> implements IListRenderer<IActionListItem<T>, IActionMenuTemplateData> {
87115

88116
get templateId(): string { return ActionListItemKind.Action; }
@@ -176,7 +204,7 @@ class PreviewSelectedEvent extends UIEvent {
176204
}
177205

178206
function getKeyboardNavigationLabel<T>(item: IActionListItem<T>): string | undefined {
179-
// Filter out header vs. action
207+
// Filter out header vs. action vs. separator
180208
if (item.kind === 'action') {
181209
return item.label;
182210
}
@@ -191,6 +219,7 @@ export class ActionList<T> extends Disposable {
191219

192220
private readonly _actionLineHeight = 24;
193221
private readonly _headerLineHeight = 26;
222+
private readonly _separatorLineHeight = 8;
194223

195224
private readonly _allMenuItems: readonly IActionListItem<T>[];
196225

@@ -210,14 +239,24 @@ export class ActionList<T> extends Disposable {
210239
this.domNode = document.createElement('div');
211240
this.domNode.classList.add('actionList');
212241
const virtualDelegate: IListVirtualDelegate<IActionListItem<T>> = {
213-
getHeight: element => element.kind === ActionListItemKind.Header ? this._headerLineHeight : this._actionLineHeight,
242+
getHeight: element => {
243+
switch (element.kind) {
244+
case ActionListItemKind.Header:
245+
return this._headerLineHeight;
246+
case ActionListItemKind.Separator:
247+
return this._separatorLineHeight;
248+
default:
249+
return this._actionLineHeight;
250+
}
251+
},
214252
getTemplateId: element => element.kind
215253
};
216254

217255

218256
this._list = this._register(new List(user, this.domNode, virtualDelegate, [
219257
new ActionItemRenderer<IActionListItem<T>>(preview, this._keybindingService),
220258
new HeaderRenderer(),
259+
new SeparatorRenderer(),
221260
], {
222261
keyboardSupport: false,
223262
typeNavigationEnabled: true,
@@ -234,7 +273,16 @@ export class ActionList<T> extends Disposable {
234273
return null;
235274
},
236275
getWidgetAriaLabel: () => localize({ key: 'customQuickFixWidget', comment: [`An action widget option`] }, "Action Widget"),
237-
getRole: (e) => e.kind === ActionListItemKind.Action ? 'option' : 'separator',
276+
getRole: (e) => {
277+
switch (e.kind) {
278+
case ActionListItemKind.Action:
279+
return 'option';
280+
case ActionListItemKind.Separator:
281+
return 'separator';
282+
default:
283+
return 'separator';
284+
}
285+
},
238286
getWidgetRole: () => 'listbox',
239287
...accessibilityProvider
240288
},
@@ -268,9 +316,11 @@ export class ActionList<T> extends Disposable {
268316
layout(minWidth: number): number {
269317
// Updating list height, depending on how many separators and headers there are.
270318
const numHeaders = this._allMenuItems.filter(item => item.kind === 'header').length;
319+
const numSeparators = this._allMenuItems.filter(item => item.kind === 'separator').length;
271320
const itemsHeight = this._allMenuItems.length * this._actionLineHeight;
272321
const heightWithHeaders = itemsHeight + numHeaders * this._headerLineHeight - numHeaders * this._actionLineHeight;
273-
this._list.layout(heightWithHeaders);
322+
const heightWithSeparators = heightWithHeaders + numSeparators * this._separatorLineHeight - numSeparators * this._actionLineHeight;
323+
this._list.layout(heightWithSeparators);
274324
let maxWidth = minWidth;
275325

276326
if (this._allMenuItems.length >= 50) {
@@ -293,7 +343,7 @@ export class ActionList<T> extends Disposable {
293343
}
294344

295345
const maxVhPrecentage = 0.7;
296-
const height = Math.min(heightWithHeaders, this._layoutService.getContainer(dom.getWindow(this.domNode)).clientHeight * maxVhPrecentage);
346+
const height = Math.min(heightWithSeparators, this._layoutService.getContainer(dom.getWindow(this.domNode)).clientHeight * maxVhPrecentage);
297347
this._list.layout(height, maxWidth);
298348

299349
this.domNode.style.height = `${height}px`;

src/vs/platform/actionWidget/browser/actionWidget.css

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656

5757
/** Styles for each row in the list element **/
5858
.action-widget .monaco-list .monaco-list-row {
59-
padding: 0 10px;
59+
padding: 0 0 0 8px;
6060
white-space: nowrap;
6161
cursor: pointer;
6262
touch-action: none;
@@ -81,6 +81,28 @@
8181
margin-top: 2px;
8282
}
8383

84+
.action-widget .monaco-scrollable-element .monaco-list-rows .monaco-list-row.separator {
85+
border-top: 1px solid var(--vscode-editorHoverWidget-border);
86+
color: var(--vscode-descriptionForeground);
87+
font-size: 12px;
88+
padding: 0;
89+
margin: 4px 0 0 0;
90+
cursor: default;
91+
user-select: none;
92+
border-radius: 0;
93+
}
94+
95+
.action-widget .monaco-scrollable-element .monaco-list-rows .monaco-list-row.separator.focused {
96+
outline: 0 solid;
97+
background-color: transparent;
98+
border-radius: 0;
99+
}
100+
101+
.action-widget .monaco-list-row.separator:first-of-type {
102+
border-top: none;
103+
margin-top: 0;
104+
}
105+
84106
.action-widget .monaco-list .group-header,
85107
.action-widget .monaco-list .option-disabled,
86108
.action-widget .monaco-list .option-disabled:before,

src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,9 @@ export class ActionWidgetDropdown extends BaseDropdown {
7373
return aOrder - bOrder;
7474
});
7575

76-
for (const [categoryLabel, categoryActions] of sortedCategories) {
76+
for (let i = 0; i < sortedCategories.length; i++) {
77+
const [, categoryActions] = sortedCategories[i];
7778

78-
if (categoryLabel !== '') {
79-
// Push headers for each category
80-
actionWidgetItems.push({
81-
label: categoryLabel,
82-
kind: ActionListItemKind.Header,
83-
canPreview: false,
84-
disabled: false,
85-
hideIcon: false,
86-
});
87-
}
8879
// Push actions for each category
8980
for (const action of categoryActions) {
9081
actionWidgetItems.push({
@@ -102,6 +93,17 @@ export class ActionWidgetDropdown extends BaseDropdown {
10293
undefined,
10394
});
10495
}
96+
97+
// Add separator at the end of each category except the last one
98+
if (i < sortedCategories.length - 1) {
99+
actionWidgetItems.push({
100+
label: '',
101+
kind: ActionListItemKind.Separator,
102+
canPreview: false,
103+
disabled: false,
104+
hideIcon: false,
105+
});
106+
}
105107
}
106108

107109
const previouslyFocusedElement = getActiveElement();
@@ -131,7 +133,16 @@ export class ActionWidgetDropdown extends BaseDropdown {
131133
isChecked(element) {
132134
return element.kind === ActionListItemKind.Action && !!element?.item?.checked;
133135
},
134-
getRole: (e) => e.kind === ActionListItemKind.Action ? 'menuitemcheckbox' : 'separator',
136+
getRole: (e) => {
137+
switch (e.kind) {
138+
case ActionListItemKind.Action:
139+
return 'menuitemcheckbox';
140+
case ActionListItemKind.Separator:
141+
return 'separator';
142+
default:
143+
return 'separator';
144+
}
145+
},
135146
getWidgetRole: () => 'menu',
136147
};
137148

0 commit comments

Comments
 (0)