diff --git a/CHANGELOG.md b/CHANGELOG.md index 50fcd7a..bd7e300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/demo/react/src/App.tsx b/demo/react/src/App.tsx index 363b380..7996880 100644 --- a/demo/react/src/App.tsx +++ b/demo/react/src/App.tsx @@ -514,7 +514,8 @@ 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) @@ -522,7 +523,7 @@ function ViewControls({ onReset }: { onReset: () => void }) { setTimeout(() => setCopied(false), 1500) } return ( -
+
@@ -643,10 +644,13 @@ function PersistedTable({ labels }: { labels?: Partial }) { }) const { reset } = usePersistence(table, VIEW_KEYS.persisted) return ( - <> - - - + } + /> ) } diff --git a/demo/solid/src/App.tsx b/demo/solid/src/App.tsx index debcfcb..c602091 100644 --- a/demo/solid/src/App.tsx +++ b/demo/solid/src/App.tsx @@ -519,7 +519,8 @@ 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) @@ -527,7 +528,7 @@ function ViewControls(props: { onReset: () => void }) { setTimeout(() => setCopied(false), 1500) } return ( -
+
@@ -659,10 +660,11 @@ function PersistedTable(props: { labels?: Partial }) { })) const { reset } = usePersistence(table, VIEW_KEYS.persisted) return ( - <> - - - + } + /> ) } diff --git a/demo/vanilla/src/main.ts b/demo/vanilla/src/main.ts index 1641678..c7c984d 100644 --- a/demo/vanilla/src/main.ts +++ b/demo/vanilla/src/main.ts @@ -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 ` -
+
` } +// 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 @@ -599,7 +607,6 @@ app.innerHTML = ` and open it in a new tab. ${docLink('view-persistence--sharing', 'πŸ“– Docs')}

- ${renderViewControls('persisted')}

Dynamic data

@@ -768,6 +775,7 @@ const tablePersist = createDataTable(document.getElementById('table-pe rowKey: 'id', initialViewState: { visibleCols: PERSISTED_VISIBLE, pageSize: 5 }, labels: LOCALES[currentLocale], + toolbarEnd: viewControlsNode('persisted'), }) wireViewPersistence(tablePersist, 'persisted') diff --git a/demo/vue/src/App.vue b/demo/vue/src/App.vue index 46f236e..dbd9087 100644 --- a/demo/vue/src/App.vue +++ b/demo/vue/src/App.vue @@ -1033,8 +1033,10 @@ function fmtSalary(n: number | null) { useTableState instance you own instead β€” reorder or hide a column, then reload the page.

- + diff --git a/demo/vue/src/ViewControls.vue b/demo/vue/src/ViewControls.vue index d74dff5..ea8f902 100644 --- a/demo/vue/src/ViewControls.vue +++ b/demo/vue/src/ViewControls.vue @@ -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) @@ -26,7 +28,7 @@ const btnStyle = { ``` -`DataTableView` takes the same props as `` minus `labels`/`defaultGroupsCollapsed`/`initialViewState` (those only make sense at `useTableState` construction time) plus `table`, and supports the same `#cell-{key}`/`#filter-{key}`/`#group-{key}` scoped slots. In fact, `` is implemented as exactly this β€” a thin wrapper that calls `useTableState` and renders ``, forwarding its own slots straight through. +`DataTableView` takes the same props as `` minus `labels`/`defaultGroupsCollapsed`/`initialViewState` (those only make sense at `useTableState` construction time) plus `table`, and supports the same `#cell-{key}`/`#filter-{key}`/`#group-{key}`/`#toolbar-end` slots. In fact, `` is implemented as exactly this β€” a thin wrapper that calls `useTableState` and renders ``, forwarding its own slots straight through. ## i18n diff --git a/packages/vue/src/DataTableView.vue b/packages/vue/src/DataTableView.vue index bb33c43..0e439be 100644 --- a/packages/vue/src/DataTableView.vue +++ b/packages/vue/src/DataTableView.vue @@ -2377,6 +2377,9 @@ async function onFilterDropdownKeydown(event: KeyboardEvent): Promise { +
+ +
@@ -2856,6 +2859,15 @@ async function onFilterDropdownKeydown(event: KeyboardEvent): Promise { color: var(--color-text-secondary); font-family: inherit; } +.dt__toolbar-end { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; +} +.dt__clear-all ~ .dt__toolbar-end { + margin-left: 0; +} /* Dropdown internals */ .dt__dd-section { diff --git a/packages/vue/src/__tests__/DataTable.test.ts b/packages/vue/src/__tests__/DataTable.test.ts index e933ab3..ff1fd64 100644 --- a/packages/vue/src/__tests__/DataTable.test.ts +++ b/packages/vue/src/__tests__/DataTable.test.ts @@ -1353,6 +1353,22 @@ describe('DataTable β€” showSearch', () => { }) }) +describe('DataTable β€” toolbar-end slot', () => { + it('renders the slot as the last child of the toolbar actions row', () => { + const wrapper = mount(DataTable, { + props: { data: ROWS, columns: COLS, rowKey: 'id' }, + slots: { 'toolbar-end': '' }, + }) + const end = wrapper.find('.dt__toolbar-actions > .dt__toolbar-end:last-child') + expect(end.find('.share').exists()).toBe(true) + }) + + it('renders no toolbar-end wrapper when the slot is absent', () => { + const wrapper = mount(DataTable, { props: { data: ROWS, columns: COLS, rowKey: 'id' } }) + expect(wrapper.find('.dt__toolbar-end').exists()).toBe(false) + }) +}) + describe('DataTable β€” sort dropdown', () => { const SORT_COLS: ColumnDef[] = [ { key: 'name', label: 'Name' },