Merge pull request #306760 from microsoft/mrleemurray/zany-purple-manatee

Enhance CI Status Widget with collapsible functionality and improved styles
This commit is contained in:
Lee Murray
2026-03-31 16:26:51 +01:00
committed by GitHub
3 changed files with 135 additions and 17 deletions

View File

@@ -644,8 +644,8 @@ export class ChangesViewPane extends ViewPane {
const ciWidget = this.ciStatusWidget;
const ciPane: IView = {
element: ciElement,
minimumSize: ciMinHeight,
maximumSize: Number.POSITIVE_INFINITY,
get minimumSize() { return ciWidget.collapsed ? CIStatusWidget.HEADER_HEIGHT : ciMinHeight; },
get maximumSize() { return ciWidget.collapsed ? CIStatusWidget.HEADER_HEIGHT : Number.POSITIVE_INFINITY; },
onDidChange: Event.map(this.ciStatusWidget.onDidChangeHeight, () => undefined),
layout: (height) => {
ciElement.style.height = `${height}px`;
@@ -668,6 +668,25 @@ export class ChangesViewPane extends ViewPane {
// Initially hide CI pane until checks arrive
this.splitView.setViewVisible(1, false);
let savedCIPaneHeight = CIStatusWidget.HEADER_HEIGHT + CIStatusWidget.PREFERRED_BODY_HEIGHT;
this._register(this.ciStatusWidget.onDidToggleCollapsed(collapsed => {
if (!this.splitView || !this.ciStatusWidget) {
return;
}
if (collapsed) {
// Save current size before collapsing
const currentSize = this.splitView.getViewSize(1);
if (currentSize > CIStatusWidget.HEADER_HEIGHT) {
savedCIPaneHeight = currentSize;
}
this.splitView.resizeView(1, CIStatusWidget.HEADER_HEIGHT);
} else {
// Restore saved size on expand
this.splitView.resizeView(1, savedCIPaneHeight);
}
this.layoutSplitView();
}));
this._register(this.ciStatusWidget.onDidChangeHeight(() => {
if (!this.splitView || !this.ciStatusWidget) {
return;

View File

@@ -148,7 +148,7 @@ class CICheckListRenderer implements IListRenderer<ICICheckListItem, ICICheckTem
*/
export class CIStatusWidget extends Disposable {
static readonly HEADER_HEIGHT = 30;
static readonly HEADER_HEIGHT = 38; // total header height in px
static readonly MIN_BODY_HEIGHT = 84; // at least 3 checks (3 * 28)
static readonly PREFERRED_BODY_HEIGHT = 112; // preferred 4 checks (4 * 28)
static readonly MAX_BODY_HEIGHT = 240; // at most ~8 checks
@@ -168,9 +168,14 @@ export class CIStatusWidget extends Disposable {
private readonly _onDidChangeHeight = this._register(new Emitter<void>());
readonly onDidChangeHeight = this._onDidChangeHeight.event;
private readonly _onDidToggleCollapsed = this._register(new Emitter<boolean>());
readonly onDidToggleCollapsed = this._onDidToggleCollapsed.event;
private _checkCount = 0;
private _collapsed = false;
private _model: GitHubPullRequestCIModel | undefined;
private _sessionResource: URI | undefined;
private readonly _chevronNode: HTMLElement;
get element(): HTMLElement {
return this._domNode;
@@ -181,6 +186,9 @@ export class CIStatusWidget extends Disposable {
if (this._checkCount === 0) {
return 0;
}
if (this._collapsed) {
return CIStatusWidget.HEADER_HEIGHT;
}
return CIStatusWidget.HEADER_HEIGHT + this._checkCount * CICheckListDelegate.ITEM_HEIGHT;
}
@@ -189,6 +197,11 @@ export class CIStatusWidget extends Disposable {
return this._checkCount > 0;
}
/** Whether the body is collapsed (header-only). */
get collapsed(): boolean {
return this._collapsed;
}
constructor(
container: HTMLElement,
@IOpenerService private readonly _openerService: IOpenerService,
@@ -201,7 +214,7 @@ export class CIStatusWidget extends Disposable {
this._domNode = dom.append(container, $('.ci-status-widget'));
this._domNode.style.display = 'none';
// Header (always visible)
// Header (always visible, click to collapse/expand)
this._headerNode = dom.append(this._domNode, $('.ci-status-widget-header'));
this._titleNode = dom.append(this._headerNode, $('.ci-status-widget-title'));
this._titleLabelNode = dom.append(this._titleNode, $('.ci-status-widget-title-label'));
@@ -213,9 +226,33 @@ export class CIStatusWidget extends Disposable {
e.preventDefault();
e.stopPropagation();
}));
this._chevronNode = dom.append(this._headerNode, $('.group-chevron'));
this._chevronNode.classList.add(...ThemeIcon.asClassNameArray(Codicon.chevronDown));
this._headerNode.setAttribute('role', 'button');
this._headerNode.setAttribute('aria-label', localize('ci.toggleChecks', "Toggle PR Checks"));
this._headerNode.setAttribute('aria-expanded', 'true');
this._headerNode.tabIndex = 0;
this._register(dom.addDisposableListener(this._headerNode, dom.EventType.CLICK, e => {
// Don't toggle when clicking the action bar
if (dom.isAncestor(e.target as HTMLElement, this._headerActionBarContainer)) {
return;
}
this._toggleCollapsed();
}));
this._register(dom.addDisposableListener(this._headerNode, dom.EventType.KEY_DOWN, e => {
if ((e.key === 'Enter' || e.key === ' ') && e.target === this._headerNode) {
e.preventDefault();
this._toggleCollapsed();
}
}));
// Body (list of checks)
this._bodyNode = dom.append(this._domNode, $('.ci-status-widget-body'));
const bodyId = 'ci-status-widget-body';
this._bodyNode = dom.append(this._domNode, $(`.${bodyId}`));
this._bodyNode.id = bodyId;
this._headerNode.setAttribute('aria-controls', bodyId);
const listContainer = $('.ci-status-widget-list');
this._list = this._register(this._instantiationService.createInstance(
@@ -250,6 +287,7 @@ export class CIStatusWidget extends Disposable {
this._model = model;
if (!model) {
this._checkCount = 0;
this._setCollapsed(false);
this._renderBody([]);
this._renderHeaderActions([]);
this._domNode.style.display = 'none';
@@ -261,6 +299,7 @@ export class CIStatusWidget extends Disposable {
if (checks.length === 0) {
this._checkCount = 0;
this._setCollapsed(false);
this._renderBody([]);
this._renderHeaderActions([]);
this._domNode.style.display = 'none';
@@ -344,9 +383,36 @@ export class CIStatusWidget extends Disposable {
* Called by the parent view after computing available space.
*/
layout(height: number): void {
if (this._collapsed) {
this._bodyNode.style.display = 'none';
return;
}
this._bodyNode.style.display = '';
this._list.layout(height);
}
private _toggleCollapsed(): void {
this._setCollapsed(!this._collapsed);
this._onDidToggleCollapsed.fire(this._collapsed);
// Also fires onDidChangeHeight so the SplitView pane updates its min/max constraints
this._onDidChangeHeight.fire();
}
private _setCollapsed(collapsed: boolean): void {
this._collapsed = collapsed;
this._updateChevron();
this._headerNode.setAttribute('aria-expanded', String(!collapsed));
}
private _updateChevron(): void {
this._chevronNode.className = 'group-chevron';
this._chevronNode.classList.add(
...ThemeIcon.asClassNameArray(
this._collapsed ? Codicon.chevronRight : Codicon.chevronDown
)
);
}
private _renderBody(checks: readonly ICICheckListItem[]): void {
this._list.splice(0, this._list.length, checks);
}

View File

@@ -18,10 +18,50 @@
position: relative;
display: flex;
align-items: center;
gap: 6px;
padding: 2px 0;
/* gap: 6px; */
padding: 6px 4px;
margin-top: 4px;
border-radius: 8px;
min-height: 22px;
font-weight: 500;
cursor: pointer;
user-select: none;
}
.ci-status-widget-header:hover {
background-color: var(--vscode-list-hoverBackground);
padding-right: 22px;
}
.ci-status-widget-header:focus {
padding-right: 22px;
}
.ci-status-widget-header:focus-visible {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
/* Chevron — right-aligned, visible on hover only */
.ci-status-widget-header .group-chevron {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
visibility: hidden;
opacity: 0;
}
.ci-status-widget-header:hover .group-chevron,
.ci-status-widget-header:focus-within .group-chevron {
visibility: visible;
opacity: 0.7;
}
/* Title - single line, overflow ellipsis */
@@ -50,8 +90,7 @@
padding-right: 8px;
}
.ci-status-widget.has-fix-actions:hover .ci-status-widget-counts,
.ci-status-widget.has-fix-actions:focus-within .ci-status-widget-counts {
.ci-status-widget-header:hover .ci-status-widget-counts, .ci-status-widget-header:focus .ci-status-widget-counts {
visibility: hidden;
}
@@ -84,15 +123,9 @@
}
.ci-status-widget-header-actions {
position: absolute;
right: 0;
display: none;
align-items: center;
}
.ci-status-widget:hover .ci-status-widget-header-actions.has-actions,
.ci-status-widget:focus-within .ci-status-widget-header-actions.has-actions {
flex-shrink: 0;
display: flex;
align-items: center;
}
.ci-status-widget-header-actions .monaco-action-bar {