diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 16e11f83457..e0476f26ae9 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -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; diff --git a/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts b/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts index f83c243b5b2..750719fba72 100644 --- a/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts +++ b/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts @@ -148,7 +148,7 @@ class CICheckListRenderer implements IListRenderer()); readonly onDidChangeHeight = this._onDidChangeHeight.event; + private readonly _onDidToggleCollapsed = this._register(new Emitter()); + 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); } diff --git a/src/vs/sessions/contrib/changes/browser/media/ciStatusWidget.css b/src/vs/sessions/contrib/changes/browser/media/ciStatusWidget.css index 199b1b933ed..94586fbd9b0 100644 --- a/src/vs/sessions/contrib/changes/browser/media/ciStatusWidget.css +++ b/src/vs/sessions/contrib/changes/browser/media/ciStatusWidget.css @@ -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 {