Skip to content

feat: support table cell wrapping with truncation tooltip#696

Open
albertodainotti wants to merge 1 commit intoNotionX:masterfrom
InetIntel:feat-table-cell-wrap
Open

feat: support table cell wrapping with truncation tooltip#696
albertodainotti wants to merge 1 commit intoNotionX:masterfrom
InetIntel:feat-table-cell-wrap

Conversation

@albertodainotti
Copy link
Contributor

Problem

Notion has view-level (table_wrap) and per-column (wrap) settings that control whether table cells wrap text or truncate to a single line. When wrapping is disabled, Notion shows truncated text with a tooltip on hover revealing the full content.

The current react-notion-x implementation ignores these settings entirely — all cells always wrap.

Solution

1. Read wrap settings from the API

The table_wrap boolean on the collection view controls the default. Each column in table_properties can override it with a per-column wrap field. Absence means false (Notion convention).

const tableWrap = collectionView.format?.table_wrap ?? false
// Per column:
const cellWrap = p.wrap ?? tableWrap

2. Apply CSS class for non-wrapping cells

.notion-table-cell.notion-table-cell-nowrap {
  white-space: nowrap;
  text-overflow: ellipsis;
}

The compound selector (.notion-table-cell.notion-table-cell-nowrap) gives sufficient specificity to override the existing .notion-table-cell-text { white-space: pre-wrap } rule without !important.

3. Truncation tooltip via React Portal

A TableCellTooltip component shows the full content on hover — but only when content is actually truncated.

Why a React Portal?

Three approaches were evaluated:

Approach DOM nodes at rest Event listeners Truncation detection
A. CSS-only (data-tooltip + ::after) 0 0 ❌ None — tooltip shows on every cell, even non-truncated ones. Also limited to plain text (no rich formatting).
B. Per-cell hidden div N hidden divs (one per cell) 2N (mouseenter + mouseleave per cell) ❌ None — same as A. Full DOM subtrees duplicated per cell.
C. React Portal (chosen) 1 div 2 (delegated on table container) ✅ On-demand scrollWidth > clientWidth

Since react-notion-x is a general-purpose library, tables can be arbitrarily large — Notion databases commonly have hundreds or thousands of rows, and the "Load more" feature can surface them all:

Table size Option B (per-cell) Option C (portal)
50 rows × 8 cols 400 hidden divs + 800 listeners 1 div + 2 listeners
500 rows × 10 cols 5,000 hidden divs + 10,000 listeners 1 div + 2 listeners
2,000 rows × 10 cols 20,000 hidden divs + 40,000 listeners 1 div + 2 listeners

Option B degrades linearly with table size. Option C stays constant.

The portal uses event delegation: a single mouseenter listener on the .notion-table container checks whether the hovered .notion-table-cell-nowrap element has scrollWidth > clientWidth. If truncated, it clones the cell's innerHTML into a portal tooltip positioned above the cell. This is O(1) regardless of table size.

Why createPortal is appropriate here

The react-notion-x codebase currently avoids createPortal and favors minimal React state. However:

  1. Performance is the deciding factor. Options A and B create O(rows × columns) extra content at page load. For databases with hundreds or thousands of rows — common in real-world Notion workspaces — this means tens of thousands of hidden DOM nodes and event listeners. The portal creates O(1) extra content regardless of table size.
  2. Correctness matters. Only the portal approach can detect actual truncation.
  3. The JS cost is minimal. One delegated listener pair, ~120 lines of code.
  4. createPortal is idiomatic React. The library already uses useState, useEffect, useRef, and useCallback. Adding createPortal is a natural extension, not a paradigm shift.

Additional behaviors

  • Detects truncation on nested select/multi-select badge containers (where overflow happens at the badge level, not the cell)
  • Skips URL cells (which have their own abbreviated display via fix-url-abbreviation)
  • Tooltip styled to match Notion's native dark tooltip design

Files Changed

  • packages/notion-types/src/collection-view.ts (+1) — add wrap?: boolean to table property type
  • packages/react-notion-x/src/third-party/collection-view-table.tsx (+16 / -2) — wrap logic + tooltip mount
  • packages/react-notion-x/src/third-party/table-cell-tooltip.tsx (+120, new) — portal tooltip component
  • packages/react-notion-x/src/styles.css (+5) — nowrap class

Implement Notion-native table cell wrap behavior:
- Read table_wrap (view-level) and per-column wrap settings
- Add notion-table-cell-nowrap class with higher specificity to override
  the default pre-wrap style
- Add TableCellTooltip component using delegated events and React portal
  to show full content on hover over truncated cells
- Add wrap?: boolean to TableCollectionView type definition

The tooltip skips URL cells (they have their own abbreviation display)
and uses scrollWidth > clientWidth truncation detection.
@vercel
Copy link

vercel bot commented Mar 11, 2026

@albertodainotti is attempting to deploy a commit to the Saasify Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant