fix: suppress title previews during Kanban multi-select (#3919)

This commit is contained in:
Carlos Florêncio
2026-09-25 10:31:14 +01:00
committed by GitHub
parent 04b84aa9ef
commit 567b965be0
10 changed files with 346 additions and 6 deletions
+1 -1
View File
@@ -275,7 +275,7 @@ export function KanbanCardShell({
<KanbanCardBody
task={task}
repositoryChips={repositoryChips ?? []}
enableTitleHover
enableTitleHover={!isMultiSelectMode}
actions={
<KanbanCardActionSlot
isMultiSelectMode={isMultiSelectMode}
@@ -208,6 +208,21 @@ describe("KanbanCard — title-preview disclosure widening (AC-UI-PIPELINE-ROW-0
});
});
describe("KanbanCard — title preview in multi-select mode", () => {
// @covers AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.2
it.each([
["unselected", false],
["selected", true],
] as const)("does not mount a title preview for a %s card", (_label, isSelected) => {
renderCard(baseTask({ description: "Some description content" }), [], {
isMultiSelectMode: true,
isSelected,
});
expect(screen.queryByTestId("task-title-preview-trigger")).toBeNull();
});
});
// @covers AC-TASKS-MOBILE-KANBAN-SCROLL-001.1, AC-TASKS-MOBILE-KANBAN-SCROLL-001.4
it.each(["Enter", " "])(
"phone cards open with %j without exposing or invoking drag pickup",
@@ -84,9 +84,11 @@ function RowMenuTrigger({
function RowInfoColumn({
task,
repositoryChips,
isMultiSelectMode,
}: {
task: Task;
repositoryChips: ReturnType<typeof resolveTaskRepositoryChips>;
isMultiSelectMode?: boolean;
}) {
const { t } = useTranslation();
const sessionCount = task.sessionCount ?? 0;
@@ -94,7 +96,7 @@ function RowInfoColumn({
return (
<div className="w-[200px] min-w-0 shrink-0" data-testid="pipeline-row-info">
<div data-testid="pipeline-row-title">
<CardTitle task={task} enableTitleHover />
<CardTitle task={task} enableTitleHover={!isMultiSelectMode} />
</div>
{/* Height is reserved whether or not the task has a repository, so a
row with no repository is the same height as one with, and the
@@ -259,7 +261,11 @@ export function PipelineRow({
/>
</div>
)}
<RowInfoColumn task={task} repositoryChips={repositoryChips} />
<RowInfoColumn
task={task}
repositoryChips={repositoryChips}
isMultiSelectMode={isMultiSelectMode}
/>
<div
ref={overflowStage.outerRef}
data-testid="pipeline-row-overflow-region"
@@ -135,13 +135,13 @@ function renderKanbanCard() {
);
}
function renderPipelineRow() {
function renderPipelineRow(task: Task = TASK, isMultiSelectMode = false) {
return render(
<ToastProvider>
<StateProvider initialState={SHARED_INITIAL_STATE}>
<TooltipProvider delayDuration={0}>
<Graph2TaskPipeline
task={TASK}
task={task}
steps={STEPS}
moveTargetSteps={STEPS}
workspaceId={WORKSPACE_ID}
@@ -151,6 +151,7 @@ function renderPipelineRow() {
onPreviewTask={() => undefined}
onOpenTask={() => undefined}
onDeleteTask={() => undefined}
isMultiSelectMode={isMultiSelectMode}
/>
</TooltipProvider>
</StateProvider>
@@ -158,6 +159,22 @@ function renderPipelineRow() {
);
}
describe("Pipeline title preview in multi-select mode", () => {
// @covers AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.2
it.each([
["outside", false, true],
["inside", true, false],
] as const)("is %s multi-select mode", (_mode, isMultiSelectMode, previewEnabled) => {
renderPipelineRow({ ...TASK, parentTaskId: "parent-1" }, isMultiSelectMode);
if (previewEnabled) {
expect(screen.getByTestId("task-title-preview-trigger")).not.toBeNull();
} else {
expect(screen.queryByTestId("task-title-preview-trigger")).toBeNull();
}
});
});
async function openMenuAndSnapshot() {
const trigger = screen.getByRole("button", { name: t("kanban:moreOptions") });
fireEvent.pointerDown(trigger, { button: 0, pointerId: 1 });
@@ -141,4 +141,61 @@ test.describe("Task title hover card on the Kanban card", () => {
await title.click();
await expect(testPage).toHaveURL(new RegExp(`/t/${parent.id}`));
});
// @covers AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.2, AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.3, AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.4
test("multi-select closes and suppresses title previews, then restores them when it ends", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
const { parent } = await seedParentWithSubtasks(
apiClient,
seedData,
"Preview selection parent",
);
const sibling = await apiClient.createTask(seedData.workspaceId, "Preview selection sibling", {
description: "Second preview eligible task",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
const kanban = new KanbanPage(testPage);
await kanban.goto();
const parentCard = kanban.taskCard(parent.id);
const siblingCard = kanban.taskCard(sibling.id);
await expect(parentCard).toBeVisible({ timeout: 45_000 });
await expect(siblingCard).toBeVisible({ timeout: 45_000 });
const parentTitle = parentCard.getByTestId("task-card-title");
await parentTitle.hover();
const hoverCard = testPage.getByTestId("task-title-hover-card");
await expect(hoverCard).toBeVisible();
// Modifier-click enters selection mode while the pointer remains on the
// open title preview, so this proves the mode transition itself closes it.
await parentTitle.click({ modifiers: ["ControlOrMeta"] });
await expect(kanban.multiSelectToolbar).toContainText("1 selected");
await expect(hoverCard).toHaveCount(0);
const siblingTitle = siblingCard.getByTestId("task-card-title");
await siblingTitle.hover();
await dwell(
testPage,
300,
"negative-assertion",
"multi-select must suppress a preview on an unselected title",
);
await expect(hoverCard).toHaveCount(0);
await siblingTitle.click();
await expect(kanban.multiSelectToolbar).toContainText("2 selected");
await expect(kanban.board).toBeVisible();
await expect(testPage).not.toHaveURL(new RegExp(`/t/${sibling.id}`));
await kanban.multiSelectToggle.first().click();
await expect(kanban.multiSelectToolbar).not.toBeVisible();
await parentTitle.hover();
await expect(hoverCard).toBeVisible();
});
});
@@ -0,0 +1,94 @@
---
created: 2026-09-24
status: implemented
requirements:
- REQ-TASKS-RICH-TASK-TITLE-PREVIEWS-001
system_design:
- ../../specs/tasks/system-design/rich-task-title-previews.md
legacy_specs: []
---
# Implementation plan: Suppress Kanban title previews during multi-select
## Overview
Kanban card titles currently keep an interactive preview while multi-select is active. The popover can cover adjacent cards and intercept selection clicks. One focused work order passes the existing selection state to the title-preview gates in both column cards and Pipeline rows, then proves the mode transition and selection path in component and browser tests.
## Scope
### In scope
- Close an open Kanban title preview when multi-select starts.
- Prevent pointer and keyboard title previews for selected and unselected cards during multi-select.
- Let title clicks select or deselect a card, and restore ordinary previews when selection mode ends.
### Out of scope
- GitHub PR or GitLab MR badge hovers, task preview panels, sidebar titles, and bulk action semantics.
- New backend or persisted selection state.
## Technical approach
`apps/web/components/kanban-card-content.tsx` already suppresses card actions when `isMultiSelectMode` is true. In `KanbanCardShell`, pass the same state to the `enableTitleHover` prop of `KanbanCardBody`. The Pipeline view has a separate `PipelineRow` → `RowInfoColumn` → `CardTitle` path, so pass its existing mode through that row and disable the same hover prop. Keep `CardTitle` and the shared `TaskTitleHoverCard` unchanged; their existing conditional render removes the trigger and closes an open popover. Retain the current click dispatch paths for title selection and existing coarse-pointer navigation.
## ASCII UI preview
`UI-01: Kanban card title`, desktop, after hovering a previewable title.
```text
Before, selection on: After, selection on:
[x] Long task title... [x] Long task title...
+---------------------------+ (no title popover)
| Full title and subtasks | Click title -> toggle selection
| covers the next card |
+---------------------------+
[ ] Next task [ ] Next task
```
Turning selection on closes an already open preview. Turning it off restores the ordinary desktop preview. The checkbox, title, and card remain in the current order; no new control or copy is added. This structure is required by `AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.2` through `.4`; ASCII spacing is illustrative.
`UI-02: Phone Kanban card`, coarse pointer, unchanged composition.
```text
[ ] Long task title...
Tap title -> existing card action
No hover preview
```
The existing phone Kanban card is the mobile exemplar. Its card content remains the touch navigation or selection target according to the active mode; there is no separate hover surface, extra scroll owner, or change to safe-area and touch-target geometry. The mobile regression check covers direct title navigation.
## Tests
| Criterion | Evidence |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.2` and `.4` | `apps/web/components/kanban-card-regression.test.tsx`: full card trigger absent in multi-select and present when mode ends. |
| `.2` | `apps/web/components/kanban/pipeline-kanban-shared-source.test.tsx`: Pipeline title trigger is absent in multi-select and present outside it. |
| `AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.3` | `apps/web/components/kanban-card-click.test.ts` already checks click dispatch; browser test below covers an actual title click. |
## E2E tests
| Criterion | Project and path | Scenario |
| ------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `.2` and `.3` | Chromium, `apps/web/e2e/tests/kanban/task-title-hover-subtasks.spec.ts` | Open a title preview, enable multi-select, confirm it closes and stays absent on hover, click a card title, and verify selection count without navigation. |
| `.4` | Chromium, same spec | End multi-select and verify the preview opens again. |
| `.4` | Mobile Chrome, existing `apps/web/e2e/tests/kanban/mobile-task-title-hover-subtasks.spec.ts` | Direct title tap navigates without a hover preview. |
## Work orders
- [x] [Task 01: Guard Kanban title previews in selection mode](task-01-guard-title-preview.md)
## Verification results
- `pnpm exec vitest run components/kanban-card-regression.test.tsx components/kanban-card-click.test.ts` - passed (2 files, 18 tests).
- `pnpm run typecheck` - passed.
- `pnpm e2e:run --host --project chromium tests/kanban/task-title-hover-subtasks.spec.ts` - passed (4 tests; managed host build and production Vite bundle).
- `pnpm e2e:run --host --project mobile-chrome tests/kanban/mobile-task-title-hover-subtasks.spec.ts` - passed (1 test; managed host build and production Vite bundle).
- Pipeline review fix: `pnpm exec vitest run components/kanban-card-regression.test.tsx components/kanban-card-click.test.ts components/kanban/pipeline-kanban-shared-source.test.tsx` - passed (3 files, 22 tests).
- `pnpm run typecheck` after the Pipeline guard - passed.
- `python3 scripts/list-docs.py validate` and `python3 scripts/lint-spec-files.py --all` after the design update - passed.
## Risks
- Switching mode while the preview owns keyboard focus must close it without leaving an overlay that intercepts input.
- The guard must affect only Kanban title previews; badge hovers and task navigation follow their existing contracts.
- The migrated preview requirement describes subtask-only eligibility, while current card tests also cover descriptions. The new suppression criterion applies to every Kanban title preview; this fix does not change normal-mode eligibility.
@@ -0,0 +1,112 @@
---
id: "01-guard-title-preview"
title: "Guard Kanban title previews in selection mode"
status: done
wave: 1
depends_on: []
plan: "plan.md"
requirements:
- REQ-TASKS-RICH-TASK-TITLE-PREVIEWS-001
acceptance_criteria:
- AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.2
- AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.3
- AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.4
system_design:
- ../../specs/tasks/system-design/rich-task-title-previews.md
---
# Task 01: Guard Kanban title previews in selection mode
## Summary
Suppress the Kanban title preview while multi-select is active so its popover cannot block nearby cards. Preserve title clicks as selection actions and restore the preview after leaving multi-select.
## In scope
- Gate `KanbanCardBody` title preview with the already supplied `isMultiSelectMode` prop in `KanbanCardShell`, and gate the Pipeline row's `CardTitle` through `RowInfoColumn`.
- Add a failing component regression before the production change, then a browser regression for closing the preview and selecting through a title.
- Check the existing phone title-navigation scenario after the change.
- Add a Pipeline-row regression proving its title preview is disabled only while multi-select is active.
## Out of scope
- Shared `TaskTitleHoverCard` behavior on other surfaces, PR/MR badge hovers, and new selection state.
## Acceptance
1. Enabling multi-select removes the Kanban title preview trigger and closes an open title popover for any card.
2. Hovering or focusing a title cannot reopen the preview in multi-select; clicking the title changes selection without task navigation.
3. Disabling multi-select restores the preview, and coarse-pointer direct title navigation still works.
## ASCII UI preview
`UI-01: Kanban card title`, desktop selection mode, excerpt from the [full plan](plan.md#ascii-ui-preview).
```text
[x] Long task title... no title popover
[ ] Next task remains clickable
```
`UI-02: Phone Kanban card`, coarse pointer, unchanged.
```text
[ ] Long task title... tap uses existing card action
```
These structural outcomes map to `AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.2` through `.4`. Spacing is illustrative; no new control or copy is required.
## Verification
```bash
cd apps/web && pnpm exec vitest run components/kanban-card-regression.test.tsx components/kanban-card-click.test.ts
cd apps/web && pnpm exec vitest run components/kanban/pipeline-kanban-shared-source.test.tsx
cd apps/web && pnpm run typecheck
cd apps/web && pnpm e2e:run --host --project chromium tests/kanban/task-title-hover-subtasks.spec.ts
cd apps/web && pnpm e2e:run --host --project mobile-chrome tests/kanban/mobile-task-title-hover-subtasks.spec.ts
```
The managed E2E runner must rebuild the web bundle before Playwright runs against the Go backend. Record each result here.
## Files likely touched
- `apps/web/components/kanban-card-content.tsx`
- `apps/web/components/kanban-card-regression.test.tsx`
- `apps/web/e2e/tests/kanban/task-title-hover-subtasks.spec.ts`
- `apps/web/components/kanban/graph2-task-pipeline-row.tsx`
- `apps/web/components/kanban/pipeline-kanban-shared-source.test.tsx`
## Dependencies
None.
## Risks
- A preview open when mode changes must unmount cleanly and release focus or pointer interception.
- The title click must bubble to the card's selection handler without a nested preview trigger consuming it.
## Parallelism
`sequential`
## Inputs
- [Rich task title preview requirements](../../specs/tasks/requirements/rich-task-title-previews.md).
- [Rich task title preview system design](../../specs/tasks/system-design/rich-task-title-previews.md).
- Existing `KanbanCardShell`, `CardTitle`, and Kanban title-hover tests.
## Results
Implemented the selection-mode guard by passing `!isMultiSelectMode` to `KanbanCardBody.enableTitleHover`. The component test failed for selected and unselected cards before the production change. The desktop browser regression proved that entering selection mode closes the active preview, title clicks select without navigation, and the preview returns after selection mode ends.
The review also identified the separate Pipeline row renderer. It now passes `isMultiSelectMode` through `RowInfoColumn` to gate its title preview. The system design and this work order now describe both title-rendering paths.
- RED: `pnpm exec vitest run components/kanban-card-regression.test.tsx` failed on both new selection-mode cases before the production change.
- `pnpm exec vitest run components/kanban-card-regression.test.tsx components/kanban-card-click.test.ts` - passed (2 files, 18 tests).
- `pnpm run typecheck` - passed.
- `pnpm e2e:run --host --project chromium tests/kanban/task-title-hover-subtasks.spec.ts` - passed (4 tests; managed host build and production Vite bundle).
- `pnpm e2e:run --host --project mobile-chrome tests/kanban/mobile-task-title-hover-subtasks.spec.ts` - passed (1 test; managed host build and production Vite bundle).
- RED: `pnpm exec vitest run components/kanban/pipeline-kanban-shared-source.test.tsx -t 'Pipeline title preview in multi-select mode'` failed because the Pipeline title trigger remained mounted in multi-select.
- `pnpm exec vitest run components/kanban/pipeline-kanban-shared-source.test.tsx -t 'Pipeline title preview in multi-select mode'` - passed (2 tests).
- `pnpm exec vitest run components/kanban-card-regression.test.tsx components/kanban-card-click.test.ts components/kanban/pipeline-kanban-shared-source.test.tsx` - passed (3 files, 22 tests).
- `pnpm run typecheck` after the Pipeline guard - passed.
- `python3 scripts/list-docs.py validate` and `python3 scripts/lint-spec-files.py --all` after the design update - passed.
+1
View File
@@ -171,6 +171,7 @@ signals, and task-scoped scheduling contracts.
- [MCP Tool Name Stability](system-design/mcp-tool-name-stability.md)
- [MCP workspace modes](system-design/mcp-workspace-mode.md)
- [Remote Contribution Tasks](system-design/remote-contribution-tasks.md)
- [Rich task title previews](system-design/rich-task-title-previews.md)
- [Branch history explanations](system-design/branch-history-explanations.md)
- [Passthrough Queued Prompt Dispatch](system-design/passthrough-queued-prompt-dispatch.md)
- [Saved Prompt Delivery](system-design/saved-prompt-delivery.md)
@@ -5,6 +5,7 @@ created: 2026-08-18
owners:
- tbd
---
# Rich task title previews Requirements
## Overview
@@ -20,6 +21,9 @@ This document is the migrated task-system source for the capability. The source
#### Acceptance criteria
- **AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.1:** When a consumer uses this capability, the system shall provide the observable behavior and exclusions documented below.
- **AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.2:** While Kanban multi-select is enabled, pointing at or focusing any card title shall not open its title preview, whether that card is selected or unselected.
- **AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.3:** Enabling Kanban multi-select while a title preview is open shall close that preview; clicking a card title in multi-select shall select or deselect the card without opening the preview or navigating to the task.
- **AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.4:** Disabling Kanban multi-select shall restore the normal fine-pointer title preview behavior. Coarse-pointer task navigation shall remain available without a hover preview.
## Migrated source detail
@@ -41,6 +45,7 @@ value and competes with those surfaces' navigation.
- A keyboard user can focus the title trigger and open the same preview.
- A keyboard user can open a subtask without opening the parent task.
- A coarse-pointer device keeps the title's direct navigation action.
- Kanban multi-select suppresses title previews so the selection controls and nearby cards remain reachable.
- On a task page, the mobile task switcher gives access to the same task tree.
- GitHub and GitLab status summaries use one provider-neutral presentation contract.
@@ -99,4 +104,5 @@ Provider-specific code supplies labels, icons, test identifiers, and status rows
## Implementation plan
See [the implementation plan](../../../plans/rich-task-title-previews/plan.md).
See [the implementation plan](../../../plans/rich-task-title-previews/plan.md).
The Kanban multi-select correction is tracked in the [preview suppression fix plan](../../../plans/kanban-multi-select-title-preview/plan.md).
@@ -0,0 +1,32 @@
---
status: current
system: tasks
requirements:
- REQ-TASKS-RICH-TASK-TITLE-PREVIEWS-001
---
# Rich task title previews system design
## Purpose and boundaries
The task system owns the task-title preview contract and its interaction with Kanban selection. This design covers the selection-mode guard on that preview. The existing Kanban selection state, card click dispatch, and coarse-pointer navigation remain the sources of truth for their respective actions. No backend, API, or persisted state changes are needed.
## Requirement mapping
| Requirement | Design section |
| ---------------------------------------- | ------------------------------------------------------------- |
| `REQ-TASKS-RICH-TASK-TITLE-PREVIEWS-001` | [Selection-mode preview guard](#selection-mode-preview-guard) |
## Selection-mode preview guard
`KanbanCardShell` already receives `isMultiSelectMode` and forwards card clicks to selection handling. It also renders `KanbanCardBody`, whose `enableTitleHover` prop controls whether `CardTitle` mounts `TaskTitleHoverCard`. The shell disables that prop when multi-select is active. The title remains visible as plain card content so a click reaches the card's existing selection handler.
The Pipeline view has a separate `PipelineRow` path. It passes the same mode into `RowInfoColumn`, which disables `CardTitle` hover when selection is active. This keeps the shared requirement true for every Kanban card presentation.
When multi-select starts, unmounting `TaskTitleHoverCard` closes any open preview, including its pointer and keyboard trigger. When multi-select ends, the title can mount its normal preview again. This uses the existing component lifecycle and does not add a second selection state or change the shared preview component for other consumers.
The same shell serves responsive Kanban cards. Coarse pointers already render the title as direct card content, so the guard preserves the phone task-navigation path and adds no touch-only disclosure.
## Verification boundary
A component regression should prove that selection mode removes the trigger on both the column-card and Pipeline-row paths and that it returns after the mode ends. A desktop browser regression should open a column-card preview, enable multi-select, verify the preview closes and does not reopen on hover, and select a card through its title. Existing mobile Kanban coverage should continue to prove direct title navigation without a hover preview.