Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Added

- **Toolbar end slot** (Solid, React, Vue, vanilla): `toolbarEnd` (`JSXElement`/`ReactNode`) on `DataTableViewProps`/`DataTableProps`, Vue's `#toolbar-end` slot, and vanilla's `toolbarEnd` option + `setToolbarEnd(node)` render consumer controls (e.g. "Share view"/"Reset view") at the right end of the toolbar's action row, in a `.dt-toolbar-end` (Vue: `.dt__toolbar-end`) wrapper — instead of a separate row above the table, or DOM appended into the toolbar's internal markup.

### Changed

- **React**: `table.focus.consumeDomFocus` now keeps a stable identity across renders, so `DataTableView`'s scroll/focus effect lists it as a dependency (clears a `react-hooks/exhaustive-deps` lint warning).
Expand Down
16 changes: 10 additions & 6 deletions demo/react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -514,15 +514,16 @@ const VIEW_CONTROL_BTN_STYLE = {
// Shared by every section below: "Copy share link" copies the whole page URL (every section's
// state round-trips through its own query param, see VIEW_KEYS); "Reset" clears just this one
// table's own storageKey/paramName via resetView, back to its construction-time defaults.
function ViewControls({ onReset }: { onReset: () => void }) {
// `inToolbar` drops the bottom margin when rendered in the table's own toolbar (`toolbarEnd`).
function ViewControls({ onReset, inToolbar }: { onReset: () => void; inToolbar?: boolean }) {
const [copied, setCopied] = useState(false)
function copyShareLink() {
navigator.clipboard.writeText(window.location.href)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
}
return (
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
<div style={{ display: 'flex', gap: 8, marginBottom: inToolbar ? 0 : 12 }}>
<button onClick={copyShareLink} style={VIEW_CONTROL_BTN_STYLE}>
{copied ? 'Copied!' : 'Copy share link'}
</button>
Expand Down Expand Up @@ -643,10 +644,13 @@ function PersistedTable({ labels }: { labels?: Partial<DataTableLabels> }) {
})
const { reset } = usePersistence(table, VIEW_KEYS.persisted)
return (
<>
<ViewControls onReset={reset} />
<DataTableView table={table} data={SAMPLE_DATA} columns={COLUMNS} rowKey="id" />
</>
<DataTableView
table={table}
data={SAMPLE_DATA}
columns={COLUMNS}
rowKey="id"
toolbarEnd={<ViewControls onReset={reset} inToolbar />}
/>
)
}

Expand Down
14 changes: 8 additions & 6 deletions demo/solid/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -519,15 +519,16 @@ const VIEW_CONTROL_BTN_STYLE = {
// Shared by every section below: "Copy share link" copies the whole page URL (every section's
// state round-trips through its own query param, see VIEW_KEYS); "Reset" clears just this one
// table's own storageKey/paramName via resetView, back to its construction-time defaults.
function ViewControls(props: { onReset: () => void }) {
// `inToolbar` drops the bottom margin when rendered in the table's own toolbar (`toolbarEnd`).
function ViewControls(props: { onReset: () => void; inToolbar?: boolean }) {
const [copied, setCopied] = createSignal(false)
function copyShareLink() {
navigator.clipboard.writeText(window.location.href)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
}
return (
<div style={{ display: 'flex', gap: '8px', 'margin-bottom': '12px' }}>
<div style={{ display: 'flex', gap: '8px', 'margin-bottom': props.inToolbar ? '0' : '12px' }}>
<button onClick={copyShareLink} style={VIEW_CONTROL_BTN_STYLE}>
{copied() ? 'Copied!' : 'Copy share link'}
</button>
Expand Down Expand Up @@ -659,10 +660,11 @@ function PersistedTable(props: { labels?: Partial<DataTableLabels> }) {
}))
const { reset } = usePersistence(table, VIEW_KEYS.persisted)
return (
<>
<ViewControls onReset={reset} />
<DataTableView table={table} rowKey="id" />
</>
<DataTableView
table={table}
rowKey="id"
toolbarEnd={<ViewControls onReset={reset} inToolbar />}
/>
)
}

Expand Down
14 changes: 11 additions & 3 deletions demo/vanilla/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,15 +489,23 @@ const VIEW_BTN_STYLE =
// table's own container div (a table's render() rebuilds its container's innerHTML on every
// change, which would wipe these out) and is wired via one delegated click listener on `app`,
// dispatching on `data-view-copy`/`data-view-reset` — see the bottom of this file.
function renderViewControls(key: string): string {
function renderViewControls(key: string, inToolbar = false): string {
return `
<div style="display:flex;gap:8px;margin-bottom:12px">
<div style="display:flex;gap:8px;margin-bottom:${inToolbar ? 0 : 12}px">
<button data-view-copy="${key}" style="${VIEW_BTN_STYLE}">Copy share link</button>
<button data-view-reset="${key}" style="${VIEW_BTN_STYLE}">Reset</button>
</div>
`
}

// The same pair as a node, for a table's own `toolbarEnd` — the delegated listener on `app`
// still reaches it there, since the table's container is inside `app`.
function viewControlsNode(key: string): Node {
const tpl = document.createElement('template')
tpl.innerHTML = renderViewControls(key, true).trim()
return tpl.content.firstChild!
}

type ViewStateTable = {
getViewState(): TableViewState
setViewState(view: TableViewState): void
Expand Down Expand Up @@ -599,7 +607,6 @@ app.innerHTML = `
and open it in a new tab.
${docLink('view-persistence--sharing', '📖 Docs')}
</p>
${renderViewControls('persisted')}
<div id="table-persist"></div>

<h2 id="dynamic-data" style="font-size:16px;font-weight:600;margin-top:40px;margin-bottom:4px;scroll-margin-top:56px">Dynamic data</h2>
Expand Down Expand Up @@ -768,6 +775,7 @@ const tablePersist = createDataTable<Employee>(document.getElementById('table-pe
rowKey: 'id',
initialViewState: { visibleCols: PERSISTED_VISIBLE, pageSize: 5 },
labels: LOCALES[currentLocale],
toolbarEnd: viewControlsNode('persisted'),
})
wireViewPersistence(tablePersist, 'persisted')

Expand Down
4 changes: 3 additions & 1 deletion demo/vue/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1033,8 +1033,10 @@ function fmtSalary(n: number | null) {
<code>useTableState</code> instance you own instead — reorder or hide a column, then reload
the page. <span v-html="docLink('view-persistence--sharing', '📖 Docs')" />
</p>
<ViewControls @reset="resetPersistedTable" />
<DataTableView :table="persistedTable" :data="SAMPLE_DATA" :columns="COLUMNS" row-key="id">
<template #toolbar-end>
<ViewControls in-toolbar @reset="resetPersistedTable" />
</template>
<template #cell-department="{ value }">
<Badge :value="String(value)" :color-map="DEPT_COLORS" />
</template>
Expand Down
4 changes: 3 additions & 1 deletion demo/vue/src/ViewControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ref } from 'vue'
// Shared by every table section in App.vue: "Copy share link" copies the whole page URL (every
// section's state round-trips through its own query param, see VIEW_KEYS in App.vue); "Reset"
// emits so the caller can clear just its own table's storageKey/paramName via resetView.
// `inToolbar` drops the bottom margin when rendered in the table's own `#toolbar-end` slot.
defineProps<{ inToolbar?: boolean }>()
const emit = defineEmits<{ reset: [] }>()
const copied = ref(false)

Expand All @@ -26,7 +28,7 @@ const btnStyle = {
</script>

<template>
<div style="display: flex; gap: 8px; margin-bottom: 12px">
<div :style="{ display: 'flex', gap: '8px', marginBottom: inToolbar ? '0' : '12px' }">
<button :style="btnStyle" @click="copyShareLink">
{{ copied ? 'Copied!' : 'Copy share link' }}
</button>
Expand Down
8 changes: 8 additions & 0 deletions docs/toolbar.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ Together, showSearch + showColumns + the Sort/Filter fixes above fully close the
- **React/Solid**: `DataTable.tsx` must explicitly forward `showSearch` through to `<DataTableView>` — it isn't covered by a prop spread, so a new `DataTableViewProps` field needs a matching line added at each `<DataTable>` wrapper's own call site (a gap this feature's own tests caught: forgetting this bullet left `showSearch` silently inert through `<DataTable>` while still working through `<DataTableView>` directly).
- **Vanilla**: `showSearch` is backed by its own `createSignal` (`options.showSearch ?? true`) mirroring `selectable`, with a matching `setShowSearch(value: boolean)` on `DataTableInstance`.

## Consumer toolbar content (`toolbarEnd`)

Consumer controls (e.g. "Share view"/"Reset view") go at the end of the toolbar's action row through a public API rather than DOM appended into an internal toolbar class: `toolbarEnd` (`JSXElement` in Solid, `ReactNode` in React), Vue's `#toolbar-end` slot, vanilla's `toolbarEnd: Node | null` option + `setToolbarEnd`. Rendered last, after "Clear all", in a `dt-toolbar-end`/`dt__toolbar-end` wrapper with `margin-left: auto` — reset to `0` when "Clear all" (itself `margin-left: auto`) is shown, so the two sit together on the right instead of splitting the free space. Absent → no wrapper.

- **Solid**: resolved once via `children()` — reading a JSX prop in both `<Show when>` and the body would build it twice.
- **Solid**: typed `JSXElement`, not `JSX.Element` — the latter fails `vite:dts`'s type rollup ("Unable to follow symbol for JSX"), only on `npm run build`, not `type-check`.
- **React**: inline styles can't express the sibling rule, so the margin is picked from `hasActiveState`.

## Active-bar chip click actions

Previously a chip's body (the label/icon text) was inert — only its `×` did anything. Each chip's body is now a real, focusable `<button>` (a sibling of the `×` button rather than nested inside it — a `<button>` can't contain another interactive element, the same reasoning already used for the toolbar's grouped clear buttons) that does something specific to that chip's own kind of active state, so tweaking an already-active sort/group/filter no longer requires reopening its dropdown and re-navigating to the same entry:
Expand Down
29 changes: 15 additions & 14 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,20 +323,21 @@ Drag a column header to reorder it, or drag a row (or press Alt+ArrowUp/Alt+Arro

## `DataTable` props

| Prop | Type | Default | Description |
| ------------------------ | --------------------------------------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------- |
| `data` | `TRow[]` | — | Row data |
| `columns` | `ColumnDef<TRow>[]` | — | Column definitions |
| `rowKey` | `keyof TRow & string` | — | React list key only — not selection identity |
| `labels` | `Partial<DataTableLabels>` | English | UI string overrides |
| `defaultGroupsCollapsed` | `boolean` | `true` | Whether newly-grouped groups start collapsed |
| `initialViewState` | `TableViewState` | `{}` | Construction-time defaults for columns/sort/filters/grouping/page/search — also what `resetView` restores |
| `getRowId` | `(row: TRow) => string \| number` | — | Opt-in id-based selection identity (see "Row selection" above) |
| `selectable` | `boolean` | `false` | Show checkbox column for row selection |
| `onSelectionChange` | `(rows: TRow[]) => void` | — | Called when selection changes |
| `onRowClick` | `(row: TRow, event: MouseEvent<HTMLTableRowElement> \| KeyboardEvent<HTMLTableRowElement>) => void` | — | Called when a data row is clicked, or on Enter with keyboard focus |
| `showSearch` | `boolean` | `true` | Shows/hides the toolbar's search box (Sort/Group/Filter already auto-hide when no column qualifies) |
| `showColumns` | `boolean` | `true` | Shows/hides the Columns toolbar button; also auto-hides when `columns.length < 2` |
| Prop | Type | Default | Description |
| ------------------------ | --------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `data` | `TRow[]` | — | Row data |
| `columns` | `ColumnDef<TRow>[]` | — | Column definitions |
| `rowKey` | `keyof TRow & string` | — | React list key only — not selection identity |
| `labels` | `Partial<DataTableLabels>` | English | UI string overrides |
| `defaultGroupsCollapsed` | `boolean` | `true` | Whether newly-grouped groups start collapsed |
| `initialViewState` | `TableViewState` | `{}` | Construction-time defaults for columns/sort/filters/grouping/page/search — also what `resetView` restores |
| `getRowId` | `(row: TRow) => string \| number` | — | Opt-in id-based selection identity (see "Row selection" above) |
| `selectable` | `boolean` | `false` | Show checkbox column for row selection |
| `onSelectionChange` | `(rows: TRow[]) => void` | — | Called when selection changes |
| `onRowClick` | `(row: TRow, event: MouseEvent<HTMLTableRowElement> \| KeyboardEvent<HTMLTableRowElement>) => void` | — | Called when a data row is clicked, or on Enter with keyboard focus |
| `showSearch` | `boolean` | `true` | Shows/hides the toolbar's search box (Sort/Group/Filter already auto-hide when no column qualifies) |
| `showColumns` | `boolean` | `true` | Shows/hides the Columns toolbar button; also auto-hides when `columns.length < 2` |
| `toolbarEnd` | `ReactNode` | — | Content at the right end of the toolbar's action row, in a `.dt-toolbar-end` wrapper (e.g. "Share view"/"Reset view" buttons); no wrapper when omitted |

## Column definition

Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function DataTable<TRow extends object>({
onRowClick,
showSearch,
showColumns,
toolbarEnd,
}: DataTableProps<TRow>) {
const table = useTableState(data, columns, {
labels,
Expand All @@ -34,6 +35,7 @@ export function DataTable<TRow extends object>({
onRowClick={onRowClick}
showSearch={showSearch}
showColumns={showColumns}
toolbarEnd={toolbarEnd}
/>
)
}
16 changes: 16 additions & 0 deletions packages/react/src/DataTableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ const S = {
flexShrink: 0,
margin: '0 2px',
} as CSSProperties,
// Right-aligned on its own, or right after "Clear all" when that's already taken the free space.
toolbarEnd: {
display: 'flex',
alignItems: 'center',
gap: 8,
marginLeft: 'auto',
} as CSSProperties,
clearAll: {
marginLeft: 'auto',
padding: '5px 10px',
Expand Down Expand Up @@ -888,6 +895,7 @@ export function DataTableView<TRow extends object>({
onRowClick,
showSearch,
showColumns,
toolbarEnd,
}: DataTableViewProps<TRow>) {
const [openColsDD, setOpenColsDD] = useState(false)
const [openSortDD, setOpenSortDD] = useState(false)
Expand Down Expand Up @@ -3131,6 +3139,14 @@ export function DataTableView<TRow extends object>({
{L.clearAll}
</button>
)}
{toolbarEnd != null && toolbarEnd !== false && (
<div
className="dt-toolbar-end"
style={hasActiveState ? { ...S.toolbarEnd, marginLeft: 0 } : S.toolbarEnd}
>
{toolbarEnd}
</div>
)}
</div>
</div>

Expand Down
Loading
Loading