Skip to content

Commit 16c60ed

Browse files
yomybabyLablup Agentclaude
authored
fix(FR-2408): add missing BAINameActionCell component files (#6271)
Co-authored-by: Lablup Agent <agent@lablup.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3ae23ab commit 16c60ed

File tree

4 files changed

+1029
-0
lines changed

4 files changed

+1029
-0
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# BAINameActionCell — Reusable Table Cell Layout Component
2+
3+
> **Status**: Implemented
4+
> **Date**: 2026-03-28
5+
> **Issue**: [FR-2408](https://lablup.atlassian.net/browse/FR-2408)
6+
> **Implemented**: PR [#6247](https://github.com/lablup/backend.ai-webui/pull/6247)
7+
8+
## Overview
9+
10+
A reusable table cell layout component that combines a title area (icon + text) with responsive action buttons. When the column is wide enough, all action buttons are visible; as the column narrows, actions collapse one-by-one into a "more" overflow menu. This standardizes the repeated pattern of title + hover action buttons seen in `VFolderNodes` and other table components.
11+
12+
## Problem Statement
13+
14+
Currently, table components like `VFolderNodes` implement the title + action buttons pattern manually:
15+
16+
- Hover state management via `onCell` + `onMouseEnter/onMouseLeave` + `useState` (causes React re-renders)
17+
- Manual `Tooltip` wrapping for each action button
18+
- Manual `BAIFlex` layout for button alignment
19+
- No responsive overflow handling — action buttons overflow or get clipped when the column is narrow
20+
- Separate "controls" column required, wasting horizontal space
21+
22+
This leads to code duplication, inconsistent UX, and no responsive behavior for action buttons.
23+
24+
## User Stories
25+
26+
- **As a developer**, I want a declarative component where I define actions as a config array (key, title, icon, onClick, type) and the component handles layout, tooltips, hover visibility, and responsive overflow automatically.
27+
- **As a developer**, I want the title area to support navigation via `to` (React Router) or `onTitleClick`, with automatic ellipsis when space is limited.
28+
- **As a user**, I want to see action buttons when I hover over a table row, without them taking up space when I'm not interacting.
29+
- **As a user**, when the column is narrow, I want a "more" button that shows all available actions in a dropdown menu with icons and labels.
30+
31+
## Requirements
32+
33+
### Must Have
34+
35+
- [x] Reusable component (`BAINameActionCell`) placed in `packages/backend.ai-ui/src/components/Table/`
36+
- [x] Title area with optional icon, text content, and navigation support (`to` or `onTitleClick`)
37+
- [x] Title auto-ellipsis with tooltip on overflow (via `BAIText` / `BAILink`)
38+
- [x] Actions defined as a config array with: `key`, `title`, `icon`, `onClick`, `action` (async), `type` (default/danger), `disabled`, `disabledReason`
39+
- [x] CSS-only hover visibility (`opacity` + `pointer-events`) — no React state needed
40+
- [x] Keyboard accessibility via `:focus-within`
41+
- [x] ResizeObserver-based responsive overflow calculation with `requestAnimationFrame` debounce
42+
- [x] Actions collapse right-to-left into a "more" (`MoreOutlined`) dropdown menu
43+
- [x] Overflow menu shows all actions with icon + title (full discoverability)
44+
- [x] `showActions` prop: `'hover'` (default) or `'always'`
45+
- [x] `minVisibleActions` prop to guarantee minimum visible action count
46+
- [x] Danger-type actions styled with error colors in both button and menu form
47+
- [x] Disabled actions with `disabledReason` shown as tooltip
48+
- [x] Async `action` prop using `useTransition` for automatic loading state (mirrors `BAIButton.action`)
49+
- [x] Exported from `packages/backend.ai-ui/src/components/Table/index.ts`
50+
51+
### Nice to Have
52+
53+
- [ ] Integration example: migrate `VFolderNodes` name + controls columns into a single column using `BAINameActionCell`
54+
55+
## Component API
56+
57+
### BAINameActionCellAction
58+
59+
```typescript
60+
interface BAINameActionCellAction {
61+
key: string; // Unique key for React rendering
62+
title: string; // Tooltip on buttons, text in menu
63+
icon?: React.ReactNode; // Icon for both button and menu
64+
onClick?: () => void; // Sync click handler
65+
action?: () => Promise<void>; // Async handler with auto-loading (useTransition)
66+
type?: 'default' | 'danger'; // Visual style
67+
disabled?: boolean; // Disable the action
68+
disabledReason?: string; // Tooltip when disabled
69+
}
70+
```
71+
72+
### BAINameActionCellProps
73+
74+
```typescript
75+
interface BAINameActionCellProps {
76+
// Title area (left side)
77+
icon?: React.ReactNode; // Icon before title
78+
title?: React.ReactNode; // Title text or custom content
79+
to?: LinkProps['to']; // React Router navigation
80+
onTitleClick?: (e: React.MouseEvent) => void; // Title click handler
81+
82+
// Actions area (right side)
83+
actions?: BAINameActionCellAction[]; // Action definitions
84+
85+
// Behavior
86+
showActions?: 'hover' | 'always'; // Default: 'hover'
87+
minVisibleActions?: number; // Default: 0
88+
89+
// Styling
90+
style?: React.CSSProperties;
91+
className?: string;
92+
}
93+
```
94+
95+
## Layout Structure
96+
97+
```
98+
┌─────────────────────────────────────────────────────────┐
99+
│ [icon] Title text (ellipsis...) [📝] [🔗] [🗑️] [...] │
100+
│ ├── titleArea (flex: 1) ──────┤ ├── actionsArea ────┤ │
101+
└─────────────────────────────────────────────────────────┘
102+
```
103+
104+
- **titleArea**: `flex: 1, min-width: 0` — fills remaining space, ellipsis on overflow
105+
- **actionsArea**: `flex-shrink: 0` — fixed width, shown on hover
106+
107+
## Behavior Details
108+
109+
### Hover Visibility (CSS-only)
110+
111+
- Default state: `opacity: 0` + `pointer-events: none`
112+
- On `:hover` or `:focus-within`: `opacity: 1` + `pointer-events: auto`
113+
- Transition: `opacity 0.15s ease`
114+
- No React state management — zero re-renders for hover
115+
116+
### Responsive Overflow
117+
118+
- `ResizeObserver` monitors container width
119+
- `requestAnimationFrame` debounces calculation to prevent infinite loops
120+
- Actions collapse right-to-left (last action overflows first)
121+
- When overflow occurs, a `MoreOutlined` button appears
122+
- More menu contains **all** actions (not just overflowed), ensuring full discoverability
123+
124+
```
125+
Wide: [Edit] [Share] [Copy] [Delete]
126+
Medium: [Edit] [Share] [...]
127+
Narrow: [Edit] [...]
128+
Minimum: [...]
129+
```
130+
131+
### Title Rendering
132+
133+
| Props provided | Renders as | Behavior |
134+
|----------------------|--------------|----------------------------------|
135+
| `to` | `BAILink` | React Router link + ellipsis |
136+
| `onTitleClick` | `BAILink` | Clickable text + ellipsis |
137+
| Neither | `BAIText` | Plain text + tooltip on overflow |
138+
139+
### Action Rendering
140+
141+
| Context | Icon button | Overflow menu item |
142+
|-----------------|----------------------------------|---------------------------------|
143+
| Normal | `BAIButton type="text" size="small"` + Tooltip | Icon + title text |
144+
| Danger | `danger` prop on BAIButton | `danger: true` on menu item |
145+
| Disabled | `disabled` + `disabledReason` tooltip | `disabled` on menu item |
146+
| Async (`action`)| `BAIButton.action` (useTransition) | `startTransition` wrapper |
147+
148+
## Implementation Details
149+
150+
### Key Dependencies
151+
152+
| Component | Source | Usage |
153+
|------------|-------------------------------------|---------------------------------|
154+
| `BAIText` | `../BAIText` | Ellipsis with ResizeObserver |
155+
| `BAILink` | `../BAILink` | Navigation + ellipsis |
156+
| `BAIButton`| `../BAIButton` | Action buttons (async support) |
157+
| `Dropdown` | `antd` | Overflow menu |
158+
| `Tooltip` | `antd` | Action button labels |
159+
| `createStyles` | `antd-style` | CSS-in-JS (project convention) |
160+
161+
### Performance
162+
163+
- CSS-only hover: zero React re-renders for show/hide
164+
- `ResizeObserver` callback debounced via `requestAnimationFrame`
165+
- Cleanup on unmount: `ro.disconnect()` + `cancelAnimationFrame(rafId)`
166+
- `visibleCount` reset when `actions` array length changes
167+
- `'use memo'` directive for React Compiler optimization
168+
169+
### Accessibility
170+
171+
- `:focus-within` ensures keyboard users can discover actions via Tab
172+
- `aria-label="More actions"` on the overflow button
173+
- antd `Dropdown` + `Menu` provides `role="menu"` and `role="menuitem"` automatically
174+
- Disabled actions communicate reason via tooltip
175+
176+
## Storybook Stories
177+
178+
| Story | Description |
179+
|--------------------|----------------------------------------------------|
180+
| Basic | Icon + title + 4 actions (edit, share, copy, delete)|
181+
| WithNavigation | `to` prop for React Router link |
182+
| AlwaysShowActions | `showActions: 'always'` |
183+
| WithDisabledAction | Disabled action with `disabledReason` |
184+
| LongTitle | Narrow container (300px), ellipsis behavior |
185+
| ResponsiveOverflow | Interactive slider to resize container |
186+
| TitleOnly | No actions, title only |
187+
| AsyncAction | `action` prop with 2s delay, loading spinner |
188+
189+
## Usage Example
190+
191+
```tsx
192+
// VFolderNodes — replacing separate name + controls columns
193+
{
194+
key: 'name',
195+
title: t('data.folders.Name'),
196+
render: (_name, vfolder) => (
197+
<BAINameActionCell
198+
icon={<VFolderNodeIdenticon vfolderNodeIdenticonFrgmt={vfolder} />}
199+
title={vfolder.name}
200+
to={generateFolderPath(toLocalId(vfolder?.id))}
201+
actions={[
202+
{
203+
key: 'share',
204+
title: t('button.Share'),
205+
icon: <BAIShareAltIcon />,
206+
onClick: () => setInviteFolderId(toLocalId(vfolder?.id)),
207+
},
208+
{
209+
key: 'delete',
210+
title: t('data.folders.MoveToTrash'),
211+
icon: <BAITrashBinIcon />,
212+
type: 'danger',
213+
onClick: () => handleDelete(vfolder),
214+
},
215+
]}
216+
/>
217+
),
218+
}
219+
```
220+
221+
## File Manifest
222+
223+
| File | Action |
224+
|------|--------|
225+
| `packages/backend.ai-ui/src/components/Table/BAINameActionCell.tsx` | Created |
226+
| `packages/backend.ai-ui/src/components/Table/BAINameActionCell.stories.tsx` | Created |
227+
| `packages/backend.ai-ui/src/components/Table/index.ts` | Modified (export added) |

0 commit comments

Comments
 (0)