Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@
--modern-ui-editor-tab-unfocused-hover-background: var(--modern-ui-editor-tab-hover-background);
--modern-ui-editor-tab-action-hover-background: var(--modern-ui-editor-tab-hover-background);
--modern-ui-editor-tab-action-unfocused-hover-background: var(--modern-ui-editor-tab-hover-background);
--modern-ui-tab-action-padding: calc(var(--vscode-spacing-size280) + var(--modern-ui-connected-tab-shoulder-radius) + var(--vscode-spacing-size20));
}

.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row > .tab-fill {
Expand All @@ -447,6 +448,14 @@
inset-block: calc(-1 * var(--vscode-spacing-size20));
}

.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row:not(.sticky-compact):not(.tab-actions-left) > .tab-actions {
right: calc(var(--modern-ui-connected-tab-shoulder-radius) + var(--vscode-spacing-size20));
}

.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row:not(.sticky-compact).tab-actions-left > .tab-actions {
left: calc(var(--modern-ui-connected-tab-shoulder-radius) + var(--vscode-spacing-size20));
}

.modern-ui.modern-ui-connected-editor-tabs.monaco-workbench:is(.hc-black, .hc-light) .part.editor > .content .editor-group-container > .title.tabs .tabs-container > .tab.connected-tab-upper-row.active > .tab-fill {
border-color: var(--modern-ui-connected-tab-border);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
background-color: var(--modern-ui-editor-tab-active-hover-background);
}

.monaco-workbench .monaco-action-bar:not(.vertical) .action-label.fixture-action-hover:not(.disabled) {
background-color: var(--vscode-toolbar-hoverBackground);
}

