@qijenchen/design-system 0.1.0-beta.89 → 0.1.0-beta.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Breadcrumb/breadcrumb.d.ts +1 -1
- package/dist/components/Breadcrumb/breadcrumb.js.map +1 -1
- package/dist/components/DataTable/data-table.js +1 -1
- package/dist/components/DataTable/data-table.js.map +1 -1
- package/dist/components/DatePicker/date-picker.d.ts.map +1 -1
- package/dist/components/DatePicker/date-picker.js +2 -1
- package/dist/components/DatePicker/date-picker.js.map +1 -1
- package/dist/components/Tag/tag.d.ts.map +1 -1
- package/dist/components/Tag/tag.js +1 -2
- package/dist/components/Tag/tag.js.map +1 -1
- package/dist/hooks/use-truncated.d.ts.map +1 -1
- package/dist/hooks/use-truncated.js.map +1 -1
- package/llms-full.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/src/components/Breadcrumb/breadcrumb.stories.tsx +3 -0
- package/src/components/Breadcrumb/breadcrumb.tsx +5 -5
- package/src/components/DataTable/data-table.tsx +6 -6
- package/src/components/DatePicker/date-picker.tsx +9 -2
- package/src/components/Slider/slider.stories.tsx +1 -1
- package/src/components/Tag/tag.tsx +7 -3
- package/src/hooks/use-truncated.ts +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-truncated.js","sources":["../../src/hooks/use-truncated.ts"],"sourcesContent":["import * as React from 'react'\n\n// ── useTruncated — 單行文字截斷偵測引擎(SSOT)─────────────────────────────────\n// 收斂 Breadcrumb TruncatedLabel / DataTable TruncateCell
|
|
1
|
+
{"version":3,"file":"use-truncated.js","sources":["../../src/hooks/use-truncated.ts"],"sourcesContent":["import * as React from 'react'\n\n// ── useTruncated — 單行文字截斷偵測引擎(SSOT)─────────────────────────────────\n// 收斂 Breadcrumb TruncatedLabel / DataTable TruncateCell(兩處 module-level shared ResizeObserver 引擎\n// **逐字重複**,僅變數名不同)+ Tag(原 per-instance `new ResizeObserver`)三處的截斷偵測 + isTruncated\n// 狀態機為唯一 owner(M17 SSOT);Tag 亦收斂進共用 shared RO,行為等價。\n//\n// 世界級對照(M8):MUI/MUI-X 官方無第一方 primitive,社群共識 = 「hook 量測 scrollWidth>clientWidth\n// + ref + 以 Tooltip `open` prop 條件控制」(issue #37211);此 hook 即該低階量測層,\n// 上層 `<TruncatedText>`(patterns/element-anatomy)對應 Ant `Typography.Text ellipsis` 高階元件。\n//\n// 三處變異點皆以 options 注入,**行為零漂移**:\n// - Breadcrumb / DataTable:預設 scrollWidth>clientWidth。\n// - Tag:trigger≠量測元素(observe root、量測內層 span)+ Canvas measureText(flex 內 scrollWidth\n// 不可靠)+ useLayoutEffect(避免 paint flash)+ deps=[children] → 全走 options。\n\ntype RoCallback = (entry: ResizeObserverEntry) => void\n\nlet sharedRO: ResizeObserver | null = null\nconst sharedROCallbacks = new WeakMap<Element, RoCallback>()\n\n// 全 DS 共用單一 ResizeObserver,dispatch 到 per-element callback(2026-04-22 DataTable D3 perf audit\n// 成果:10 col × 100 row = 1 RO 而非 1000)。element 卸載時 cleanup,singleton RO 本身不 disconnect。\nfunction getSharedRO(): ResizeObserver {\n if (sharedRO) return sharedRO\n sharedRO = new ResizeObserver((entries) => {\n entries.forEach((e) => {\n const cb = sharedROCallbacks.get(e.target)\n if (cb) cb(e)\n })\n })\n return sharedRO\n}\n\nfunction observeShared(el: Element, cb: RoCallback): () => void {\n const obs = getSharedRO()\n sharedROCallbacks.set(el, cb)\n obs.observe(el)\n return () => {\n sharedROCallbacks.delete(el)\n obs.unobserve(el)\n }\n}\n\nexport interface UseTruncatedOptions {\n /**\n * 偵測策略:回傳 `true` = 截斷、`false` = 未截斷、`undefined` = 本次無法量測(保留前一狀態)。\n * 預設 = `el.scrollWidth > el.clientWidth`(觀察元素自身)。\n * Tag 傳自訂 fn:observe root、內部 `querySelector('[data-tag-text]')` + Canvas measureText。\n */\n measure?: (observedEl: HTMLElement) => boolean | undefined\n /** deps 變更時 cleanup + 重新量測 / 重新訂閱(Tag 傳 `[children]`)。預設 `[]`(mount-once)。 */\n deps?: React.DependencyList\n /**\n * 是否在首次 paint 後以 `requestAnimationFrame` + `setTimeout(100)` 再量一次,捕獲首幀 layout\n * 未完成 / 字型 async load 的假陰性(Breadcrumb 已採此 robust 版)。預設 `true`。\n */\n recheckAfterPaint?: boolean\n /** `'layoutEffect'` = `useLayoutEffect`(量測在 paint 前,避免 flash;Tag 用)/ `'effect'` = `useEffect`。預設 `'effect'`。 */\n timing?: 'effect' | 'layoutEffect'\n}\n\nconst defaultMeasure = (el: HTMLElement): boolean => el.scrollWidth > el.clientWidth\n\n/**\n * 偵測 ref 所指元素的文字是否被截斷,回傳 `{ ref, isTruncated }`。\n * `isTruncated` 用來驅動 Tooltip「僅截斷時顯示」(對齊 `tooltip.principles.stories.tsx`「沒被截斷就不該顯示 tooltip」)。\n */\nexport function useTruncated<E extends HTMLElement = HTMLElement>(\n options: UseTruncatedOptions = {},\n): { ref: React.MutableRefObject<E | null>; isTruncated: boolean } {\n const { measure = defaultMeasure, deps = [], recheckAfterPaint = true, timing = 'effect' } = options\n // MutableRefObject(consumer 可經 callback ref 寫入 .current 以與 forwardedRef 合併,如 Tag)。\n const ref = React.useRef<E | null>(null)\n const [isTruncated, setIsTruncated] = React.useState(false)\n\n // measure 每 render 可能是新 closure(consumer inline);收進 ref 讓 effect 不因它重訂閱,\n // 但 RO 回呼仍讀到最新版。\n const measureRef = React.useRef(measure)\n measureRef.current = measure\n\n // timing 為 call-site 固定字面值(某 consumer 永遠同一值)→ 每個 component instance 每 render\n // 呼叫同一個 effect hook,符合 rules-of-hooks。\n const useIsoEffect = timing === 'layoutEffect' ? React.useLayoutEffect : React.useEffect\n\n useIsoEffect(() => {\n const el = ref.current\n if (!el) return\n const check = () => {\n const r = measureRef.current(el)\n if (r !== undefined) setIsTruncated(r)\n }\n check()\n let raf = 0\n let t: ReturnType<typeof setTimeout> | undefined\n if (recheckAfterPaint) {\n raf = requestAnimationFrame(check)\n t = setTimeout(check, 100)\n }\n const cleanup = observeShared(el, check)\n return () => {\n if (raf) cancelAnimationFrame(raf)\n if (t) clearTimeout(t)\n cleanup()\n }\n // deps 由 consumer 決定(Tag: [children];其餘: []);measure 走 ref 不需進 deps。\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps)\n\n return { ref, isTruncated }\n}\n"],"names":[],"mappings":";AAkBA,IAAI,WAAkC;AACtC,MAAM,wCAAwB,QAAA;AAI9B,SAAS,cAA8B;AACrC,MAAI,SAAU,QAAO;AACrB,aAAW,IAAI,eAAe,CAAC,YAAY;AACzC,YAAQ,QAAQ,CAAC,MAAM;AACrB,YAAM,KAAK,kBAAkB,IAAI,EAAE,MAAM;AACzC,UAAI,OAAO,CAAC;AAAA,IACd,CAAC;AAAA,EACH,CAAC;AACD,SAAO;AACT;AAEA,SAAS,cAAc,IAAa,IAA4B;AAC9D,QAAM,MAAM,YAAA;AACZ,oBAAkB,IAAI,IAAI,EAAE;AAC5B,MAAI,QAAQ,EAAE;AACd,SAAO,MAAM;AACX,sBAAkB,OAAO,EAAE;AAC3B,QAAI,UAAU,EAAE;AAAA,EAClB;AACF;AAoBA,MAAM,iBAAiB,CAAC,OAA6B,GAAG,cAAc,GAAG;AAMlE,SAAS,aACd,UAA+B,IACkC;AACjE,QAAM,EAAE,UAAU,gBAAgB,OAAO,CAAA,GAAI,oBAAoB,MAAM,SAAS,SAAA,IAAa;AAE7F,QAAM,MAAM,MAAM,OAAiB,IAAI;AACvC,QAAM,CAAC,aAAa,cAAc,IAAI,MAAM,SAAS,KAAK;AAI1D,QAAM,aAAa,MAAM,OAAO,OAAO;AACvC,aAAW,UAAU;AAIrB,QAAM,eAAe,WAAW,iBAAiB,MAAM,kBAAkB,MAAM;AAE/E,eAAa,MAAM;AACjB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AACT,UAAM,QAAQ,MAAM;AAClB,YAAM,IAAI,WAAW,QAAQ,EAAE;AAC/B,UAAI,MAAM,OAAW,gBAAe,CAAC;AAAA,IACvC;AACA,UAAA;AACA,QAAI,MAAM;AACV,QAAI;AACJ,QAAI,mBAAmB;AACrB,YAAM,sBAAsB,KAAK;AACjC,UAAI,WAAW,OAAO,GAAG;AAAA,IAC3B;AACA,UAAM,UAAU,cAAc,IAAI,KAAK;AACvC,WAAO,MAAM;AACX,UAAI,0BAA0B,GAAG;AACjC,UAAI,gBAAgB,CAAC;AACrB,cAAA;AAAA,IACF;AAAA,EAGF,GAAG,IAAI;AAEP,SAAO,EAAE,KAAK,YAAA;AAChB;"}
|
package/llms-full.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @qijenchen/design-system — 完整設計參考(llms-full)
|
|
2
2
|
|
|
3
|
-
> 全 component / pattern 的 variants / sizes / 禁止事項。build-time 從 spec.md frontmatter 生成,禁手改。v0.1.0-beta.
|
|
3
|
+
> 全 component / pattern 的 variants / sizes / 禁止事項。build-time 從 spec.md frontmatter 生成,禁手改。v0.1.0-beta.91。
|
|
4
4
|
|
|
5
5
|
# Components
|
|
6
6
|
|
package/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @qijenchen/design-system
|
|
2
2
|
|
|
3
3
|
> World-class React design system(Radix/shadcn + Tailwind v4 + 自訂 design token)。
|
|
4
|
-
> 57 components + 4 public patterns + design tokens。v0.1.0-beta.
|
|
4
|
+
> 57 components + 4 public patterns + design tokens。v0.1.0-beta.91。
|
|
5
5
|
|
|
6
6
|
本檔由 source(spec.md frontmatter + Storybook index)build-time 自動生成,**禁手改**(CI --check drift gate 守)。
|
|
7
7
|
每元件 / pattern 的完整 variants / sizes / 禁止事項 全文見 [llms-full.txt](./llms-full.txt)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qijenchen/design-system",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.91",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "World-class design system — components, patterns, tokens, hooks (single source of truth for team distribution).",
|
|
6
6
|
"type": "module",
|
|
@@ -370,5 +370,8 @@ export const IntegrateRouterLinkAutoCollapse: Story = {
|
|
|
370
370
|
await expect(link, '摺疊的 router-link「產品團隊」應保留為可點 <a href="/org/team">(修前退化成純文字)').not.toBeNull()
|
|
371
371
|
await expect(link?.textContent).toContain('產品團隊')
|
|
372
372
|
})
|
|
373
|
+
// 收尾關摺疊選單:Radix DropdownMenu(modal)開著時對背景 nav 上 aria-hidden,背景麵包屑連結
|
|
374
|
+
// 仍 focusable → axe aria-hidden-focus。斷言已驗完,按 Escape 還原(選單開只為跑斷言,關掉無損展示)。
|
|
375
|
+
await userEvent.keyboard('{Escape}')
|
|
373
376
|
},
|
|
374
377
|
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
DropdownMenuItem,
|
|
14
14
|
} from '@/design-system/components/DropdownMenu/dropdown-menu'
|
|
15
15
|
|
|
16
|
-
// ──
|
|
16
|
+
// ── TruncatedText ── 2026-07-19:truncate+tooltip 引擎 + presentation 已抽成 SSOT primitive
|
|
17
17
|
// `patterns/element-anatomy/truncated-text`(`<TruncatedText>` 消費 `useTruncated` hook)。本檔改為
|
|
18
18
|
// 消費 `<TruncatedText display="block" tooltip={fullText}>`,原 file-local shared RO + TruncatedLabel 已移除。
|
|
19
19
|
|
|
@@ -146,7 +146,7 @@ const BreadcrumbList = React.forwardRef<HTMLOListElement, BreadcrumbListProps>(
|
|
|
146
146
|
const declarativeContent = React.useMemo(() => {
|
|
147
147
|
if (!items) return null
|
|
148
148
|
// 2026-07-04 修:非字串 label 的 key 原用 Math.random() → 每次 render 新 key 造成
|
|
149
|
-
// BreadcrumbItem unmount/remount(重置
|
|
149
|
+
// BreadcrumbItem unmount/remount(重置 TruncatedText ResizeObserver 與 state)→ 改 stable idx key
|
|
150
150
|
const renderItem = (spec: BreadcrumbItemSpec, role: 'root' | 'middle' | 'current', idx: number) => (
|
|
151
151
|
<BreadcrumbItem key={`${role}-${typeof spec.label === 'string' ? spec.label : idx}`} role={role}>
|
|
152
152
|
{role === 'current'
|
|
@@ -342,7 +342,7 @@ BreadcrumbList.displayName = 'BreadcrumbList'
|
|
|
342
342
|
* 設計回應 user 兩 challenges:
|
|
343
343
|
* (a) Root 也 truncate(shrink:3,不是 shrink-0)
|
|
344
344
|
* (b) 不用 fixed max-width — flex-shrink hierarchy 容器寬時自然展開不浪費空間,
|
|
345
|
-
* 窄時按優先級縮 +
|
|
345
|
+
* 窄時按優先級縮 + TruncatedText 內部 CSS truncate + tooltip。
|
|
346
346
|
*/
|
|
347
347
|
interface BreadcrumbItemProps extends React.ComponentPropsWithoutRef<'li'> {
|
|
348
348
|
role?: 'root' | 'middle' | 'current' | 'ellipsis'
|
|
@@ -396,7 +396,7 @@ const BreadcrumbLink = React.forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(
|
|
|
396
396
|
({ asChild, className, children, startIcon: StartIcon, ...props }, ref) => {
|
|
397
397
|
const { size } = React.useContext(BreadcrumbContext)
|
|
398
398
|
// 2026-05-12 fix(user 抓 image 2 Deep story 麵包屑沒符合 single-line + truncate canonical):
|
|
399
|
-
// 純文字 children → auto-wrap
|
|
399
|
+
// 純文字 children → auto-wrap TruncatedText(canonical「single-line + ellipsis + tooltip
|
|
400
400
|
// on truncate」per spec.md / Polaris breadcrumb)。Non-string children(consumer 自訂 icon+text
|
|
401
401
|
// 結構)→ pass-through 不 force-wrap(consumer own truncate)。
|
|
402
402
|
const wrappedChildren = typeof children === 'string'
|
|
@@ -446,7 +446,7 @@ interface BreadcrumbPageProps extends React.ComponentPropsWithoutRef<'span'> {
|
|
|
446
446
|
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, BreadcrumbPageProps>(
|
|
447
447
|
({ className, children, startIcon: StartIcon, ...props }, ref) => {
|
|
448
448
|
const { size } = React.useContext(BreadcrumbContext)
|
|
449
|
-
// 2026-05-12 fix(同 BreadcrumbLink):純文字 children → auto-wrap
|
|
449
|
+
// 2026-05-12 fix(同 BreadcrumbLink):純文字 children → auto-wrap TruncatedText。
|
|
450
450
|
const wrappedChildren = typeof children === 'string'
|
|
451
451
|
? <TruncatedText display="block" tooltip={children}>{children}</TruncatedText>
|
|
452
452
|
: children
|
|
@@ -404,7 +404,7 @@ function columnSizeStyle(
|
|
|
404
404
|
const SYSTEM_COL_IDS = new Set([SELECT_COL_ID, '__drag__', '__actions__'])
|
|
405
405
|
const isSystemColumn = (colId: string) => SYSTEM_COL_IDS.has(colId)
|
|
406
406
|
|
|
407
|
-
// ──
|
|
407
|
+
// ── TruncatedText ── 2026-07-19:truncate+tooltip 引擎 + presentation 已抽成 SSOT primitive
|
|
408
408
|
// `patterns/element-anatomy/truncated-text`(`<TruncatedText>` 消費 `useTruncated` hook)。原 file-local
|
|
409
409
|
// shared RO(2026-04-22 D3 perf audit 的「全 DS 共用單一 RO」特性由 hook 承接)+ TruncateCell 已移除,
|
|
410
410
|
// 改用 `<TruncatedText tooltip={children} className={...}>`(display 預設 inline,保 cell baseline)。
|
|
@@ -1473,10 +1473,10 @@ function DataTableInner<TData>(
|
|
|
1473
1473
|
const meta = cell.column.columnDef.meta
|
|
1474
1474
|
const colType = meta?.type as ColumnType | undefined
|
|
1475
1475
|
const wrap = autoRowHeight && meta?.wrap === true
|
|
1476
|
-
// 已知 compound 欄位(Tag / PersonDisplay / LinkInput 等自帶 layout)直接 bypass
|
|
1476
|
+
// 已知 compound 欄位(Tag / PersonDisplay / LinkInput 等自帶 layout)直接 bypass TruncatedText,
|
|
1477
1477
|
// 因為 `truncate` 的 inline baseline context 會破壞自訂 layout 的垂直對齊。
|
|
1478
1478
|
// 2026-05-09 D-path:date / time 加入(showDisplayEndIcon → Field naked-view 需 full width 才能
|
|
1479
|
-
// 右對齊 ItemSuffix。
|
|
1479
|
+
// 右對齊 ItemSuffix。TruncatedText 的 `<span truncate min-w-0>` block-display 會 collapse Field
|
|
1480
1480
|
// to content size,讓 Calendar / Clock icon 緊貼 value text 而非右邊緣)。
|
|
1481
1481
|
const isKnownCompound = colType === 'select' || colType === 'multiSelect' || colType === 'person' || colType === 'multiPerson' || colType === 'url' || colType === 'date' || colType === 'time'
|
|
1482
1482
|
const rowId = cell.row.id
|
|
@@ -1517,8 +1517,8 @@ function DataTableInner<TData>(
|
|
|
1517
1517
|
content = flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
1518
1518
|
}
|
|
1519
1519
|
// Consumer 自訂 cell(無 colType)若回傳 React element,視為 compound — consumer 自己處理
|
|
1520
|
-
// 對齊與截斷。回傳 primitive(string / number)才走
|
|
1521
|
-
// 理由:
|
|
1520
|
+
// 對齊與截斷。回傳 primitive(string / number)才走 TruncatedText。
|
|
1521
|
+
// 理由:TruncatedText 的 `span.truncate` 強制 white-space:nowrap + inline baseline,
|
|
1522
1522
|
// 對 inline-flex / icon+text 自訂結構會拉歪(見 circular-progress sync table 案例)。
|
|
1523
1523
|
// **edit mode bypass**(2026-05-05 v9 Bug 2 修):editing cell 內部是 Field 控件
|
|
1524
1524
|
// (Input/Textarea/Select etc.)自管 layout + 替代元素(textarea)不該被包進 inline span
|
|
@@ -1752,7 +1752,7 @@ function DataTableInner<TData>(
|
|
|
1752
1752
|
// - **沒有** cell 自己 box-shadow ring — focus / hover / open ring 由 Field naked 自帶
|
|
1753
1753
|
// state machine 提供(對齊 user「狀態樣式取決於原輸入框」reminder)
|
|
1754
1754
|
// 字級隨 size:sm/md text-body / lg text-body-lg(fieldDisplayTextClass),對齊 Field family
|
|
1755
|
-
// size→font SSOT。此為非-Field content(consumer 自訂 cell /
|
|
1755
|
+
// size→font SSOT。此為非-Field content(consumer 自訂 cell / TruncatedText 純文字)的 fallback 字級;
|
|
1756
1756
|
// typed cell 各自的 Field 控件已自帶 size→font(cell-registry 傳 size)。
|
|
1757
1757
|
'group/cell flex text-foreground font-normal shrink-0 relative self-stretch',
|
|
1758
1758
|
fieldDisplayTextClass(size),
|
|
@@ -541,9 +541,16 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(
|
|
|
541
541
|
commitDraft(now)
|
|
542
542
|
}
|
|
543
543
|
|
|
544
|
+
// typeable 模式外層 div 用 PopoverAnchor(僅定位,不注入 trigger a11y);非 typeable 用 PopoverTrigger
|
|
545
|
+
// (div 即 combobox 觸發)。修 dim-10 遺留的 axe aria-valid-attr-value(critical):PopoverTrigger 對
|
|
546
|
+
// typeable div 注入 aria-controls 指向「closed 時 unmount 的 popover content id」,而 typeable 已剝
|
|
547
|
+
// aria-expanded → axe「collapsed control 可缺 controlled 元素」豁免失效 → 判 aria-controls 值不合法。
|
|
548
|
+
// typeable 開啟走 controlled setOpen(Calendar icon click / input ArrowDown),不需 div 的 trigger onClick,
|
|
549
|
+
// 故改 anchor 零行為損失;combobox 語意全在內層 <input>(APG editable-combobox,見下方 :588)。
|
|
550
|
+
const TriggerOrAnchor = typeable ? PopoverAnchor : PopoverTrigger
|
|
544
551
|
return (
|
|
545
552
|
<Popover open={open} onOpenChange={setOpen}>
|
|
546
|
-
<
|
|
553
|
+
<TriggerOrAnchor asChild>
|
|
547
554
|
<div
|
|
548
555
|
ref={ref}
|
|
549
556
|
// a11y(2026-07-14 dim-10 修):typeable 模式 combobox 語意(name / state / 鍵盤)
|
|
@@ -643,7 +650,7 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(
|
|
|
643
650
|
<CalendarIcon size={iconSize} className="text-fg-muted" aria-hidden />
|
|
644
651
|
</ItemSuffix>
|
|
645
652
|
</div>
|
|
646
|
-
</
|
|
653
|
+
</TriggerOrAnchor>
|
|
647
654
|
<PopoverContent
|
|
648
655
|
className="w-auto p-0"
|
|
649
656
|
align="start"
|
|
@@ -34,7 +34,7 @@ export const SizeAlignment: Story = {
|
|
|
34
34
|
<div className="flex items-center gap-3">
|
|
35
35
|
<span className="text-body w-10 shrink-0">音量</span>
|
|
36
36
|
<Slider size={size} defaultValue={[40]} aria-label="音量" className="flex-1" />
|
|
37
|
-
<NumberInput size={size} value={40} onChange={() => {}} className="w-20 shrink-0" />
|
|
37
|
+
<NumberInput size={size} value={40} onChange={() => {}} aria-label="音量數值" className="w-20 shrink-0" />
|
|
38
38
|
</div>
|
|
39
39
|
</div>
|
|
40
40
|
))}
|
|
@@ -208,10 +208,14 @@ function TagInner(
|
|
|
208
208
|
</div>
|
|
209
209
|
)
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
// 永遠 wrap Tooltip、以 open 控制可見(對齊 truncated-text.spec.md:49「永遠 wrap」+ :55「❌ 條件 wrap」canonical
|
|
212
|
+
// + useTruncated 家族;tooltip.principles.stories.tsx:190「沒被截斷就不該顯示 tooltip」由 open={false} 機械保證)。
|
|
213
|
+
// 未截斷 open={false} → tooltip 靜默(TooltipTrigger asChild 只把 handler/data-state 合併到 tag div,零多餘 DOM、
|
|
214
|
+
// 視覺與裸 tag 等價,pixel-identical)。always-wrap 保 tag div 生命週期穩定 → isTruncated false→true 不 remount:
|
|
215
|
+
// 先前條件-wrap(未截斷回裸 tag / 截斷才 wrap)切換 JSX 樹會 remount tag div,但 useTruncated deps=[children] 未變
|
|
216
|
+
// → effect 不重跑重新 observe 新 div → shared RO 卡在舊 detached div → isTruncated 卡 true(未截斷仍顯示 tooltip)。
|
|
213
217
|
return (
|
|
214
|
-
<Tooltip>
|
|
218
|
+
<Tooltip open={isTruncated ? undefined : false}>
|
|
215
219
|
<TooltipTrigger asChild>{tag}</TooltipTrigger>
|
|
216
220
|
<TooltipContent>{children}</TooltipContent>
|
|
217
221
|
</Tooltip>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
|
|
3
3
|
// ── useTruncated — 單行文字截斷偵測引擎(SSOT)─────────────────────────────────
|
|
4
|
-
// 收斂 Breadcrumb TruncatedLabel / DataTable TruncateCell
|
|
5
|
-
//
|
|
4
|
+
// 收斂 Breadcrumb TruncatedLabel / DataTable TruncateCell(兩處 module-level shared ResizeObserver 引擎
|
|
5
|
+
// **逐字重複**,僅變數名不同)+ Tag(原 per-instance `new ResizeObserver`)三處的截斷偵測 + isTruncated
|
|
6
|
+
// 狀態機為唯一 owner(M17 SSOT);Tag 亦收斂進共用 shared RO,行為等價。
|
|
6
7
|
//
|
|
7
8
|
// 世界級對照(M8):MUI/MUI-X 官方無第一方 primitive,社群共識 = 「hook 量測 scrollWidth>clientWidth
|
|
8
9
|
// + ref + 以 Tooltip `open` prop 條件控制」(issue #37211);此 hook 即該低階量測層,
|