Skip to content

Commit ff88105

Browse files
fix(plugin-grid): list grid row actions trigger visible at rest in a muted tone (#28922)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbYXRisiPeY3Ds8vvkqmFfEg)_ Fixes #28871 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:列表表格「操作」列里每行的 ⋯ 按钮,静止时也画出来(灰色弱化色),鼠标移到该行、行内获得焦点、行被选中、按钮自己获得键盘焦点、菜单打开时,或在触屏上,变为正常前景色。菜单里有哪些项、各项的显示条件和顺序都不变。 - **为什么改**:表头「操作」一直在,但桌面端按钮在鼠标移上行之前是透明的,整列看起来是空的,读者会以为「没有可用操作」。 - **风险与回滚**:只改 `ui/packages/plugin-grid` 一个组件里那个按钮的样式类,不动 spec、服务端、元数据和菜单逻辑。弱化色用的是文字色(`text-muted-foreground`),不是透明度,对比度与其它次要图标一致。回滚 = revert 本 PR。 - **先看**:`ui/packages/plugin-grid/src/components/RowActionMenu.tsx` 里 `data-testid="row-action-trigger"` 那个按钮的 `className`;再看新测试 `RowActionMenu.atRest-28871.test.tsx`。 - **两分钟验证**:`cd ui && pnpm exec vitest run packages/plugin-grid/src/components/__tests__/RowActionMenu.atRest-28871.test.tsx`,应 1 个文件 4 个用例全部通过;或在 Console 打开线索列表(1440 px),不移动鼠标就能看到每行的 ⋯。 - **需要你做的**:无,席位落地。 ## What changes `RowActionMenu`'s ⋯ trigger (`data-testid="row-action-trigger"`, the one the list grid's `_actions` column draws per row): - **At rest:** drawn on every row, on every pointer type, in `text-muted-foreground`. The `opacity-0` / `transition-opacity` pair is gone, so nothing hides the trigger before the pointer reaches the row. - **Full strength (`text-foreground`)** on row hover (`group-hover/row`), focus within the row (`group-focus-within/row`), a selected row (`group-data-[state=selected]/row`), an open menu (`data-[state=open]`), its own keyboard focus (`focus-visible`) and a coarse pointer (`pointer-coarse`), which cannot hover. - The tone is a text colour, not an opacity, so the resting glyph keeps the same contrast as every other muted secondary icon. The ghost variant's own `hover:bg-accent hover:text-accent-foreground` still applies when the pointer is on the button itself. Unchanged: the menu's items, their gates and their order (`useRowActionPlan`, #28434), the inline primary buttons, the empty-menu guard (no trigger when the plan offers nothing), the header (`objectGridActionsColumn.tsx` is not touched), the 44 px touch target, and the accessible name. No key, option or prop is added. ## Tests - `RowActionMenu.atRest-28871.test.tsx` (new, 4 cases, `dom` project). jsdom applies no stylesheet, so visibility is read off the classes that decide it, as `data-table-leadingCell-12423.test.tsx` does: - at rest the trigger carries no `opacity-0` (under any variant), `invisible`, `hidden` or `sr-only`, and keeps its accessible name; - it rests in `text-muted-foreground` and carries the five full-strength variants; - the only `pointer-coarse:` class is `pointer-coarse:text-foreground`, so a coarse pointer strengthens the tone and is never what makes the trigger appear; - control: opening the menu flips the trigger's `data-state` to `open` (the state `data-[state=open]:` keys on) and shows the item. <details><summary>Ablation (scripts/ablation-replace.mjs, at 7d46751, source-resolved, no dist)</summary> - `opacity-0` put back at rest (`text-muted-foreground transition-colors` to `text-muted-foreground opacity-0 transition-colors`): 1 failed ("is drawn at rest: nothing hides it before the pointer reaches the row"), 3 passed. Restored, blob `adea0869` equal to HEAD, `git diff HEAD` empty. - The coarse-pointer leg put back to an opacity reveal (`pointer-coarse:text-foreground` to `pointer-coarse:opacity-100`): 1 failed (the every-pointer-type case), 3 passed. Restored, blob `adea0869` equal to HEAD, `git diff HEAD` empty. </details> ## Acceptance notes - `ui/packages/components/src/renderers/complex/data-table.tsx` draws its own row-actions trigger (`data-testid="data-table-row-actions"`, the generic `data-table` renderer's schema `rowActions`) with the old `opacity-0` set, pinned by `data-table-leadingCell-12423.test.tsx`. It is not the list grid's trigger and is outside this card's file surface, so it keeps hover-reveal. `ui/packages/app-shell/src/views/objectViewOpenAsPage.tsx` has a separate hover-revealed control in the same style, also untouched. - The enlarged related tab inherits this once it draws `object-grid` (#28865), with no change here. ## Verification All on the final head `7d467519` (base `origin/main` `b15b634c`), after `pnpm install --frozen-lockfile` and `turbo run build --filter='@object-ui/plugin-grid^...'` plus formula, sdui-parser, lint and client (49 tasks, exit 0), under `scripts/pm/os-verify-lock.sh`. - The whole `packages/plugin-grid/` vitest suite (`--maxWorkers=2`): 271 files, 2353 tests passed. The five `RowActionMenu*` files alone: 81 passed. - `pnpm --filter @object-ui/plugin-grid run type-check` (both `tsc` programs, the test one included): exit 0. - ESLint on the two changed tsx files: exit 0, no findings. - Derived gates (`node scripts/pm/dispatch-gates.mjs --commands`): 34 of 34 run, each exit 0 (`check:type-check-debt` and `pnpm --dir ui exec vitest run scripts/` under the lock; the latter 169 files passed, 2 skipped), including `node scripts/check-sdui-manifest.mjs` and the PR-body gates with this body as `GITHUB_EVENT_PATH`. `--ran` reconciles 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. `check-partof-closing-keyword` with `PR_BODY`: exit 0. `ui` `check:new-line-citations`: 0 new citations. - Not run locally: the other packages whose tests click `row-action-trigger` (plugin-view, plugin-designer, app-shell). They find the trigger by test id and never read its classes; CI's Test UI runs them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jSUCWFfGqhaRR97caT Co-authored-by: Claude <noreply@anthropic.com>
1 parent e0dc077 commit ff88105

3 files changed

Lines changed: 105 additions & 5 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/plugin-grid': patch
3+
---
4+
5+
fix(plugin-grid): the list grid's row ⋯ trigger is visible at rest, in a muted tone, instead of appearing only under the pointer
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The Actions column header is always drawn, but each row's ⋯ trigger was hidden by opacity on a fine pointer until the pointer reached the row, so a desktop list showed a headed column of cells that looked empty and read as "no actions available". The trigger is now drawn on every row at rest in the muted foreground tone, on every pointer type alike, and turns full strength on row hover, focus within the row, a selected row, its own keyboard focus, while its menu is open and on a coarse pointer. The menu's items, their gates and their order are unchanged. No key, option or prop is added.
11+
12+
What to do: nothing.

‎ui/packages/plugin-grid/src/components/RowActionMenu.tsx‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -724,11 +724,15 @@ export const RowActionMenu: React.FC<RowActionMenuProps> = (props) => {
724724
<Button
725725
variant="ghost"
726726
size="icon"
727-
// objectui#12423 — shown on row hover, on focus-within, on a
728-
// selected row, while its menu is open and on a coarse pointer;
729-
// hidden by OPACITY only, so it keeps its Tab stop and its name.
730-
// The `data-table` row menu trigger carries the same set.
731-
className="h-8 w-8 min-h-[44px] min-w-[44px] sm:min-h-0 sm:min-w-0 opacity-0 transition-opacity group-hover/row:opacity-100 group-focus-within/row:opacity-100 group-data-[state=selected]/row:opacity-100 data-[state=open]:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100"
727+
// Drawn on every row at rest, on every pointer type, in the muted
728+
// tone (objectstack#28871): a header over a column of cells that
729+
// look empty until the pointer reaches a row reads as "no actions".
730+
// Full strength on row hover, on focus-within, on a selected row,
731+
// on its own keyboard focus, while its menu is open and on a coarse
732+
// pointer, where no hover will ever lift it. The tone
733+
// is a text colour, never an opacity, so the resting glyph keeps
734+
// the muted-foreground contrast every other secondary icon has.
735+
className="h-8 w-8 min-h-[44px] min-w-[44px] sm:min-h-0 sm:min-w-0 text-muted-foreground transition-colors group-hover/row:text-foreground group-focus-within/row:text-foreground group-data-[state=selected]/row:text-foreground data-[state=open]:text-foreground focus-visible:text-foreground pointer-coarse:text-foreground"
732736
data-testid="row-action-trigger"
733737
>
734738
<MoreVertical className="h-4 w-4" />
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectstack#28871 — the row's ⋯ trigger is visible at rest.
11+
*
12+
* The list grid always draws its Actions header, so a trigger hidden until the
13+
* pointer reaches a row left a headed column of cells that looked empty, read
14+
* as "no actions available". The trigger is now drawn on every row at rest in
15+
* the muted tone, on every pointer type alike, and rises to full strength on
16+
* row hover, focus-within, a selected row, its own focus, an open menu and a
17+
* coarse pointer (which cannot hover).
18+
*
19+
* jsdom applies no stylesheet, so "visible" is read off the classes that decide
20+
* it: no `opacity-0` / `invisible` / `hidden` at rest, the muted tone as the
21+
* resting colour, and the reveal variants that lift it.
22+
*/
23+
import { describe, it, expect } from 'vitest';
24+
import { render, screen } from '@testing-library/react';
25+
import userEvent from '@testing-library/user-event';
26+
import '@testing-library/jest-dom';
27+
import React from 'react';
28+
import { PredicateScopeProvider } from '@object-ui/react';
29+
import { RowActionMenu } from '../RowActionMenu';
30+
31+
const ARCHIVE = { name: 'archive', label: 'Archive', variant: 'secondary' as const };
32+
33+
function renderMenu(props: Record<string, any> = {}) {
34+
return render(
35+
<PredicateScopeProvider scope={{}}>
36+
<RowActionMenu row={{ id: 'r1' }} onActionDef={() => {}} rowActionDefs={[ARCHIVE]} {...props} />
37+
</PredicateScopeProvider>,
38+
);
39+
}
40+
41+
describe('RowActionMenu trigger at rest (objectstack#28871)', () => {
42+
it('is drawn at rest: nothing hides it before the pointer reaches the row', () => {
43+
renderMenu();
44+
const trigger = screen.getByTestId('row-action-trigger');
45+
for (const hiding of ['opacity-0', 'invisible', 'hidden', 'sr-only']) {
46+
expect(trigger).not.toHaveClass(hiding);
47+
}
48+
// No class carrying an opacity of zero under any variant either: a hover
49+
// reveal written as `opacity-0 hover:opacity-100` is still empty at rest.
50+
expect(trigger.className.split(/\s+/).filter((c) => /(^|:)opacity-0$/.test(c))).toEqual([]);
51+
expect(trigger).toHaveAccessibleName();
52+
});
53+
54+
it('rests in the muted tone and lifts to full strength on hover, focus, selection and an open menu', () => {
55+
renderMenu();
56+
const trigger = screen.getByTestId('row-action-trigger');
57+
expect(trigger).toHaveClass(
58+
'text-muted-foreground',
59+
'group-hover/row:text-foreground',
60+
'group-focus-within/row:text-foreground',
61+
'group-data-[state=selected]/row:text-foreground',
62+
'data-[state=open]:text-foreground',
63+
'focus-visible:text-foreground',
64+
);
65+
});
66+
67+
it('is drawn at rest on every pointer type alike; a coarse pointer, which cannot hover, gets full strength', () => {
68+
renderMenu();
69+
const trigger = screen.getByTestId('row-action-trigger');
70+
const coarse = trigger.className.split(/\s+/).filter((c) => c.startsWith('pointer-coarse:'));
71+
// The coarse pointer only strengthens the tone; it is never the thing that
72+
// makes the trigger appear.
73+
expect(coarse).toEqual(['pointer-coarse:text-foreground']);
74+
});
75+
76+
it('control: the open menu marks the trigger open, the state the full-strength variant keys on', async () => {
77+
renderMenu();
78+
const trigger = screen.getByTestId('row-action-trigger');
79+
expect(trigger).toHaveAttribute('data-state', 'closed');
80+
await userEvent.click(trigger);
81+
expect(trigger).toHaveAttribute('data-state', 'open');
82+
expect(await screen.findByText('Archive')).toBeInTheDocument();
83+
});
84+
});

0 commit comments

Comments
 (0)