.modern-ui-tabs.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.fixture-hover:not(.close-action-off) > .tab-actions {
background-color: var(--modern-ui-editor-tab-action-active-hover-background);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ export interface IEditorTabBarFixtureOptions {
readonly headerMenuIds?: IEditorGroupMenuIds;
readonly colorCustomizations?: Readonly<Record<string, string>>;
readonly forcedHoverTab?: number;
readonly forcedHoverTabAction?: number;
readonly focusedTabAction?: number;
readonly editorContents?: string;
readonly activeTabClipping?: 'left' | 'right' | 'left-shoulder' | 'right-shoulder';
Expand Down Expand Up @@ -591,6 +592,9 @@ export function renderEditorTabBarFixture(ctx: ComponentFixtureContext, options:
if (options.forcedHoverTab !== undefined) {
tabs[options.forcedHoverTab]?.classList.add('fixture-hover');
}
if (options.forcedHoverTabAction !== undefined) {
tabs[options.forcedHoverTabAction]?.querySelector<HTMLElement>('.tab-actions .action-label')?.classList.add('fixture-action-hover');
}
if (options.focusedTabAction !== undefined) {
const action = tabs[options.focusedTabAction]?.querySelector<HTMLElement>('.tab-actions .action-label');
if (action) {
Expand Down Expand Up @@ -826,6 +830,15 @@ function renderWrappedConnectedSurface(activeTabIndex: number, forcedHoverTab?:
});
}

function renderWrappedConnectedCloseActionHover(activeTabIndex: number): (ctx: ComponentFixtureContext) => void {
return renderConnectedSurface(activeTabIndex, activeTabIndex, undefined, {
width: 820,
editors: manyEditorSpecs().slice(0, 10).map((spec, index) => ({ ...spec, dirty: false, active: index <= activeTabIndex })),
partOptions: { wrapTabs: true, editorActionsLocation: 'hidden' },
forcedHoverTabAction: activeTabIndex,
});
}

const connectedSurfaceThemes: readonly ComponentFixtureAdditionalTheme[] = ['darkModern', 'light2026', 'darkPlus', 'lightPlus', 'visualStudioDark', 'visualStudioLight', 'darkHighContrast', 'lightHighContrast', 'abyss', 'monokai', 'quietLight', 'solarizedDark', 'solarizedLight'];

export default defineThemedFixtureGroup({ path: 'editor/editorTabBar/' }, {
Expand Down Expand Up @@ -863,6 +876,34 @@ export default defineThemedFixtureGroup({ path: 'editor/editorTabBar/' }, {
}),
expectedVisualDescriptions: ['Hover reveals the inactive close without covering its ellipsized basename or extension.'],
}),
SingleTab: defineComponentFixture({
render: renderConnectedSurface(0, undefined, undefined, {
editors: [{ resource: file('/project/README.md'), pinned: true, active: true }],
partOptions: { editorActionsLocation: 'hidden' },
}),
additionalThemes: connectedSurfaceThemes,
expectedVisualDescriptions: ['The single connected tab retains the same close-button spacing and visible cap width as an active tab beside another tab. Its terminal shoulder turns into the document well without crowding the action.'],
}),
SingleTabCloseActionHovered: defineComponentFixture({
render: renderConnectedSurface(0, 0, undefined, {
editors: [{ resource: file('/project/README.md'), pinned: true, active: true }],
partOptions: { editorActionsLocation: 'hidden' },
forcedHoverTabAction: 0,
}),
expectedVisualDescriptions: ['The close action hover background has even inset spacing on every side and remains separated from the single tab terminal shoulder.'],
}),
LastTabCloseActionHovered: defineComponentFixture({
render: renderConnectedSurface(3, 3, undefined, {
editors: [
{ resource: file('/project/README.md'), pinned: true },
{ resource: file('/project/src/app/main.ts'), pinned: true },
{ resource: file('/project/src/app/styles.css'), pinned: true },
{ resource: file('/project/package.json'), pinned: true, active: true },
],
forcedHoverTabAction: 3,
}),
expectedVisualDescriptions: ['The last tab close action hover background has the same even inset spacing as the single-tab state and remains separated from the terminal shoulder.'],
}),
NarrowWindow: defineComponentFixture({
render: renderConnectedSurface(0, undefined, undefined, {
width: 420,
Expand All @@ -882,6 +923,14 @@ export default defineThemedFixtureGroup({ path: 'editor/editorTabBar/' }, {
additionalThemes: connectedSurfaceThemes,
expectedVisualDescriptions: ['The selected tab in the bottom wrapped row connects directly to the document well with curved shoulders and no bottom gap, just like a single row. The adjacent inactive tab also reaches the well boundary. Upper-row tabs retain separate rounded pills.'],
}),
UpperWrappedCloseActionHovered: defineComponentFixture({
render: renderWrappedConnectedCloseActionHover(0),
expectedVisualDescriptions: ['The upper-row pill close action hover background has even inset spacing on every side and remains separated from the pill edge.'],
}),
BottomWrappedCloseActionHovered: defineComponentFixture({
render: renderWrappedConnectedCloseActionHover(9),
expectedVisualDescriptions: ['The bottom-row connected tab close action hover background has even inset spacing on every side and remains separated from the terminal shoulder.'],
}),
UpperWrappedHover: defineComponentFixture({
render: renderWrappedConnectedSurface(9, 1),
additionalThemes: connectedSurfaceThemes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,91 @@ suite('MultiEditorTabsControl', () => {
assert.deepStrictEqual({ clean, dirty, focused }, { clean: ['1', '0'], dirty: ['1', '1'], focused: ['1', '1'] });
});

test('connected close actions keep consistent spacing across terminal and wrapped tabs', async () => {
const group = connectedGroup();
const measure = () => {
const tab = container.querySelector<HTMLElement>('.tab.active')!;
const fill = tab.querySelector<HTMLElement>('.tab-fill')!;
const action = tab.querySelector<HTMLElement>('.action-label')!;
const label = tab.querySelector<HTMLElement>('.monaco-icon-label-container')!;
const fillBounds = fill.getBoundingClientRect();
const actionBounds = action.getBoundingClientRect();
const actionStyle = mainWindow.getComputedStyle(action);
return {
top: actionBounds.top - fillBounds.top,
right: fillBounds.right - actionBounds.right,
left: actionBounds.left - label.getBoundingClientRect().right,
width: fillBounds.width,
padding: [actionStyle.paddingTop, actionStyle.paddingRight, actionStyle.paddingBottom, actionStyle.paddingLeft],
};
};

await layoutConnectedGroup(group, 400);
const multiple = measure();

const secondEditor = model.getEditorByIndex(1)!;
model.closeEditor(secondEditor);
control.closeEditor(secondEditor);
await layoutConnectedGroup(group, 400);
const single = measure();

model.openEditor(secondEditor, { pinned: true, active: true });
control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL));
const oldOptions = partOptions;
partOptions = { ...partOptions, wrapTabs: true, tabSizing: 'fixed', tabSizingFixedMinWidth: 120, tabSizingFixedMaxWidth: 120, editorActionsLocation: 'hidden' };
control.updateOptions(oldOptions, partOptions);
await layoutConnectedGroup(group, 150);
const wrappedBottom = measure();

model.openEditor(model.getEditorByIndex(0)!, { active: true });
control.openEditors(model.getEditors(EditorsOrder.SEQUENTIAL));
await layoutConnectedGroup(group, 150);
const wrappedUpper = measure();
const measurements = [multiple, single, wrappedBottom, wrappedUpper];

const oldWrappedOptions = partOptions;
partOptions = { ...partOptions, wrapTabs: false, tabSizing: 'fit', tabActionLocation: 'left' };
control.updateOptions(oldWrappedOptions, partOptions);
await layoutConnectedGroup(group, 400);
const leftMultiple = measure();

model.closeEditor(secondEditor);
control.closeEditor(secondEditor);
await layoutConnectedGroup(group, 400);
const leftSingle = measure();

assert.deepStrictEqual({
single: {
top: single.top === multiple.top,
right: single.right === multiple.right,
left: single.left === multiple.left,
width: single.width === multiple.width,
},
wrapped: {
top: wrappedBottom.top === wrappedUpper.top,
right: wrappedBottom.right === wrappedUpper.right,
left: wrappedBottom.left === wrappedUpper.left,
},
horizontal: {
right: measurements.every(measurement => measurement.right === multiple.right),
left: measurements.every(measurement => measurement.left === multiple.left),
},
leftAction: {
top: leftSingle.top === leftMultiple.top,
right: leftSingle.right === leftMultiple.right,
left: leftSingle.left === leftMultiple.left,
width: leftSingle.width === leftMultiple.width,
},
actionPadding: measurements.every(measurement => new Set(measurement.padding).size === 1 && measurement.padding[0] === multiple.padding[0]),
}, {
single: { top: true, right: true, left: true, width: true },
wrapped: { top: true, right: true, left: true },
horizontal: { right: true, left: true },
leftAction: { top: true, right: true, left: true, width: true },
actionPadding: true,
});
});

test('reveals the active tab with its right shoulder outside the label and action', async () => {
const group = connectedGroup();
const oldOptions = partOptions;
Expand Down
Loading