Repository navigation
docs(useTruncation): add hook docs and a live table cell truncation example block #10555
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
4bf8e7b
8a7542b
ad25d79
89967ab
37509f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| '@astryxdesign/core': patch | ||
| '@astryxdesign/cli': patch | ||
| --- | ||
|
|
||
| [docs] Add hook documentation and interactive table cell truncation example block for `useTruncation`, demonstrating responsive string truncation and accessibility tooltips. | ||
|
|
||
| @utkarshpatwa393-sys |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| // @vitest-environment jsdom | ||
|
|
||
| /** | ||
| * The useTruncation example demonstrates text truncation across table cells | ||
| * with single-line and multi-line clamps. | ||
| */ | ||
|
|
||
| import {act} from 'react'; | ||
| import {createRoot, type Root} from 'react-dom/client'; | ||
| import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; | ||
| import UseTruncationHookUsage from '../../assets/templates/blocks/components/Text/useTruncationHookUsage'; | ||
|
|
||
| (globalThis as {IS_REACT_ACT_ENVIRONMENT?: boolean}).IS_REACT_ACT_ENVIRONMENT = | ||
| true; | ||
|
|
||
| let root: Root | null = null; | ||
| let container: HTMLElement; | ||
|
|
||
| beforeEach(() => { | ||
| vi.stubGlobal( | ||
| 'matchMedia', | ||
| (query: string) => | ||
| ({ | ||
| matches: false, | ||
| media: query, | ||
| onchange: null, | ||
| addListener: () => {}, | ||
| removeListener: () => {}, | ||
| addEventListener: () => {}, | ||
| removeEventListener: () => {}, | ||
| dispatchEvent: () => false, | ||
| }) as unknown as MediaQueryList, | ||
| ); | ||
| container = document.createElement('div'); | ||
| document.body.appendChild(container); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| act(() => root?.unmount()); | ||
| root = null; | ||
| container.remove(); | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| describe('useTruncationHookUsage example', () => { | ||
| it('renders the narrow path column with the fixture rows and clamped cells', () => { | ||
| root = createRoot(container); | ||
| act(() => root!.render(<UseTruncationHookUsage />)); | ||
|
|
||
| const cells = Array.from(container.querySelectorAll('td')).map( | ||
| cell => cell.textContent ?? '', | ||
| ); | ||
| expect(cells).toContain('build'); | ||
| expect(cells).toContain('bundle'); | ||
| expect(cells).toContain('readme'); | ||
|
|
||
| // The paths reach the DOM | ||
| expect(container.textContent).toContain('build-20261103-120344'); | ||
| expect(container.textContent).toContain('reports/coverage'); | ||
| expect(container.textContent).toContain('README.md'); | ||
| }); | ||
|
|
||
| it('renders measured text elements across all table cells', async () => { | ||
| root = createRoot(container); | ||
| act(() => { | ||
| root!.render(<UseTruncationHookUsage />); | ||
| }); | ||
|
|
||
| await act(async () => {}); | ||
|
|
||
| // In the DOM, cells containing paths render span elements with truncation refs | ||
| const spans = Array.from(container.querySelectorAll('td span')); | ||
| expect(spans.length).toBe(3); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| // Copyright (c) Meta Platforms, Inc. and affiliates. | ||
|
|
||
| /** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */ | ||
| export const doc = { | ||
| type: 'block', | ||
| exampleFor: 'useTruncation', | ||
| name: 'useTruncation — Tooltip on Cut-Off Text', | ||
| displayName: 'useTruncation — Tooltip on Cut-Off Text', | ||
| description: | ||
| 'A narrow table column shows the full path in a Tooltip only where useTruncation reports the text is cut off; the short path shows none. One cell clamped to two lines shows the multi-line case.', | ||
| isReady: true, | ||
| aspectRatio: 4 / 3, | ||
| componentsUsed: ['Table', 'Tooltip'], | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| // Copyright (c) Meta Platforms, Inc. and affiliates. | ||
|
|
||
| 'use client'; | ||
|
|
||
| import {useTruncation} from '@astryxdesign/core/Text'; | ||
| import {Tooltip} from '@astryxdesign/core/Tooltip'; | ||
| import { | ||
| Table, | ||
| TableBody, | ||
| TableCell, | ||
| TableHeader, | ||
| TableHeaderCell, | ||
| TableRow, | ||
| } from '@astryxdesign/core/Table'; | ||
|
|
||
| interface FileAsset { | ||
| name: string; | ||
| path: string; | ||
| mode: 'single' | 'clamped' | 'short'; | ||
| } | ||
|
|
||
| const FILES: FileAsset[] = [ | ||
| { | ||
| name: 'build', | ||
| path: 'artifacts/release/build-20261103-120344/index.html', | ||
| mode: 'single', | ||
| }, | ||
| { | ||
| name: 'bundle', | ||
| path: 'reports/coverage/packages/core/src/components/Calendar/index.html', | ||
| mode: 'clamped', | ||
| }, | ||
| { | ||
| name: 'readme', | ||
| path: 'README.md', | ||
| mode: 'short', | ||
| }, | ||
| ]; | ||
|
|
||
| function TruncatedCell({children}: {children: string}) { | ||
| const truncation = useTruncation({maxLines: 1}); | ||
| return ( | ||
| <Tooltip | ||
| content={truncation.fullText} | ||
| placement="above" | ||
| isEnabled={truncation.isTruncated}> | ||
| <span | ||
| ref={truncation.ref} | ||
| tabIndex={truncation.isTruncated ? 0 : undefined} | ||
| style={{ | ||
| display: 'block', | ||
| maxWidth: 120, | ||
| overflow: 'hidden', | ||
| textOverflow: 'ellipsis', | ||
| whiteSpace: 'nowrap', | ||
| }}> | ||
| {children} | ||
| </span> | ||
| </Tooltip> | ||
| ); | ||
| } | ||
|
|
||
| function ClampedCell({children}: {children: string}) { | ||
| const truncation = useTruncation({maxLines: 2}); | ||
| return ( | ||
| <Tooltip | ||
| content={truncation.fullText} | ||
| placement="above" | ||
| isEnabled={truncation.isTruncated}> | ||
| <span | ||
| ref={truncation.ref} | ||
| tabIndex={truncation.isTruncated ? 0 : undefined} | ||
| style={{ | ||
| display: '-webkit-box', | ||
| WebkitLineClamp: 2, | ||
| WebkitBoxOrient: 'vertical', | ||
| overflow: 'hidden', | ||
| maxWidth: 120, | ||
| wordBreak: 'break-all', | ||
| }}> | ||
| {children} | ||
| </span> | ||
| </Tooltip> | ||
| ); | ||
| } | ||
|
|
||
| export default function UseTruncationHookUsage() { | ||
| return ( | ||
| <Table> | ||
| <TableHeader> | ||
| <TableRow> | ||
| <TableHeaderCell>Asset</TableHeaderCell> | ||
| <TableHeaderCell>Path</TableHeaderCell> | ||
| </TableRow> | ||
| </TableHeader> | ||
| <TableBody> | ||
| {FILES.map(file => ( | ||
| <TableRow key={file.name}> | ||
| <TableCell>{file.name}</TableCell> | ||
| <TableCell> | ||
| {file.mode === 'clamped' ? ( | ||
| <ClampedCell>{file.path}</ClampedCell> | ||
| ) : ( | ||
| <TruncatedCell>{file.path}</TruncatedCell> | ||
| )} | ||
| </TableCell> | ||
| </TableRow> | ||
| ))} | ||
| </TableBody> | ||
| </Table> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| // Copyright (c) Meta Platforms, Inc. and affiliates. | ||
|
|
||
| /** @type {import('@astryxdesign/cli/authoring').HookDoc} */ | ||
| export const docs = { | ||
| name: 'useTruncation', | ||
| displayName: 'useTruncation', | ||
| group: 'Text', | ||
| keywords: [ | ||
| 'truncation', | ||
| 'truncate', | ||
| 'ellipsis', | ||
| 'overflow', | ||
| 'line-clamp', | ||
| 'clamp', | ||
| 'tooltip', | ||
| 'full text', | ||
| 'text cut off', | ||
| 'table cell', | ||
| ], | ||
| params: [ | ||
| { | ||
| name: 'options', | ||
| type: 'UseTruncationOptions', | ||
| description: 'Configuration object.', | ||
| required: true, | ||
| }, | ||
| { | ||
| name: 'options.maxLines', | ||
| type: 'number', | ||
| description: | ||
| 'Maximum number of lines before truncation. 1 detects single-line overflow (scrollWidth against offsetWidth); values above 1 detect a -webkit-line-clamp cut by measuring the real content height with a Range, bypassing the clamped scrollHeight. 0 disables detection: isTruncated stays false and fullText stays empty.', | ||
| required: true, | ||
| }, | ||
| ], | ||
| returns: [ | ||
| { | ||
| name: 'ref', | ||
| type: 'RefCallback<HTMLElement>', | ||
| description: | ||
| 'Ref callback to attach to the text element being measured. Re-attaching to another element re-observes it and cleans up the previous observation.', | ||
| }, | ||
| { | ||
| name: 'isTruncated', | ||
| type: 'boolean', | ||
| description: | ||
| 'Whether the text is currently cut off. Drives composing a Tooltip with the full text: render the tooltip only when this is true, so a cell that fits shows none.', | ||
| }, | ||
| { | ||
| name: 'fullText', | ||
| type: 'string', | ||
| description: | ||
| "The element's current text content, captured on every measurement, for the tooltip a truncated cell shows. Follows content changes that resize nothing (a fixed-width status line whose text swaps).", | ||
| }, | ||
| ], | ||
| usage: { | ||
| description: | ||
| "Detects whether an element's text is cut off — single-line overflow or clamped to several lines — and gives back the full text, updating when the size or the content changes. Built on a shared ResizeObserver and a shared MutationObserver, so hundreds of table cells create one observer of each kind, and a first measurement runs batched in a microtask after the commit instead of forcing a layout pass per text during mount. Use it wherever a truncated cell should show its full content only when it is actually truncated: compose a Tooltip off isTruncated and fullText instead of always rendering one. Text and Heading build their truncation tooltips on it; reach for it directly when composing truncation feedback into your own component.", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking: since #10668 landed on |
||
| bestPractices: [ | ||
| { | ||
| guidance: true, | ||
| description: | ||
| 'Render the full-content Tooltip only when isTruncated is true, and read the tooltip text from fullText: a cell that fits shows no tooltip, and the text always matches what was measured.', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking: the example block keeps every Tooltip mounted and gates it with |
||
| }, | ||
| { | ||
| guidance: true, | ||
| description: | ||
| 'Apply the matching clamp yourself next to the hook — white-space: nowrap for maxLines 1, -webkit-line-clamp for more — so what the user sees and what the hook measures stay the same shape.', | ||
| }, | ||
| { | ||
| guidance: true, | ||
| description: | ||
| 'Reuse one useTruncation instance per measured element; the ref callback cleans up its own observers, so swapping elements is safe.', | ||
| }, | ||
| { | ||
| guidance: false, | ||
| description: | ||
| 'Compare scrollHeight to offsetHeight yourself for clamped text: with -webkit-line-clamp active browsers report the clamped size, which is exactly the case the Range measurement exists for.', | ||
| }, | ||
| ], | ||
| }, | ||
| relatedComponents: ['Text', 'Heading', 'Tooltip'], | ||
| importPath: '@astryxdesign/core/Text', | ||
| category: 'text', | ||
| }; | ||
|
|
||
| /** @type {import('@astryxdesign/cli/authoring').HookTranslationDoc} */ | ||
| export const docsDense = { | ||
| description: | ||
| 'Detects whether text is cut off (single-line overflow or -webkit-line-clamp) and returns the full text, updating on size and content changes. Shared Resize/Mutation observers keep hundreds of cells at one observer each; first measurement batches in a microtask after commit.', | ||
| paramDescriptions: { | ||
| options: 'config object.', | ||
| 'options.maxLines': | ||
| 'lines allowed before truncation; 1 = single-line overflow check, >1 = clamp detection via Range measurement, 0 = off.', | ||
| }, | ||
| returnDescriptions: { | ||
| ref: 'ref callback for the measured text element; swapping elements re-observes and cleans up.', | ||
| isTruncated: | ||
| 'true while the text is cut off; gate the full-content Tooltip on it.', | ||
| fullText: | ||
| 'current text content for the tooltip; follows content changes that resize nothing.', | ||
| }, | ||
| usage: { | ||
| description: | ||
| 'Truncation detection + full text for tooltips. Gate a Tooltip on isTruncated and read its text from fullText so only actually-cut cells show one. Shared observers; batched first measurement. Text/Heading build their truncation tooltips on it; use directly when composing truncation feedback yourself.', | ||
| bestPractices: [ | ||
| { | ||
| guidance: true, | ||
| description: 'Tooltip only when isTruncated; text from fullText.', | ||
| }, | ||
| { | ||
| guidance: true, | ||
| description: | ||
| 'Apply the matching clamp (nowrap / line-clamp) next to the hook so view and measurement agree.', | ||
| }, | ||
| { | ||
| guidance: true, | ||
| description: | ||
| 'One hook instance per measured element; the ref cleans up after itself.', | ||
| }, | ||
| { | ||
| guidance: false, | ||
| description: | ||
| 'Manual scrollHeight checks on clamped text — browsers report the clamped size.', | ||
| }, | ||
| ], | ||
| }, | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.