Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/vs/workbench/browser/media/floatingPanels.css
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,9 @@

/* Separate the activity bar items so they read as distinct floating targets. The gap is
* published by activitybarPart.ts as `--activity-bar-action-gap` (0px at the compact size)
* so that it stays in step with the overflow computation. */
.monaco-workbench.floating-panels .part.activitybar > .content .monaco-action-bar .action-item + .action-item {
* so that it stays in step with the overflow computation. Nested menus keep their own
* row and separator spacing. */
.monaco-workbench.floating-panels .part.activitybar > .content :not(.monaco-menu) > .monaco-action-bar > .actions-container > .action-item + .action-item {
margin-top: var(--activity-bar-action-gap, 0px);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
*--------------------------------------------------------------------------------------------*/

import assert from 'assert';
import { getWindow } from '../../../../../base/browser/dom.js';
import { getWindow, scheduleAtNextAnimationFrame } from '../../../../../base/browser/dom.js';
import { ActionBar, ActionsOrientation } from '../../../../../base/browser/ui/actionbar/actionbar.js';
import { HorizontalDirection, Menu, unthemedMenuStyles, VerticalDirection } from '../../../../../base/browser/ui/menu/menu.js';
import { MenuBar } from '../../../../../base/browser/ui/menu/menubar.js';
import { Orientation } from '../../../../../base/browser/ui/sash/sash.js';
import { Pane } from '../../../../../base/browser/ui/splitview/paneview.js';
import { DeferredPromise } from '../../../../../base/common/async.js';
import { Action, Separator } from '../../../../../base/common/actions.js';
import { Color } from '../../../../../base/common/color.js';
import { Emitter } from '../../../../../base/common/event.js';
import { DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
import { combinedDisposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { isIMenuItem, isISubmenuItem, MenuId, MenuRegistry } from '../../../../../platform/actions/common/actions.js';
import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
Expand Down Expand Up @@ -1019,6 +1023,92 @@ suite('ModernUIContribution', () => {
});
});

for (const { name, classes, gap } of [
{ name: 'default density', classes: 'modern-ui floating-panels', gap: 8 },
{ name: 'compact density', classes: 'modern-ui floating-panels modern-ui-compact', gap: 4 },
{ name: 'compact activity bar', classes: 'modern-ui floating-panels activitybar-compact', gap: 0 },
{ name: 'classic layout', classes: '', gap: 0 },
]) {
test(`keeps compact application menu spacing independent of the activity rail in ${name}`, async () => {
const root = document.createElement('div');
root.className = `monaco-workbench ${classes}`;
root.style.height = '400px';
root.style.setProperty('--activity-bar-width', '36px');
root.style.setProperty('--activity-bar-action-height', '36px');
root.style.setProperty('--activity-bar-action-gap', `${gap}px`);
root.style.setProperty('--vscode-spacing-sizeNone', '0px');
root.style.setProperty('--vscode-spacing-size20', '2px');
root.style.setProperty('--vscode-spacing-size40', '4px');
root.style.setProperty('--vscode-spacing-size60', '6px');
root.style.setProperty('--vscode-spacing-size80', '8px');
document.body.appendChild(root);
const disposables = new DisposableStore();
store.add(combinedDisposable(disposables, toDisposable(() => root.remove())));

const activityBar = appendElement(root, `part activitybar left${name === 'compact activity bar' ? ' compact' : ''}`);
const content = appendElement(activityBar, 'content');
const menubar = appendElement(content, 'menubar');
const rail = disposables.add(new ActionBar(appendElement(content, 'composite-bar'), { orientation: ActionsOrientation.VERTICAL }));
rail.push([
disposables.add(new Action('explorer', 'Explorer')),
disposables.add(new Action('search', 'Search')),
]);
const actions = [
disposables.add(new Action('undo', 'Undo')),
disposables.add(new Action('redo', 'Redo')),
new Separator(),
disposables.add(new Action('cut', 'Cut')),
disposables.add(new Action('copy', 'Copy')),
];
const menuBar = new MenuBar(menubar, {
visibility: 'compact',
compactMode: { horizontal: HorizontalDirection.Right, vertical: VerticalDirection.Below },
}, unthemedMenuStyles);
disposables.add(combinedDisposable(toDisposable(() => menuBar.blur()), menuBar));
menuBar.push([
{ label: 'Edit', actions },
{ label: 'View', actions: [] },
{ label: 'Help', actions: [] },
]);
menuBar.update();
await new Promise<void>(resolve => disposables.add(scheduleAtNextAnimationFrame(getWindow(root), () => resolve())));
menuBar.toggleFocus();
menubar.querySelector<HTMLElement>('.menubar-menu-button')!.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', keyCode: 13, bubbles: true }));

const applicationMenu = menubar.querySelector<HTMLElement>('.monaco-menu');
assert.ok(applicationMenu);
applicationMenu.querySelector<HTMLElement>('.action-menu-item')!.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight', keyCode: 39, bubbles: true }));
const submenu = applicationMenu.querySelector<HTMLElement>('.monaco-submenu .monaco-menu');
assert.ok(submenu);

const referenceHost = appendElement(root, 'reference-menu');
disposables.add(new Menu(referenceHost, actions, {}, unthemedMenuStyles));
const referenceMenu = referenceHost.querySelector<HTMLElement>('.monaco-menu')!;
const menuGeometry = (menu: HTMLElement) => {
const items = [...menu.querySelector('.actions-container')!.children];
const top = items[0].getBoundingClientRect().top;
return items.map(item => ({
height: item.getBoundingClientRect().height,
offset: item.getBoundingClientRect().top - top,
marginTop: getWindow(menu).getComputedStyle(item).marginTop,
}));
};
const applicationRows = menuGeometry(applicationMenu);
const railItems = content.querySelectorAll<HTMLElement>('.composite-bar .action-item');
assert.deepStrictEqual({
railGap: getWindow(root).getComputedStyle(railItems[1]).marginTop,
applicationMargins: applicationRows.map(row => row.marginTop),
applicationRowStep: applicationRows[1].offset,
submenu: menuGeometry(submenu),
}, {
railGap: `${gap}px`,
applicationMargins: ['0px', '0px', '0px'],
applicationRowStep: 24,
submenu: menuGeometry(referenceMenu),
});
});
}

test('styles focused and open compact application menu states', () => {
const root = document.createElement('div');
root.className = 'monaco-workbench modern-ui floating-panels';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as DOM from '../../../../base/browser/dom.js';
import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js';
import { HorizontalDirection, VerticalDirection } from '../../../../base/browser/ui/menu/menu.js';
import { MenuBar } from '../../../../base/browser/ui/menu/menubar.js';
import { Action, Separator } from '../../../../base/common/actions.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { combinedDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { defaultMenuStyles } from '../../../../platform/theme/browser/defaultStyles.js';
import { ActivitybarPart } from '../../../browser/parts/activitybar/activitybarPart.js';
import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js';

import '../../../browser/media/floatingPanels.css';
import '../../../browser/parts/activitybar/media/activitybarpart.css';
import '../../../browser/parts/activitybar/media/activityaction.css';
import '../../../browser/parts/titlebar/media/menubarControl.css';
import '../../../contrib/modernUI/browser/media/activityBar.css';
import '../../../contrib/modernUI/browser/media/roundedCorners.css';

function renderActivityBarMenu({ container, disposableStore }: ComponentFixtureContext, compact: boolean): void {
container.style.width = '560px';
container.style.height = '360px';
const root = DOM.append(container, DOM.$('.monaco-workbench.modern-ui.floating-panels'));
root.classList.toggle('modern-ui-compact', compact);
root.style.width = '100%';
root.style.height = '100%';
root.style.position = 'relative';

const activityBar = DOM.append(root, DOM.$('.part.activitybar.left'));
activityBar.style.setProperty('--activity-bar-width', `${ActivitybarPart.FLOATING_ACTIVITYBAR_WIDTH}px`);
activityBar.style.setProperty('--activity-bar-action-height', `${ActivitybarPart.FLOATING_ACTION_HEIGHT}px`);
activityBar.style.setProperty('--activity-bar-action-gap', `${compact ? ActivitybarPart.FLOATING_COMPACT_ACTION_GAP : ActivitybarPart.FLOATING_ACTION_GAP}px`);
activityBar.style.backgroundColor = 'var(--vscode-modernActivityBar-background)';
const content = DOM.append(activityBar, DOM.$('.content'));
const menubar = DOM.append(content, DOM.$('.menubar'));
const rail = disposableStore.add(new ActionBar(DOM.append(content, DOM.$('.composite-bar')), {
orientation: ActionsOrientation.VERTICAL,
ariaLabel: 'Activity Bar',
}));
rail.push([
disposableStore.add(new Action('explorer', 'Explorer', ThemeIcon.asClassName(Codicon.files))),
disposableStore.add(new Action('search', 'Search', ThemeIcon.asClassName(Codicon.search))),
], { icon: true, label: false });

const action = (id: string, label: string) => disposableStore.add(new Action(id, label));
const menuOptions = {
visibility: 'compact',
compactMode: { horizontal: HorizontalDirection.Right, vertical: VerticalDirection.Below },
};
const menuBar = new MenuBar(menubar, { ...menuOptions, visibility: 'hidden' }, defaultMenuStyles);
disposableStore.add(combinedDisposable(toDisposable(() => menuBar.blur()), menuBar));
menuBar.push([
{ label: 'File', actions: [action('new', 'New File'), action('open', 'Open File')] },
{
label: 'Edit',
actions: [
action('undo', 'Undo'), action('redo', 'Redo'), new Separator(),
action('cut', 'Cut'), action('copy', 'Copy'), action('paste', 'Paste'), new Separator(),
action('find', 'Find'), action('replace', 'Replace'),
],
},
{ label: 'Selection', actions: [action('selectAll', 'Select All')] },
{ label: 'View', actions: [action('appearance', 'Appearance')] },
{ label: 'Go', actions: [action('goToFile', 'Go to File')] },
{ label: 'Run', actions: [action('start', 'Start Debugging')] },
{ label: 'Terminal', actions: [action('terminal', 'New Terminal')] },
{ label: 'Help', actions: [action('about', 'About')] },
]);
menuBar.update(menuOptions);
menuBar.toggleFocus();
const toggle = menubar.querySelector<HTMLElement>('.menubar-menu-button');
if (!toggle) {
throw new Error('Expected the compact application menu button.');
}
toggle.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', keyCode: 13, bubbles: true }));
const [file, edit] = menubar.querySelectorAll<HTMLElement>('.monaco-menu .action-menu-item');
if (!file || !edit) {
throw new Error('Expected the File and Edit menu entries.');
}
file.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', keyCode: 40, bubbles: true }));
edit.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight', keyCode: 39, bubbles: true }));
}

export default defineThemedFixtureGroup({ path: 'workbench/' }, {
CompactActivityBarMenu_DefaultDensity: defineComponentFixture({
labels: { kind: 'screenshot', blocksCi: true },
additionalThemes: ['darkHighContrast'],
expectedVisualDescriptions: ['The compact application menu and its open Edit submenu use the same menu-row spacing. Separators retain normal menu spacing, without extra activity-bar gaps. The Explorer and Search icons remain separated on the rail.'],
render: ctx => renderActivityBarMenu(ctx, false),
}),
CompactActivityBarMenu_CompactDensity: defineComponentFixture({
labels: { kind: 'screenshot', blocksCi: true },
additionalThemes: ['darkHighContrast'],
expectedVisualDescriptions: ['Compact Modern UI density tightens the activity rail, but the application menu and Edit submenu retain the same row and separator spacing as default density.'],
render: ctx => renderActivityBarMenu(ctx, true),
}),
});
18 changes: 18 additions & 0 deletions test/componentFixtures/blocks-ci-screenshots.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,21 @@

#### sessions/sessionsList/SessionsList_SelectedKeyboardFocus/Light
![screenshot](https://hediet-screenshots.azurewebsites.net/images/6a869c24b606e74a74331f03f693b8459e7ab2a92f77269b4b2ca49b880a4ffa)

#### workbench/activityBarMenu/CompactActivityBarMenu_CompactDensity/Dark
![screenshot](https://hediet-screenshots.azurewebsites.net/images/f8767471201b45f1193d7df64a848809e6e7557a4973896a9722f78e17e40610)

#### workbench/activityBarMenu/CompactActivityBarMenu_CompactDensity/DarkHighContrast
![screenshot](https://hediet-screenshots.azurewebsites.net/images/50440b966cd7c6cf462f584c7048f5a669b292463ae8914657281b048c1a6806)

#### workbench/activityBarMenu/CompactActivityBarMenu_CompactDensity/Light
![screenshot](https://hediet-screenshots.azurewebsites.net/images/1378229a45238eac02e9b4d12d2a37da63d58e9e81612a7a0c09da7bc5a11242)

#### workbench/activityBarMenu/CompactActivityBarMenu_DefaultDensity/Dark
![screenshot](https://hediet-screenshots.azurewebsites.net/images/c3cc700411e3faa1c62693702347277e25f5ce218bcaff3c8382978e4ebed96c)

#### workbench/activityBarMenu/CompactActivityBarMenu_DefaultDensity/DarkHighContrast
![screenshot](https://hediet-screenshots.azurewebsites.net/images/d17288e25fd966b8dc7fe9300c1adcfe5c77b5b700ff6ffb4eea37e6d1a773ae)

#### workbench/activityBarMenu/CompactActivityBarMenu_DefaultDensity/Light
![screenshot](https://hediet-screenshots.azurewebsites.net/images/2786fba1b7f0b41a26e238ea678ef381bd004d977f16abcb600239e142d1488e)