diff --git a/src/vs/workbench/browser/media/floatingPanels.css b/src/vs/workbench/browser/media/floatingPanels.css index 20d856e1078b23..4f692abc60fb24 100644 --- a/src/vs/workbench/browser/media/floatingPanels.css +++ b/src/vs/workbench/browser/media/floatingPanels.css @@ -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); } diff --git a/src/vs/workbench/contrib/modernUI/test/browser/modernUI.contribution.test.ts b/src/vs/workbench/contrib/modernUI/test/browser/modernUI.contribution.test.ts index 887fb07cd8430a..2ccf411fe14183 100644 --- a/src/vs/workbench/contrib/modernUI/test/browser/modernUI.contribution.test.ts +++ b/src/vs/workbench/contrib/modernUI/test/browser/modernUI.contribution.test.ts @@ -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'; @@ -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(resolve => disposables.add(scheduleAtNextAnimationFrame(getWindow(root), () => resolve()))); + menuBar.toggleFocus(); + menubar.querySelector('.menubar-menu-button')!.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', keyCode: 13, bubbles: true })); + + const applicationMenu = menubar.querySelector('.monaco-menu'); + assert.ok(applicationMenu); + applicationMenu.querySelector('.action-menu-item')!.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowRight', keyCode: 39, bubbles: true })); + const submenu = applicationMenu.querySelector('.monaco-submenu .monaco-menu'); + assert.ok(submenu); + + const referenceHost = appendElement(root, 'reference-menu'); + disposables.add(new Menu(referenceHost, actions, {}, unthemedMenuStyles)); + const referenceMenu = referenceHost.querySelector('.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('.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'; diff --git a/src/vs/workbench/test/browser/componentFixtures/activityBarMenu.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/activityBarMenu.fixture.ts new file mode 100644 index 00000000000000..21d2ab8b978a38 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/activityBarMenu.fixture.ts @@ -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('.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('.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), + }), +}); diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md index 4b17a45e4def8b..18da96c342174a 100644 --- a/test/componentFixtures/blocks-ci-screenshots.md +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -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)