Skip to content

Commit 57deef8

Browse files
authored
feat: animate notebook cell execution footer expand/collapse (#12946)
Partially addresses #11775 Hide the cell execution footer when a cell has never been run or was only run in a previous session. The footer collapses with a CSS transition (height, opacity, borders) and expands when the cell is queued or executed. ### Demo _The execution info smoothly slides in and out as the info is available._ https://github.com/user-attachments/assets/e5876226-1d5d-4e10-817b-9ec864197b59 - Respects `prefers-reduced-motion` by disabling transitions - ARIA labels suppressed on collapsed footers via `aria-hidden` - Active states (running/pending) prioritized over static states in ARIA label ordering - Border radius adjusts when footer is collapsed and no outputs are present #### Performance In theory this could be a performance issue with really large notebooks because the animation triggers a reflow of lower cells. In practice, it's not bad as if a cell has no output it's not _normally_ followed by a bunch of cells with outputs. Also the animations are all done with CSS so they're fairly optimized. I tested with a notebook with ~100 cells and didnt notice any real performance issues but we can/should keep an eye on it. ### Release Notes #### New Features - Notebook cell execution footer now hides when the cell has not been run and smoothly animates in/out during execution #### Bug Fixes - N/A ### QA Notes @:notebooks @:positron-notebooks 2. Verify cells that have never been run do NOT show the execution footer 3. Run a cell -- footer should animate in and show timing info 4. Add a new cell without running it -- footer should not be visible 5. Queue multiple cells -- footer should appear immediately for pending cells 6. Check with system reduced-motion enabled -- transitions should be instant
1 parent 13f1217 commit 57deef8

5 files changed

Lines changed: 65 additions & 15 deletions

File tree

src/vs/workbench/contrib/positronNotebook/browser/notebookCells/CodeCellStatusFooter.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
gap: 8px;
1111
padding: 0px 1rem;
1212
height: 24px;
13+
overflow: hidden;
1314

1415
/* Visual separators from editor content above and outputs below */
1516
border-top: 1px solid var(--vscode-widget-border);
@@ -25,6 +26,23 @@
2526

2627
/* Override inherited cursor from .positron-notebook class */
2728
cursor: default;
29+
30+
/* Animate expand/collapse */
31+
transition: height 150ms ease-out, opacity 150ms ease-out,
32+
border-top-width 150ms ease-out, border-bottom-width 150ms ease-out;
33+
}
34+
35+
.positron-notebook-code-cell-footer.collapsed {
36+
height: 0;
37+
opacity: 0;
38+
border-top-width: 0;
39+
border-bottom-width: 0;
40+
}
41+
42+
@media (prefers-reduced-motion: reduce) {
43+
.positron-notebook-code-cell-footer {
44+
transition: none;
45+
}
2846
}
2947

3048
/* Override inherited cursor from .positron-notebook class for children spans */

src/vs/workbench/contrib/positronNotebook/browser/notebookCells/CodeCellStatusFooter.tsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ExecutionStatus } from '../PositronNotebookCells/IPositronNotebookCell.
1818
import { formatCellDuration, formatTimestamp, getRelativeTime, isMoreThanOneHourAgo } from './cellExecutionUtils.js';
1919
import { Icon } from '../../../../../platform/positronActionBar/browser/components/icon.js';
2020
import { Codicon } from '../../../../../base/common/codicons.js';
21+
import { positronClassNames } from '../../../../../base/common/positronUtilities.js';
2122

