Skip to content

Commit 3ed9cfe

Browse files
committed
feat(ui): add title and testid slots to text
1 parent 7457c7f commit 3ed9cfe

2 files changed

Lines changed: 72 additions & 1 deletion

File tree

‎app/src/ui/primitives/Text.test.tsx‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,44 @@ describe("Text 语义与透传", () => {
128128
expect(container.innerHTML).not.toMatch(/\b(?:rgb|hsl)a?\(/);
129129
});
130130
});
131+
132+
/**
133+
* 两个**属性级**受控槽(批 4 T16-A 裁决:切片内 `data-testid` 与 `title` 各 ≥3 处缺口,
134+
* 按 B6 阈值回来改原语)。每槽**正反两向**都要钉:传了就落属性、**不传时不得出现该属性** ——
135+
* 只钉正向会让「原语偷偷给所有文本加一个空 title」这种回潮静默通过(空 title 会吃掉子元素的
136+
* tooltip 继承,是真实可观测的行为差异)。
137+
*/
138+
describe("Text 属性级受控槽(testId / title)", () => {
139+
it("testId ⇒ 渲染 data-testid;**不传 ⇒ 属性根本不出现**", () => {
140+
const on = render(<Text testId="notes-empty-hint">x</Text>);
141+
expect(root(on.container).getAttribute("data-testid")).toBe("notes-empty-hint");
142+
const off = render(<Text>x</Text>);
143+
expect(root(off.container).hasAttribute("data-testid")).toBe(false);
144+
// 反向:`title` 也在场时不得顺带产出 testid(两槽互不牵连)
145+
const onlyTitle = render(<Text title="提示">x</Text>);
146+
expect(root(onlyTitle.container).hasAttribute("data-testid")).toBe(false);
147+
});
148+
149+
it("title ⇒ 渲染原生 tooltip;**不传 ⇒ 属性根本不出现**", () => {
150+
const on = render(<Text title="共 12 条,只显示前 5">x</Text>);
151+
expect(root(on.container).getAttribute("title")).toBe("共 12 条,只显示前 5");
152+
const off = render(<Text>x</Text>);
153+
expect(root(off.container).hasAttribute("title")).toBe(false);
154+
expect(root(off.container).getAttribute("title")).toBeNull();
155+
});
156+
157+
it("两槽是**属性级**:不改变标签、类名序列,也不落任何排版内联 style", () => {
158+
const { container } = render(<Text testId="t" title="p" as="p" tone="ink-3">x</Text>);
159+
const el = root(container);
160+
expect(el.tagName).toBe("P");
161+
expect(el.className).toBe("ed-text ed-text--s4 ed-text--ink-3 ed-text--font-ui");
162+
expect(el.style.color).toBe("");
163+
expect(el.style.fontSize).toBe("");
164+
});
165+
166+
it("空串是**显式传入**而不是缺席:`testId=\"\"` 仍落一个空属性(契约是 undefined 才算未传)", () => {
167+
const { container } = render(<Text testId="">x</Text>);
168+
expect(root(container).hasAttribute("data-testid")).toBe(true);
169+
expect(root(container).getAttribute("data-testid")).toBe("");
170+
});
171+
});

‎app/src/ui/primitives/Text.tsx‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
* 变为**块级**(`display: block`)—— 放在 `<p>` 等内联语境时由调用点负责;③ `as` 决定语义标签,
1616
* 本组件不额外包裹任何 DOM(不制造「多一层 div」的排版权威);④ `ink-4` 是过渡态,
1717
* 不得承载唯一关键信息,剪报底纹上禁止使用(见 `Text.css` 的墨度边界注释)。
18+
*
19+
* ★ 属性级受控槽(批 4 T16-A 裁决):`testId` / `title` 两个 —— 见 `TextProps` 里的理由。
20+
* **明确不加** `role` / `onClick` / `aria-*` / `id` / 任意 DOM 透传(`...rest`):可交互文本
21+
* 不是 `Text` 的职责,是 `Button` 的域(批 5/7);开一个 `...rest` 就等于把「谁能改这个元素的
22+
* 行为与语义」重新交回调用点,ADR-033 §4 的「调用点不掌握排版权威」会在这一步失效。
1823
*/
1924

2025
import type { CSSProperties, ReactElement, ReactNode } from "react";
@@ -46,6 +51,22 @@ export interface TextProps {
4651
truncate?: boolean;
4752
/** 语义标签,默认 `span` */
4853
as?: TextTag;
54+
/**
55+
* 渲染 `data-testid`(探针锚点)。
56+
*
57+
* Why 开这一槽:批 4 T16-A 的探针实测切片内有 **≥3 处**弱化文本元素带着 `data-testid`,
58+
* 而 `TextProps` 表达不了 ⇒ 按 B6 阈值(同一缺口 ≥3 处共用 ⇒ 回来改原语)补槽。
59+
* ★ 它是**属性级**、不是排版级:`testId` 不影响任何排版或墨度决策,因此**不**打开
60+
* 「调用点重新掌握排版权威」的口子(ADR-033 §4);同理 `title`。二者是唯一被批准的两槽。
61+
*/
62+
testId?: string;
63+
/**
64+
* 原生 `title`(悬停 tooltip)。
65+
*
66+
* Why:同上 —— 探针实测 ≥3 处弱化文本带 `title`。**要不要 tooltip 是内容决策(调用点的)**,
67+
* 怎么显示是浏览器的事 ⇒ 原语只负责把值送到 DOM,不做任何包装 / 自绘气泡。
68+
*/
69+
title?: string;
4970
/** 追加类名(调用点只做定位,不参与排版权威 —— 排版与墨度一律由本原语的类决定) */
5071
className?: string;
5172
/** 透传内联样式:批 6 的动效接缝(`letterSpacing` / `color`)与调用点布局用 */
@@ -65,6 +86,8 @@ export function Text({
6586
font = "ui",
6687
truncate = false,
6788
as = "span",
89+
testId,
90+
title,
6891
className,
6992
style,
7093
children,
@@ -81,8 +104,15 @@ export function Text({
81104
.join(" ");
82105

83106
const Tag = as;
107+
// 未传 ⇒ 属性**根本不出现**(不是 `undefined` 字符串):`data-testid={undefined}` 在 React 里
108+
// 恰好也不落属性,但显式三元把这条契约写在源码里,`Text.test.tsx` 的反向用例钉的就是它。
84109
return (
85-
<Tag className={cls} style={style}>
110+
<Tag
111+
className={cls}
112+
style={style}
113+
data-testid={testId === undefined ? undefined : testId}
114+
title={title === undefined ? undefined : title}
115+
>
86116
{children}
87117
</Tag>
88118
);

0 commit comments

Comments
 (0)