Files
vscode/src/vs/workbench/test/browser/componentFixtures/baseUI.fixture.ts
2026-03-03 12:57:45 +01:00

509 lines
19 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { $ } from '../../../../base/browser/dom.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { Action, Separator } from '../../../../base/common/actions.js';
// UI Components
import { Button, ButtonBar, ButtonWithDescription, unthemedButtonStyles } from '../../../../base/browser/ui/button/button.js';
import { Toggle, Checkbox, unthemedToggleStyles } from '../../../../base/browser/ui/toggle/toggle.js';
import { InputBox, MessageType, unthemedInboxStyles } from '../../../../base/browser/ui/inputbox/inputBox.js';
import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js';
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js';
import { HighlightedLabel } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';
import { ComponentFixtureContext, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js';
export default defineThemedFixtureGroup({
Buttons: defineComponentFixture({
labels: { kind: 'screenshot' },
render: renderButtons,
}),
ButtonBar: defineComponentFixture({
labels: { kind: 'screenshot' },
render: renderButtonBar,
}),
Toggles: defineComponentFixture({
labels: { kind: 'screenshot' },
render: renderToggles,
}),
InputBoxes: defineComponentFixture({
labels: { kind: 'screenshot' },
render: renderInputBoxes,
}),
CountBadges: defineComponentFixture({
labels: { kind: 'screenshot' },
render: renderCountBadges,
}),
ActionBar: defineComponentFixture({
labels: { kind: 'screenshot' },
render: renderActionBar,
}),
ProgressBars: defineComponentFixture({
labels: { kind: 'screenshot' },
render: renderProgressBars,
}),
HighlightedLabels: defineComponentFixture({
labels: { kind: 'screenshot' },
render: renderHighlightedLabels,
}),
});
// ============================================================================
// Styles (themed versions for fixture display)
// ============================================================================
const themedButtonStyles = {
...unthemedButtonStyles,
buttonBackground: 'var(--vscode-button-background)',
buttonHoverBackground: 'var(--vscode-button-hoverBackground)',
buttonForeground: 'var(--vscode-button-foreground)',
buttonSecondaryBackground: 'var(--vscode-button-secondaryBackground)',
buttonSecondaryHoverBackground: 'var(--vscode-button-secondaryHoverBackground)',
buttonSecondaryForeground: 'var(--vscode-button-secondaryForeground)',
buttonBorder: 'var(--vscode-button-border)',
};
const themedToggleStyles = {
...unthemedToggleStyles,
inputActiveOptionBorder: 'var(--vscode-inputOption-activeBorder)',
inputActiveOptionForeground: 'var(--vscode-inputOption-activeForeground)',
inputActiveOptionBackground: 'var(--vscode-inputOption-activeBackground)',
};
const themedCheckboxStyles = {
checkboxBackground: 'var(--vscode-checkbox-background)',
checkboxBorder: 'var(--vscode-checkbox-border)',
checkboxForeground: 'var(--vscode-checkbox-foreground)',
checkboxDisabledBackground: undefined,
checkboxDisabledForeground: undefined,
};
const themedInputBoxStyles = {
...unthemedInboxStyles,
inputBackground: 'var(--vscode-input-background)',
inputForeground: 'var(--vscode-input-foreground)',
inputBorder: 'var(--vscode-input-border)',
inputValidationInfoBackground: 'var(--vscode-inputValidation-infoBackground)',
inputValidationInfoBorder: 'var(--vscode-inputValidation-infoBorder)',
inputValidationWarningBackground: 'var(--vscode-inputValidation-warningBackground)',
inputValidationWarningBorder: 'var(--vscode-inputValidation-warningBorder)',
inputValidationErrorBackground: 'var(--vscode-inputValidation-errorBackground)',
inputValidationErrorBorder: 'var(--vscode-inputValidation-errorBorder)',
};
const themedBadgeStyles = {
badgeBackground: 'var(--vscode-badge-background)',
badgeForeground: 'var(--vscode-badge-foreground)',
badgeBorder: undefined,
};
const themedProgressBarOptions = {
progressBarBackground: 'var(--vscode-progressBar-background)',
};
// ============================================================================
// Buttons
// ============================================================================
function renderButtons({ container, disposableStore }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '12px';
// Section: Primary Buttons
const primarySection = $('div');
primarySection.style.display = 'flex';
primarySection.style.gap = '8px';
primarySection.style.alignItems = 'center';
container.appendChild(primarySection);
const primaryButton = disposableStore.add(new Button(primarySection, { ...themedButtonStyles, title: 'Primary button' }));
primaryButton.label = 'Primary Button';
const primaryIconButton = disposableStore.add(new Button(primarySection, { ...themedButtonStyles, title: 'With Icon', supportIcons: true }));
primaryIconButton.label = '$(add) Add Item';
const smallButton = disposableStore.add(new Button(primarySection, { ...themedButtonStyles, title: 'Small button', small: true }));
smallButton.label = 'Small';
// Section: Secondary Buttons
const secondarySection = $('div');
secondarySection.style.display = 'flex';
secondarySection.style.gap = '8px';
secondarySection.style.alignItems = 'center';
container.appendChild(secondarySection);
const secondaryButton = disposableStore.add(new Button(secondarySection, { ...themedButtonStyles, secondary: true, title: 'Secondary button' }));
secondaryButton.label = 'Secondary Button';
const secondaryIconButton = disposableStore.add(new Button(secondarySection, { ...themedButtonStyles, secondary: true, title: 'Cancel', supportIcons: true }));
secondaryIconButton.label = '$(close) Cancel';
// Section: Disabled Buttons
const disabledSection = $('div');
disabledSection.style.display = 'flex';
disabledSection.style.gap = '8px';
disabledSection.style.alignItems = 'center';
container.appendChild(disabledSection);
const disabledButton = disposableStore.add(new Button(disabledSection, { ...themedButtonStyles, title: 'Disabled', disabled: true }));
disabledButton.label = 'Disabled';
disabledButton.enabled = false;
const disabledSecondary = disposableStore.add(new Button(disabledSection, { ...themedButtonStyles, secondary: true, title: 'Disabled Secondary', disabled: true }));
disabledSecondary.label = 'Disabled Secondary';
disabledSecondary.enabled = false;
}
function renderButtonBar({ container, disposableStore }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '16px';
// Button Bar
const barContainer = $('div');
container.appendChild(barContainer);
const buttonBar = new ButtonBar(barContainer);
disposableStore.add(buttonBar);
const okButton = buttonBar.addButton({ ...themedButtonStyles, title: 'OK' });
okButton.label = 'OK';
const cancelButton = buttonBar.addButton({ ...themedButtonStyles, secondary: true, title: 'Cancel' });
cancelButton.label = 'Cancel';
// Button with Description
const descContainer = $('div');
descContainer.style.width = '300px';
container.appendChild(descContainer);
const buttonWithDesc = disposableStore.add(new ButtonWithDescription(descContainer, { ...themedButtonStyles, title: 'Install Extension', supportIcons: true }));
buttonWithDesc.label = '$(extensions) Install Extension';
buttonWithDesc.description = 'This will install the extension and enable it globally';
}
// ============================================================================
// Toggles and Checkboxes
// ============================================================================
function renderToggles({ container, disposableStore }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '12px';
// Toggles
const toggleSection = $('div');
toggleSection.style.display = 'flex';
toggleSection.style.gap = '16px';
toggleSection.style.alignItems = 'center';
container.appendChild(toggleSection);
const toggle1 = disposableStore.add(new Toggle({
...themedToggleStyles,
title: 'Case Sensitive',
isChecked: false,
icon: Codicon.caseSensitive,
}));
toggleSection.appendChild(toggle1.domNode);
const toggle2 = disposableStore.add(new Toggle({
...themedToggleStyles,
title: 'Whole Word',
isChecked: true,
icon: Codicon.wholeWord,
}));
toggleSection.appendChild(toggle2.domNode);
const toggle3 = disposableStore.add(new Toggle({
...themedToggleStyles,
title: 'Use Regular Expression',
isChecked: false,
icon: Codicon.regex,
}));
toggleSection.appendChild(toggle3.domNode);
// Checkboxes
const checkboxSection = $('div');
checkboxSection.style.display = 'flex';
checkboxSection.style.flexDirection = 'column';
checkboxSection.style.gap = '8px';
container.appendChild(checkboxSection);
const createCheckboxRow = (label: string, checked: boolean) => {
const row = $('div');
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.gap = '8px';
const checkbox = disposableStore.add(new Checkbox(label, checked, themedCheckboxStyles));
row.appendChild(checkbox.domNode);
const labelEl = $('span');
labelEl.textContent = label;
labelEl.style.color = 'var(--vscode-foreground)';
row.appendChild(labelEl);
return row;
};
checkboxSection.appendChild(createCheckboxRow('Enable auto-save', true));
checkboxSection.appendChild(createCheckboxRow('Show line numbers', true));
checkboxSection.appendChild(createCheckboxRow('Word wrap', false));
}
// ============================================================================
// Input Boxes
// ============================================================================
function renderInputBoxes({ container, disposableStore }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '16px';
container.style.width = '350px';
// Input with value
const filledInput = disposableStore.add(new InputBox(container, undefined, {
placeholder: 'File path',
inputBoxStyles: themedInputBoxStyles,
}));
filledInput.value = '/src/vs/editor/browser';
// Input with info validation
const infoInput = disposableStore.add(new InputBox(container, undefined, {
placeholder: 'Username',
inputBoxStyles: themedInputBoxStyles,
validationOptions: {
validation: (value) => value.length < 3 ? { content: 'Username must be at least 3 characters', type: MessageType.INFO } : null
}
}));
infoInput.value = 'ab';
infoInput.validate();
// Input with warning validation
const warningInput = disposableStore.add(new InputBox(container, undefined, {
placeholder: 'Password',
inputBoxStyles: themedInputBoxStyles,
validationOptions: {
validation: (value) => value.length < 8 ? { content: 'Password should be at least 8 characters for security', type: MessageType.WARNING } : null
}
}));
warningInput.value = 'pass';
warningInput.validate();
// Input with error validation
const errorInput = disposableStore.add(new InputBox(container, undefined, {
placeholder: 'Email address',
inputBoxStyles: themedInputBoxStyles,
validationOptions: {
validation: (value) => !value.includes('@') ? { content: 'Please enter a valid email address', type: MessageType.ERROR } : null
}
}));
errorInput.value = 'invalid-email';
errorInput.validate();
}
// ============================================================================
// Count Badges
// ============================================================================
function renderCountBadges({ container }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.gap = '12px';
container.style.alignItems = 'center';
// Various badge counts
const counts = [1, 5, 12, 99, 999];
for (const count of counts) {
const badgeContainer = $('div');
badgeContainer.style.display = 'flex';
badgeContainer.style.alignItems = 'center';
badgeContainer.style.gap = '8px';
const label = $('span');
label.textContent = 'Issues';
label.style.color = 'var(--vscode-foreground)';
badgeContainer.appendChild(label);
new CountBadge(badgeContainer, { count }, themedBadgeStyles);
container.appendChild(badgeContainer);
}
}
// ============================================================================
// Action Bar
// ============================================================================
function renderActionBar({ container, disposableStore }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '16px';
// Horizontal action bar
const horizontalLabel = $('div');
horizontalLabel.textContent = 'Horizontal Actions:';
horizontalLabel.style.color = 'var(--vscode-foreground)';
horizontalLabel.style.marginBottom = '4px';
container.appendChild(horizontalLabel);
const horizontalContainer = $('div');
container.appendChild(horizontalContainer);
const horizontalBar = disposableStore.add(new ActionBar(horizontalContainer, {
ariaLabel: 'Editor Actions',
}));
horizontalBar.push([
new Action('editor.action.save', 'Save', ThemeIcon.asClassName(Codicon.save), true, async () => console.log('Save')),
new Action('editor.action.undo', 'Undo', ThemeIcon.asClassName(Codicon.discard), true, async () => console.log('Undo')),
new Action('editor.action.redo', 'Redo', ThemeIcon.asClassName(Codicon.redo), true, async () => console.log('Redo')),
new Separator(),
new Action('editor.action.find', 'Find', ThemeIcon.asClassName(Codicon.search), true, async () => console.log('Find')),
new Action('editor.action.replace', 'Replace', ThemeIcon.asClassName(Codicon.replaceAll), true, async () => console.log('Replace')),
]);
// Action bar with disabled items
const mixedLabel = $('div');
mixedLabel.textContent = 'Mixed States:';
mixedLabel.style.color = 'var(--vscode-foreground)';
mixedLabel.style.marginBottom = '4px';
container.appendChild(mixedLabel);
const mixedContainer = $('div');
container.appendChild(mixedContainer);
const mixedBar = disposableStore.add(new ActionBar(mixedContainer, {
ariaLabel: 'Mixed Actions',
}));
mixedBar.push([
new Action('action.enabled', 'Enabled', ThemeIcon.asClassName(Codicon.play), true, async () => { }),
new Action('action.disabled', 'Disabled', ThemeIcon.asClassName(Codicon.debugPause), false, async () => { }),
new Action('action.enabled2', 'Enabled', ThemeIcon.asClassName(Codicon.debugStop), true, async () => { }),
]);
}
// ============================================================================
// Progress Bar
// ============================================================================
function renderProgressBars({ container, disposableStore }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '24px';
container.style.width = '400px';
const createSection = (label: string) => {
const section = $('div');
const labelEl = $('div');
labelEl.textContent = label;
labelEl.style.color = 'var(--vscode-foreground)';
labelEl.style.marginBottom = '8px';
labelEl.style.fontSize = '12px';
section.appendChild(labelEl);
// Progress bar container with proper constraints
const barContainer = $('div');
barContainer.style.position = 'relative';
barContainer.style.width = '100%';
barContainer.style.height = '4px';
barContainer.style.overflow = 'hidden';
section.appendChild(barContainer);
container.appendChild(section);
return barContainer;
};
// Discrete progress - 30%
const progress30Section = createSection('Discrete Progress - 30%');
const progress30Bar = disposableStore.add(new ProgressBar(progress30Section, themedProgressBarOptions));
progress30Bar.total(100);
progress30Bar.worked(30);
// Discrete progress - 60%
const progress60Section = createSection('Discrete Progress - 60%');
const progress60Bar = disposableStore.add(new ProgressBar(progress60Section, themedProgressBarOptions));
progress60Bar.total(100);
progress60Bar.worked(60);
// Discrete progress - 90%
const progress90Section = createSection('Discrete Progress - 90%');
const progress90Bar = disposableStore.add(new ProgressBar(progress90Section, themedProgressBarOptions));
progress90Bar.total(100);
progress90Bar.worked(90);
// Completed progress
const doneSection = createSection('Completed (100%)');
const doneBar = disposableStore.add(new ProgressBar(doneSection, themedProgressBarOptions));
doneBar.total(100);
doneBar.worked(100);
}
// ============================================================================
// Highlighted Label
// ============================================================================
function renderHighlightedLabels({ container }: ComponentFixtureContext): void {
container.style.padding = '16px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '8px';
container.style.color = 'var(--vscode-foreground)';
const createHighlightedLabel = (text: string, highlights: { start: number; end: number }[]) => {
const row = $('div');
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.gap = '8px';
const labelContainer = $('div');
const label = new HighlightedLabel(labelContainer);
label.set(text, highlights);
row.appendChild(labelContainer);
const queryLabel = $('span');
queryLabel.style.color = 'var(--vscode-descriptionForeground)';
queryLabel.style.fontSize = '12px';
queryLabel.textContent = `(matches highlighted)`;
row.appendChild(queryLabel);
return row;
};
// File search examples
container.appendChild(createHighlightedLabel('codeEditorWidget.ts', [{ start: 0, end: 4 }])); // "code"
container.appendChild(createHighlightedLabel('inlineCompletionsController.ts', [{ start: 6, end: 10 }])); // "Comp"
container.appendChild(createHighlightedLabel('diffEditorViewModel.ts', [{ start: 0, end: 4 }, { start: 10, end: 14 }])); // "diff" and "View"
container.appendChild(createHighlightedLabel('workbenchTestServices.ts', [{ start: 9, end: 13 }])); // "Test"
}