2223
interface CodeCellStatusFooterProps {
2324
cell: PositronNotebookCodeCell;
@@ -77,13 +78,18 @@ export function CodeCellStatusFooter({ cell, hasError }: CodeCellStatusFooterPro
7778
const hasCurrentSessionContent = hasExecutionResult || isCurrentlyRunning || hasTimingInfo;
7879

7980
// Check if cell has never been run (no execution order and no current session data)
80-
const hasNeverBeenRun = !hasExecutionOrder && !hasExecutionResult && !isCurrentlyRunning;
81+
const hasNeverBeenRun = !hasExecutionOrder && !hasCurrentSessionContent;
8182

8283
// Check if we only have execution order from previous session (no current session data)
8384
const wasRunInPreviousSession = hasExecutionOrder && !hasCurrentSessionContent;
8485

8586
const isPending = executionStatus === 'pending';
8687

88+
// Collapse the footer when there's no execution info to display.
89+
// This covers cells that have never been run and cells only run in a previous session.
90+
// isPending guard keeps the clock icon visible for queued cells.
91+
const isCollapsed = !isPending && !hasCurrentSessionContent;
92+
8793
const dataExecutionStatus = executionStatus || 'idle';
8894

8995
const renderIcon = () => {
@@ -147,16 +153,10 @@ export function CodeCellStatusFooter({ cell, hasError }: CodeCellStatusFooterPro
147153
return null;
148154
};
149155

150-
// Build ARIA label for accessibility
156+
// Build ARIA label for accessibility.
157+
// Active states (running/pending) take priority over static states so a
158+
// queued cell that has never been run is announced as "queued", not "not yet run".
151159
const getAriaLabel = () => {
152-
if (hasNeverBeenRun) {
153-
return localize('cellExecution.notYetRun', 'Cell not yet run');
154-
}
155-
156-
if (wasRunInPreviousSession) {
157-
return localize('cellExecution.notRunThisSession', 'Not run this session');
158-
}
159-
160160
if (isCurrentlyRunning) {
161161
return localize('cellExecution.running', 'Cell is executing');
162162
}
@@ -165,6 +165,14 @@ export function CodeCellStatusFooter({ cell, hasError }: CodeCellStatusFooterPro
165165
return localize('cellExecution.pending', 'Cell is queued for execution');
166166
}
167167

168+
if (hasNeverBeenRun) {
169+
return localize('cellExecution.notYetRun', 'Cell not yet run');
170+
}
171+
172+
if (wasRunInPreviousSession) {
173+
return localize('cellExecution.notRunThisSession', 'Not run this session');
174+
}
175+
168176
if (hasTimingInfo && duration !== undefined && lastRunEndTime !== undefined) {
169177
const status = hasError || lastRunSuccess === false
170178
? localize('cellExecution.failed', 'Cell execution failed')
@@ -181,9 +189,10 @@ export function CodeCellStatusFooter({ cell, hasError }: CodeCellStatusFooterPro
181189
return (
182190
<div
183191
ref={containerRef}
184-
aria-label={getAriaLabel()}
192+
aria-hidden={isCollapsed || undefined}
193+
aria-label={isCollapsed ? undefined : getAriaLabel()}
185194
aria-live={isCurrentlyRunning ? 'polite' : 'off'}
186-
className='positron-notebook-code-cell-footer'
195+
className={positronClassNames('positron-notebook-code-cell-footer', { 'collapsed': isCollapsed })}
187196
data-execution-status={dataExecutionStatus}
188197
role='status'
189198
>

src/vs/workbench/contrib/positronNotebook/browser/notebookCells/NotebookCellSelection.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@
7676
border-bottom-color: transparent;
7777
}
7878

79+
/* When footer is collapsed (cell hasn't been run) and no outputs, editor container needs bottom radius */
80+
.positron-notebook-code-cell:has(.positron-notebook-outputs-section.no-outputs) .positron-notebook-editor-section:has(.positron-notebook-code-cell-footer.collapsed) .positron-notebook-editor-container {
81+
border-bottom-left-radius: var(--vscode-positronNotebook-cell-radius);
82+
border-bottom-right-radius: var(--vscode-positronNotebook-cell-radius);
83+
transition: border-bottom-left-radius 150ms ease-out, border-bottom-right-radius 150ms ease-out;
84+
}
85+
7986
/* Update the border color for code cells with no outputs based on the selection state */
8087
.positron-notebook-cell.selected:has(.positron-notebook-outputs-section.no-outputs) .positron-notebook-editor-section,
8188
.positron-notebook-cell.editing:has(.positron-notebook-outputs-section.no-outputs) .positron-notebook-editor-section {
@@ -201,4 +208,8 @@
201208
animation: none;
202209
opacity: 0;
203210
}
211+
212+
.positron-notebook-code-cell:has(.positron-notebook-outputs-section.no-outputs) .positron-notebook-editor-section:has(.positron-notebook-code-cell-footer.collapsed) .positron-notebook-editor-container {
213+
transition: none;
214+
}
204215
}

test/e2e/pages/notebooksPositron.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,18 @@ export class PositronNotebooks extends Notebooks {
11451145
});
11461146
}
11471147

1148+
/**
1149+
* Verify: Cell footer is collapsed (hidden via CSS animation).
1150+
* @param cellIndex - The index of the cell whose footer to check.
1151+
*/
1152+
async expectFooterNotVisible(cellIndex: number): Promise<void> {
1153+
await test.step(`Expect cell footer to be collapsed`, async () => {
1154+
const footer = this.cellFooterAtIndex(cellIndex);
1155+
await expect(footer).toHaveClass(/\bcollapsed\b/);
1156+
await expect(footer).not.toBeVisible();
1157+
});
1158+
}
1159+
11481160
/**
11491161
* Verify: Cell execution status matches expected status.
11501162
* @param cellIndex - The index of the cell to check.

test/e2e/tests/notebooks-positron/notebook-cell-execution-info.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,12 @@ test.describe('Positron Notebooks: Cell Execution Footer', {
8686
// ========================================
8787
// Cell 4: Footer visibility based on cell state
8888
// ========================================
89-
await test.step('Cell 4 - Footer visibility for never-run cell', async () => {
89+
await test.step('Cell 4 - Footer hidden for never-run cell', async () => {
9090
// Add a new cell but don't run it
9191
await notebooksPositron.addCodeToCell(4, 'print("never run")', { run: false });
9292

93-
// Footer should show "Not run this session" in aria-label
94-
await notebooksPositron.expectFooterAriaLabel(4, 'Cell not yet run');
93+
// Footer should not be rendered for cells that have never been run
94+
await notebooksPositron.expectFooterNotVisible(4);
9595
});
9696
});
9797
});

0 commit comments

Comments
 (0)