diff --git a/.changeset/olive-buttons-shave.md b/.changeset/olive-buttons-shave.md
new file mode 100644
index 0000000..e923fd1
--- /dev/null
+++ b/.changeset/olive-buttons-shave.md
@@ -0,0 +1,31 @@
+---
+'@zvndev/yable-core': minor
+'@zvndev/yable-react': patch
+'@zvndev/yable-themes': minor
+---
+
+Symmetric select-all handlers and token-driven header/cell typography.
+
+**core:** `table.getToggleAllRowsSelectedHandler()` and
+`table.getToggleAllPageRowsSelectedHandler()` mirror `row.getToggleSelectedHandler()`.
+Both ignore their event argument, so wiring one straight to `onChange` toggles
+instead of reading the event object as a truthy `value` flag (the trap in the old
+`onChange={table.toggleAllPageRowsSelected}` docs example).
+
+**themes:** new typography tokens, all inheriting existing defaults so nothing
+renders differently out of the box:
+
+- `--yable-font-family-header`, `--yable-font-family-cell` (both default to
+ `--yable-font-family`), so mono data under sans headers is a one-line override
+- `--yable-font-weight-header`, `--yable-header-text-transform`,
+ `--yable-header-letter-spacing`
+
+Every bundled theme now declares its header typography through these tokens
+instead of hardcoding it on its own `.yable-th` rule. That rule outranked any
+token set on the grid root, so killing a theme's uppercase headers previously
+required class overrides; setting `--yable-header-text-transform` is now enough.
+Density utilities that set `--yable-font-size-header` also reach the header for
+the first time. `createTheme()` gains matching `fontFamilyHeader`,
+`fontFamilyCell`, `fontWeightHeader`, `headerTextTransform`, and
+`headerLetterSpacing` keys, and the Tailwind preset gains `font-yable-header` /
+`font-yable-cell`.
diff --git a/.changeset/olive-moons-count.md b/.changeset/olive-moons-count.md
new file mode 100644
index 0000000..eb117e7
--- /dev/null
+++ b/.changeset/olive-moons-count.md
@@ -0,0 +1,10 @@
+---
+'@zvndev/yable-core': patch
+'@zvndev/yable-react': patch
+'@zvndev/yable-themes': patch
+'@zvndev/yable-vanilla': patch
+---
+
+Export `./package.json` from every package. `require.resolve('@zvndev/yable-react/package.json')`
+previously threw `ERR_PACKAGE_PATH_NOT_EXPORTED`, which breaks common
+version-introspection tooling.
diff --git a/.changeset/quiet-planets-repeat.md b/.changeset/quiet-planets-repeat.md
new file mode 100644
index 0000000..d4436f4
--- /dev/null
+++ b/.changeset/quiet-planets-repeat.md
@@ -0,0 +1,29 @@
+---
+'@zvndev/yable-react': minor
+---
+
+Fix container underflow on fully sized tables, and stop `clickableRows` gating `onRowClick`.
+
+**Underflow.** `autoColumnWidth`'s `distribute` / `stretch` policies only grew
+auto-sized columns, so a table where every column has an explicit `size` had
+nothing to grow: it pinned to its sized total and left a dead band on the right
+that no policy could close. When a table has no auto columns, every visible
+column now participates instead. New `underflow: 'stretch-last'` hands the whole
+remainder to the last visible column (ignoring its `maxSize`) and leaves the rest
+at their exact widths.
+
+Resolved widths flow through `columnSizing`, so the sticky header, the body, the
+virtualized inner table, and pinned offsets all read one set of numbers. The CSS
+workaround for this (`.yable-table { min-width: 100% }`) let the header and the
+virtualized body resolve the slack independently and drift out of column
+alignment. Widths an underflow policy grows are tracked against their declared
+base, so shrinking the container un-stretches instead of ratcheting upward.
+
+**`clickableRows`.** `row:click` was only emitted when the rendered `
` had
+`clickableRows` set, so passing `onRowClick` to `useTable` without also setting
+that prop silently did nothing (`row:dblclick` and `row:contextmenu` always
+emitted unconditionally). `row:click` now emits unconditionally, and
+`clickableRows` is purely the visual affordance: it defaults on when the table
+has an `onRowClick` handler, and `clickableRows={false}` drops the affordance
+while keeping the handler. `clickableRows` can also be set on `YableProvider`'s
+`tableProps` now, like every other visual default.
diff --git a/.changeset/tidy-hoops-tell.md b/.changeset/tidy-hoops-tell.md
new file mode 100644
index 0000000..a2ca3fa
--- /dev/null
+++ b/.changeset/tidy-hoops-tell.md
@@ -0,0 +1,34 @@
+---
+'@zvndev/yable-core': minor
+'@zvndev/yable-react': minor
+'@zvndev/yable-vanilla': minor
+'@zvndev/yable-themes': minor
+---
+
+Column alignment, a `data-sorted` hook, and tokens for frosted headers and detail accents.
+
+**`align` on a column def** (`'left' | 'center' | 'right'`) emits `data-align` on
+that column's header, body, and footer cells, so one declaration replaces a
+per-cell style. Right-aligned body cells also get `font-variant-numeric:
+tabular-nums`, which is what numeric columns want. Header labels live in a flex
+wrapper, so the alignment is applied there too rather than through `text-align`
+alone. Supported by both the React and vanilla renderers.
+
+**`data-sorted="asc|desc"`** on `.yable-th` mirrors `aria-sort` as a plain
+attribute. Highlighting the actively sorted column previously meant
+`:has(.yable-sort-indicator[data-active='true'])`, which reaches into the sort
+indicator's internals. Vanilla footers/headers also gained `data-column-id` on
+footer cells for targeting.
+
+**New theme tokens**, all defaulting to visual no-ops:
+
+- `--yable-header-backdrop-filter` (with the `-webkit-` prefix applied for
+ Safari) makes a frosted sticky header declarative: pair it with a translucent
+ `--yable-bg-header`.
+- `--yable-detail-accent-width` / `--yable-detail-accent-color` draw an inset
+ edge on an expanded master-detail panel, tying it to its parent row.
+
+`createTheme()` gained `headerBackdropFilter`, `detailAccentWidth`, and
+`detailAccentColor`. The themes README now documents the density presets
+(`
` / `.yable--density-*`), which already replace hand-setting six
+spacing tokens, plus the data-attribute styling hooks.
diff --git a/apps/docs/content/docs/api/column-definition-types.mdx b/apps/docs/content/docs/api/column-definition-types.mdx
index 00e3238..d984790 100644
--- a/apps/docs/content/docs/api/column-definition-types.mdx
+++ b/apps/docs/content/docs/api/column-definition-types.mdx
@@ -62,12 +62,22 @@ interface ColumnDefExtensions {
editConfig?: CellEditConfig
// Styling
+ align?: 'left' | 'center' | 'right'
cellClassName?: string | ((ctx: CellContext) => string | undefined)
headerClassName?: string
footerClassName?: string
}
```
+`align` sets the horizontal alignment for the column's header, body, and footer
+cells at once, emitting `data-align` on each. Right-aligned body cells also get
+`font-variant-numeric: tabular-nums`, so numeric columns line up digit by digit
+without a per-cell style:
+
+```tsx
+columnHelper.accessor('amount', { header: 'Amount', align: 'right' })
+```
+
### CellEditConfig
```typescript
diff --git a/apps/docs/content/docs/api/table-instance.mdx b/apps/docs/content/docs/api/table-instance.mdx
index 5dfeaac..dfc05b3 100644
--- a/apps/docs/content/docs/api/table-instance.mdx
+++ b/apps/docs/content/docs/api/table-instance.mdx
@@ -107,6 +107,8 @@ The object returned by `createTable()` or `useTable()`. All methods are grouped
| `getIsSomePageRowsSelected()` | `boolean` | Some current page rows selected? |
| `toggleAllRowsSelected(value?)` | `void` | Select/deselect all rows |
| `toggleAllPageRowsSelected(value?)` | `void` | Select/deselect current page rows |
+| `getToggleAllRowsSelectedHandler()` | `(event) => void` | Event handler that toggles all rows |
+| `getToggleAllPageRowsSelectedHandler()` | `(event) => void` | Event handler that toggles current page rows |
| `setRowSelection(updater)` | `void` | Set row selection state |
| `resetRowSelection(defaultState?)` | `void` | Reset row selection |
diff --git a/apps/docs/content/docs/features/column-sizing.mdx b/apps/docs/content/docs/features/column-sizing.mdx
index 3e244bd..7ab7b64 100644
--- a/apps/docs/content/docs/features/column-sizing.mdx
+++ b/apps/docs/content/docs/features/column-sizing.mdx
@@ -23,7 +23,7 @@ policy, or an object to tune it.
autoColumnWidth={{
sampleSize: 100, // rows sampled for measurement (default 100)
overflow: 'fit', // 'fit' | 'scroll' (default 'fit')
- underflow: 'leave', // 'leave' | 'distribute' | 'stretch' (default 'leave')
+ underflow: 'leave', // 'leave' | 'distribute' | 'stretch' | 'stretch-last' (default 'leave')
fitThreshold: 0.15, // compress instead of scroll when overflow ≤ 15% (default 0 = off)
}}
/>
@@ -74,12 +74,29 @@ When natural widths sum to **less** than the container:
proportionally so the container fills **exactly** — never a gutter. Use this on
modal/settings tables with a few capped columns where a trailing dead gutter
looks broken.
+- **`stretch-last`** — the entire remainder goes to the **last visible column**,
+ ignoring its `maxSize`. Every other column keeps its exact width. Use this when
+ one trailing column (a title, a notes field) should absorb the slack.
Both `distribute` and `stretch` are total-preserving: final widths sum to the
container width (with the ±1px rounding remainder folded into the last grown
column), except `distribute` when every column is capped, which intentionally
leaves the remainder as a gutter.
+
+**Fully sized tables fill too.** Growth normally targets auto-sized columns. A table where **every** column has an
+explicit `size` has none, so `distribute` and `stretch` used to have nothing to
+grow and left a dead band on the right no matter what you set. When there are no
+auto columns, every visible column participates instead, so a fully-sized table
+fills its container. `stretch-last` always targets the last column, auto or not.
+
+Solving this in the sizing layer rather than with CSS matters: the widths flow
+through `columnSizing`, so the sticky header, the body, the virtualized inner
+table, and pinned offsets all read the same numbers. A blanket
+`.yable-table { min-width: 100% }` lets the header and the virtualized body
+resolve the slack independently and drift out of alignment.
+
+
### Per-column opt-out
A column keeps its width and is excluded from measurement and squishing when it
diff --git a/apps/docs/content/docs/features/event-system.mdx b/apps/docs/content/docs/features/event-system.mdx
index 5ac72e5..ea07f89 100644
--- a/apps/docs/content/docs/features/event-system.mdx
+++ b/apps/docs/content/docs/features/event-system.mdx
@@ -79,3 +79,8 @@ const table = useTable({
onHeaderClick: (event) => console.log('Header clicked:', event),
})
```
+
+`onRowClick` fires on every row click, whether or not the `
` element sets
+`clickableRows`. That prop is a purely visual affordance (pointer cursor, hover
+treatment) and defaults on when the table has an `onRowClick` handler; set
+`clickableRows={false}` to keep the handler but drop the affordance.
diff --git a/apps/docs/content/docs/features/row-selection.mdx b/apps/docs/content/docs/features/row-selection.mdx
index 53b587c..0b58d51 100644
--- a/apps/docs/content/docs/features/row-selection.mdx
+++ b/apps/docs/content/docs/features/row-selection.mdx
@@ -57,7 +57,7 @@ columnHelper.display({
),
cell: ({ row }) => (
diff --git a/docs/API.md b/docs/API.md
index 312d025..ec6cf5f 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -532,19 +532,21 @@ The object returned by `createTable()` or `useTable()`. All methods are grouped
### Selection API
-| Method | Return | Description |
-| ----------------------------------- | ---------- | --------------------------------- |
-| `getSelectedRowModel()` | `RowModel` | Model of selected rows |
-| `getFilteredSelectedRowModel()` | `RowModel` | Selected rows (filtered) |
-| `getGroupedSelectedRowModel()` | `RowModel` | Selected rows (grouped) |
-| `getIsAllRowsSelected()` | `boolean` | All rows selected? |
-| `getIsSomeRowsSelected()` | `boolean` | Some (but not all) rows selected? |
-| `getIsAllPageRowsSelected()` | `boolean` | All current page rows selected? |
-| `getIsSomePageRowsSelected()` | `boolean` | Some current page rows selected? |
-| `toggleAllRowsSelected(value?)` | `void` | Select/deselect all rows |
-| `toggleAllPageRowsSelected(value?)` | `void` | Select/deselect current page rows |
-| `setRowSelection(updater)` | `void` | Set row selection state |
-| `resetRowSelection(defaultState?)` | `void` | Reset row selection |
+| Method | Return | Description |
+| --------------------------------------- | ----------------- | -------------------------------------------- |
+| `getSelectedRowModel()` | `RowModel` | Model of selected rows |
+| `getFilteredSelectedRowModel()` | `RowModel` | Selected rows (filtered) |
+| `getGroupedSelectedRowModel()` | `RowModel` | Selected rows (grouped) |
+| `getIsAllRowsSelected()` | `boolean` | All rows selected? |
+| `getIsSomeRowsSelected()` | `boolean` | Some (but not all) rows selected? |
+| `getIsAllPageRowsSelected()` | `boolean` | All current page rows selected? |
+| `getIsSomePageRowsSelected()` | `boolean` | Some current page rows selected? |
+| `toggleAllRowsSelected(value?)` | `void` | Select/deselect all rows |
+| `toggleAllPageRowsSelected(value?)` | `void` | Select/deselect current page rows |
+| `getToggleAllRowsSelectedHandler()` | `(event) => void` | Event handler that toggles all rows |
+| `getToggleAllPageRowsSelectedHandler()` | `(event) => void` | Event handler that toggles current page rows |
+| `setRowSelection(updater)` | `void` | Set row selection state |
+| `resetRowSelection(defaultState?)` | `void` | Reset row selection |
### Visibility API
@@ -1145,12 +1147,22 @@ interface ColumnDefExtensions {
rowSpan?: (row: Row, rows: Row[], rowIndex: number) => number | undefined
// Styling
+ align?: 'left' | 'center' | 'right'
cellClassName?: string | ((ctx: CellContext) => string | undefined)
headerClassName?: string
footerClassName?: string
}
```
+`align` sets the horizontal alignment for the column's header, body, and footer
+cells at once, emitting `data-align` on each. Right-aligned body cells also get
+`font-variant-numeric: tabular-nums`, so numeric columns line up digit by digit
+without a per-cell style:
+
+```tsx
+columnHelper.accessor('amount', { header: 'Amount', align: 'right' })
+```
+
### CellEditConfig
```typescript
diff --git a/docs/FEATURES.md b/docs/FEATURES.md
index 8b946e8..e189bac 100644
--- a/docs/FEATURES.md
+++ b/docs/FEATURES.md
@@ -792,7 +792,7 @@ columnHelper.display({
),
cell: ({ row }) => (
@@ -1398,6 +1398,11 @@ const table = useTable({
})
```
+`onRowClick` fires on every row click, whether or not the `
` element sets
+`clickableRows`. That prop is a purely visual affordance (pointer cursor, hover
+treatment) and defaults on when the table has an `onRowClick` handler; set
+`clickableRows={false}` to keep the handler but drop the affordance.
+
---
## i18n
diff --git a/e2e/interactions.spec.ts b/e2e/interactions.spec.ts
index dacb4d2..58c0587 100644
--- a/e2e/interactions.spec.ts
+++ b/e2e/interactions.spec.ts
@@ -615,6 +615,66 @@ test.describe('smart column width', () => {
.poll(async () => (await amountHeader.boundingBox())?.width ?? 0, { timeout: 5000 })
.toBeGreaterThan(placeholderWidth + 40)
})
+
+ // A table where EVERY column has an explicit `size` has no auto columns, so
+ // the underflow waterfall had nothing to grow and left a dead band on the
+ // right that no policy could close.
+ test.describe('underflow on a fully sized table', () => {
+ const SIZED_TOTAL = 120 + 140 + 220
+
+ test('leave keeps the dead band (unchanged default)', async ({ page }) => {
+ const grid = page.getByTestId('auto-underflow-leave')
+ await expect(grid.locator('tbody tr').first()).toBeVisible()
+ const header = grid.locator('.yable-thead').first()
+ const width = (await header.boundingBox())?.width ?? 0
+ expect(Math.abs(width - SIZED_TOTAL)).toBeLessThanOrEqual(4)
+ })
+
+ for (const policy of ['stretch', 'stretch-last'] as const) {
+ test(`${policy} fills the container and keeps header and body aligned`, async ({ page }) => {
+ const grid = page.getByTestId(`auto-underflow-${policy}`)
+ const scroller = grid.locator('.yable-virtual-scroll-container')
+ await expect(scroller).toBeVisible()
+ await expect(grid.locator('tbody tr').first()).toBeVisible()
+
+ const { overflow, headerW, clientW } = await scroller.evaluate((n) => {
+ const header = n.querySelector('.yable-thead')
+ return {
+ overflow: n.scrollWidth - n.clientWidth,
+ headerW: header ? (header as HTMLElement).getBoundingClientRect().width : 0,
+ clientW: n.clientWidth,
+ }
+ })
+
+ // Fills the container: no dead band, and no horizontal scrollbar either.
+ expect(headerW).toBeGreaterThanOrEqual(clientW - 2)
+ expect(headerW).toBeGreaterThan(SIZED_TOTAL + 100)
+ expect(overflow).toBeLessThanOrEqual(2)
+
+ // The CSS workaround for this (`min-width:100%`) let the sticky header
+ // and the virtualized body resolve the slack independently and drift.
+ // Solving it through columnSizing keeps every layer on one colgroup.
+ for (const id of ['code', 'region', 'notes']) {
+ const headerBox = await grid.locator(`th[data-column-id="${id}"]`).first().boundingBox()
+ const cellBox = await grid.locator(`td[data-column-id="${id}"]`).first().boundingBox()
+ expect(Math.abs((headerBox?.x ?? 0) - (cellBox?.x ?? -1))).toBeLessThanOrEqual(1)
+ expect(Math.abs((headerBox?.width ?? 0) - (cellBox?.width ?? -1))).toBeLessThanOrEqual(1)
+ }
+ })
+ }
+
+ test('stretch-last gives the slack to the last column only', async ({ page }) => {
+ const grid = page.getByTestId('auto-underflow-stretch-last')
+ await expect(grid.locator('tbody tr').first()).toBeVisible()
+
+ const width = async (id: string) =>
+ (await grid.locator(`th[data-column-id="${id}"]`).first().boundingBox())?.width ?? 0
+
+ expect(Math.abs((await width('code')) - 120)).toBeLessThanOrEqual(2)
+ expect(Math.abs((await width('region')) - 140)).toBeLessThanOrEqual(2)
+ expect(await width('notes')).toBeGreaterThan(220 + 100)
+ })
+ })
})
// The resize handle (visual bar AND pointer hit zone) must sit ON the column
diff --git a/examples/react-demo/src/app/e2e/auto-column-width/page.tsx b/examples/react-demo/src/app/e2e/auto-column-width/page.tsx
index c4042d2..7513deb 100644
--- a/examples/react-demo/src/app/e2e/auto-column-width/page.tsx
+++ b/examples/react-demo/src/app/e2e/auto-column-width/page.tsx
@@ -179,6 +179,48 @@ function AsyncMergeGrid() {
)
}
+// --- Underflow on a FULLY SIZED table --------------------------------------
+// Every column has an explicit `size`, so there are no auto columns to grow and
+// the sized total (480px) is far narrower than the 900px container. Before the
+// no-auto-columns fallback this left a dead band on the right that no underflow
+// policy could close. Row virtualization + sticky header are on because the
+// CSS workaround for the dead band (`min-width:100%`) let the header and the
+// virtualized body resolve the slack independently and drift out of alignment —
+// the spec asserts header and body cells stay column-aligned.
+
+interface SizedRow {
+ id: number
+ code: string
+ region: string
+ notes: string
+}
+
+const SIZED_ROWS: SizedRow[] = Array.from({ length: 60 }, (_, i) => ({
+ id: i + 1,
+ code: `SKU-${1000 + i}`,
+ region: ['Marlborough', 'Casablanca', 'Willamette'][i % 3]!,
+ notes: 'Bin check pending',
+}))
+
+const sizedCol = createColumnHelper()
+const fullySizedColumns = [
+ sizedCol.accessor('code', { header: 'Code', size: 120 }),
+ sizedCol.accessor('region', { header: 'Region', size: 140 }),
+ sizedCol.accessor('notes', { header: 'Notes', size: 220 }),
+]
+
+function FullySizedGrid({ underflow }: { underflow: 'stretch' | 'stretch-last' | 'leave' }) {
+ const table = useTable({
+ data: SIZED_ROWS,
+ columns: fullySizedColumns,
+ getRowId: (row) => String(row.id),
+ enableVirtualization: true,
+ virtualViewportHeight: 240,
+ rowHeight: 36,
+ })
+ return
+}
+
export default function AutoColumnWidthFixturePage() {
return (
@@ -199,10 +241,22 @@ export default function AutoColumnWidthFixturePage() {
overflow: scroll
-
+
autoSizeText (rendered-content measurement)
+
+
fully sized, underflow: leave
+
+
+
+
fully sized, underflow: stretch
+
+
+
+
fully sized, underflow: stretch-last
+
+
)
}
diff --git a/packages/core/package.json b/packages/core/package.json
index f695288..72900ea 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -86,7 +86,8 @@
"types": "./dist/features/pivot.d.cts",
"default": "./dist/features/pivot.cjs"
}
- }
+ },
+ "./package.json": "./package.json"
},
"files": [
"dist"
diff --git a/packages/core/src/core/__tests__/selection-scope.test.ts b/packages/core/src/core/__tests__/selection-scope.test.ts
index 397f0e3..b58c1c1 100644
--- a/packages/core/src/core/__tests__/selection-scope.test.ts
+++ b/packages/core/src/core/__tests__/selection-scope.test.ts
@@ -68,4 +68,27 @@ describe('Row selection scope', () => {
table.toggleAllPageRowsSelected(true)
expect(Object.keys(getState().rowSelection).sort()).toEqual(['1', '2'])
})
+
+ // The handler form mirrors row.getToggleSelectedHandler(): it ignores its
+ // event argument, so wiring it straight to onChange still TOGGLES rather than
+ // reading the event object as the `value` flag (which is always truthy).
+ it('getToggleAllRowsSelectedHandler toggles instead of reading the event as value', () => {
+ const { table, getState } = makeTable(data, cols)
+ const handler = table.getToggleAllRowsSelectedHandler()
+ handler({ target: { checked: false } })
+ expect(Object.keys(getState().rowSelection).sort()).toEqual(['1', '2', '3', '4'])
+ handler({ target: { checked: true } })
+ expect(getState().rowSelection).toEqual({})
+ })
+
+ it('getToggleAllPageRowsSelectedHandler toggles only the current page', () => {
+ const { table, getState } = makeTable(data, cols, {
+ initialState: { pagination: { pageIndex: 0, pageSize: 2 } },
+ })
+ const handler = table.getToggleAllPageRowsSelectedHandler()
+ handler(new Event('change'))
+ expect(Object.keys(getState().rowSelection).sort()).toEqual(['1', '2'])
+ handler(new Event('change'))
+ expect(getState().rowSelection).toEqual({})
+ })
})
diff --git a/packages/core/src/core/table.ts b/packages/core/src/core/table.ts
index b1e8280..4f58d66 100644
--- a/packages/core/src/core/table.ts
+++ b/packages/core/src/core/table.ts
@@ -473,6 +473,16 @@ export function createTable(options: TableOptions)
return next
})
},
+ getToggleAllRowsSelectedHandler: () => {
+ return (_e: unknown) => {
+ table.toggleAllRowsSelected()
+ }
+ },
+ getToggleAllPageRowsSelectedHandler: () => {
+ return (_e: unknown) => {
+ table.toggleAllPageRowsSelected()
+ }
+ },
setRowSelection: (_updater: Updater) => {},
resetRowSelection: (defaultState?: boolean) => {
table.setRowSelection(defaultState ? {} : table.getState().rowSelection)
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index db677ec..6d69bee 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -210,6 +210,13 @@ export interface ColumnDefExtensions {
rowSpan?: (row: Row, rows: Row[], rowIndex: number) => number | undefined
// Styling
+ /**
+ * Horizontal alignment for this column's header AND body cells. Emits
+ * `data-align` on both, so one declaration centralizes what would otherwise be
+ * a per-cell style. Right-aligned body cells also get tabular figures, which
+ * is what numeric columns want.
+ */
+ align?: 'left' | 'center' | 'right'
cellClassName?: string | ((ctx: CellContext) => string | undefined)
cellStyle?:
| React.CSSProperties
@@ -930,6 +937,8 @@ export interface Table {
getIsSomePageRowsSelected: () => boolean
toggleAllRowsSelected: (value?: boolean) => void
toggleAllPageRowsSelected: (value?: boolean) => void
+ getToggleAllRowsSelectedHandler: () => (event: unknown) => void
+ getToggleAllPageRowsSelectedHandler: () => (event: unknown) => void
setRowSelection: (updater: Updater) => void
resetRowSelection: (defaultState?: boolean) => void
getCellSelectionRange: () => CellRange | null
diff --git a/packages/react/package.json b/packages/react/package.json
index 1a5e0b9..b1bccfa 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -44,7 +44,8 @@
"types": "./dist/pretext.d.cts",
"default": "./dist/pretext.cjs"
}
- }
+ },
+ "./package.json": "./package.json"
},
"files": [
"dist"
diff --git a/packages/react/src/YableProvider.tsx b/packages/react/src/YableProvider.tsx
index 97e04eb..c22d798 100644
--- a/packages/react/src/YableProvider.tsx
+++ b/packages/react/src/YableProvider.tsx
@@ -35,6 +35,7 @@ export interface YableDefaults {
| 'direction'
| 'ariaLabel'
| 'adaptiveLayout'
+ | 'clickableRows'
>
>
/** Default column definition merged under every table's own `defaultColumnDef` */
diff --git a/packages/react/src/__tests__/Sorting.test.tsx b/packages/react/src/__tests__/Sorting.test.tsx
index c85d04a..6838df2 100644
--- a/packages/react/src/__tests__/Sorting.test.tsx
+++ b/packages/react/src/__tests__/Sorting.test.tsx
@@ -79,6 +79,22 @@ describe('Sorting', () => {
expect(nameHeader).toHaveAttribute('aria-sort', 'descending')
})
+ // `data-sorted` mirrors aria-sort so themes can target the active column
+ // without depending on the sort indicator's internal markup.
+ it('mirrors the sort direction onto data-sorted', async () => {
+ const user = userEvent.setup()
+ render()
+
+ const nameHeader = screen.getByRole('columnheader', { name: /Name/ })
+ expect(nameHeader).not.toHaveAttribute('data-sorted')
+
+ await user.click(nameHeader)
+ expect(nameHeader).toHaveAttribute('data-sorted', 'asc')
+
+ await user.click(nameHeader)
+ expect(nameHeader).toHaveAttribute('data-sorted', 'desc')
+ })
+
it('sorts row data in ascending order after click', async () => {
const user = userEvent.setup()
render()
@@ -104,9 +120,7 @@ describe('Sorting', () => {
await user.click(nameHeader)
const cells = screen.getAllByRole('gridcell')
- const nameValues = cells
- .filter((_cell, i) => i % 2 === 0)
- .map((cell) => cell.textContent)
+ const nameValues = cells.filter((_cell, i) => i % 2 === 0).map((cell) => cell.textContent)
expect(nameValues).toEqual(['Charlie', 'Bob', 'Alice'])
})
diff --git a/packages/react/src/__tests__/Table.test.tsx b/packages/react/src/__tests__/Table.test.tsx
index 4f9df56..b7b67cc 100644
--- a/packages/react/src/__tests__/Table.test.tsx
+++ b/packages/react/src/__tests__/Table.test.tsx
@@ -91,6 +91,36 @@ function SelectableTable() {
return