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
5 changes: 4 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ permissions:
contents: write

jobs:
tests:
uses: ./.github/workflows/test.yml

verify-examples:
runs-on: ubuntu-latest
steps:
Expand All @@ -37,7 +40,7 @@ jobs:
run: pnpm run build:examples

publish:
needs: verify-examples
needs: [tests, verify-examples]
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Test

on:
pull_request:
push:
branches:
- main
- master
# Allow other workflows (e.g. publish) to gate on this suite.
workflow_call:

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

# The React adapter Vitest suite aliases `simple-table-core` to the core
# *source*, so no build step is required.
- name: Run React adapter tests
run: pnpm --filter @simple-table/react run test
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,23 @@ const FooterRendererContent = () => {
footer.
</p>

<p className="text-gray-700 dark:text-gray-300 mb-4">
Set{" "}
<code className="bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded text-gray-800 dark:text-gray-200">
footerPosition=&quot;top&quot;
</code>{" "}
to render the footer above the table body instead of below it. This applies to both the
default footer and a custom{" "}
<code className="bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded text-gray-800 dark:text-gray-200">
footerRenderer
</code>
, and defaults to{" "}
<code className="bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded text-gray-800 dark:text-gray-200">
&quot;bottom&quot;
</code>
.
</p>

<PropTable props={FOOTER_RENDERER_PROPS} title="Footer Renderer Configuration" />

<PropTable props={FOOTER_RENDERER_PARAMS_PROPS} title="FooterRendererProps Interface" />
Expand Down
22 changes: 22 additions & 0 deletions apps/marketing/src/constants/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,27 @@ export interface ChangelogEntry {
link?: string;
}[];
}
export const v3_6_3: ChangelogEntry = {
version: "3.6.3",
date: "2026-05-31",
title: "footerPosition prop",
description:
'New footerPosition prop renders the pagination footer (built-in or footerRenderer) above the table body when set to "top".',
changes: [
{
type: "feature",
description:
'New footerPosition prop ("top" | "bottom", default "bottom") controls placement of the pagination footer.',
link: "/docs/footer-renderer",
},
{
type: "feature",
description:
"Added a st-row-position-{position} class to every rendered row (body cells, state rows, and nested-grid rows), letting consumers style any specific row via CSS (e.g. .st-row-position-3 { ... }).",
},
],
};

export const v3_6_2: ChangelogEntry = {
version: "3.6.2",
date: "2026-05-16",
Expand Down Expand Up @@ -1743,6 +1764,7 @@ export const v1_4_4: ChangelogEntry = {

// Array of all changelog entries (newest first)
export const CHANGELOG_ENTRIES: ChangelogEntry[] = [
v3_6_3,
v3_6_2,
v3_6_0,
v3_5_3,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,15 @@ initialSortDirection="asc"`,
type: "boolean",
example: `hideFooter={true}`,
},
{
key: "footerPosition",
name: "footerPosition",
required: false,
description:
'Placement of the pagination footer (built-in or footerRenderer). Use "top" to render above the table body; default is "bottom".',
type: '"top" | "bottom"',
example: `footerPosition="top"`,
},
{
key: "quickFilter",
name: "quickFilter",
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@simple-table/angular",
"version": "3.6.2",
"version": "3.6.3",
"main": "dist/cjs/index.js",
"module": "dist/index.es.js",
"types": "dist/types/angular/src/index.d.ts",
Expand Down
2 changes: 2 additions & 0 deletions packages/angular/src/lib/SimpleTableComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export class SimpleTableComponent implements OnInit, OnChanges, OnDestroy {
@Input() columnBorders?: SimpleTableAngularProps["columnBorders"];
@Input() rowButtons?: SimpleTableAngularProps["rowButtons"];
@Input() hideFooter?: SimpleTableAngularProps["hideFooter"];
@Input() footerPosition?: SimpleTableAngularProps["footerPosition"];
@Input() initialSortColumn?: SimpleTableAngularProps["initialSortColumn"];
@Input() initialSortDirection?: SimpleTableAngularProps["initialSortDirection"];
@Input() expandAll?: SimpleTableAngularProps["expandAll"];
Expand Down Expand Up @@ -194,6 +195,7 @@ export class SimpleTableComponent implements OnInit, OnChanges, OnDestroy {
if (this.columnBorders !== undefined) props.columnBorders = this.columnBorders;
if (this.rowButtons !== undefined) props.rowButtons = this.rowButtons;
if (this.hideFooter !== undefined) props.hideFooter = this.hideFooter;
if (this.footerPosition !== undefined) props.footerPosition = this.footerPosition;
if (this.initialSortColumn !== undefined) props.initialSortColumn = this.initialSortColumn;
if (this.initialSortDirection !== undefined)
props.initialSortDirection = this.initialSortDirection;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "simple-table-core",
"version": "3.6.2",
"version": "3.6.3",
"main": "dist/cjs/index.js",
"module": "dist/index.es.js",
"types": "dist/src/index.d.ts",
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/core/SimpleTableVanilla.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,10 @@ export class SimpleTableVanilla {
this.domManager.updateTheme(config.theme);
}

if (config.footerPosition !== undefined) {
this.domManager.syncFooterPosition(this.config.footerPosition);
}

if (config.customTheme !== undefined) {
const previousTheme = this.customTheme;
this.customTheme = TableInitializer.mergeCustomTheme(this.config);
Expand Down
41 changes: 39 additions & 2 deletions packages/core/src/core/dom/DOMManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export class DOMManager {
const footerContainer = document.createElement("div");
footerContainer.id = "st-footer-container";

const footerAtTop = config.footerPosition === "top";
if (footerAtTop) {
rootElement.classList.add("st-footer-position-top");
}

const ariaLiveRegion = document.createElement("div");
ariaLiveRegion.setAttribute("aria-live", "polite");
ariaLiveRegion.setAttribute("aria-atomic", "true");
Expand All @@ -88,8 +93,13 @@ export class DOMManager {

contentWrapper.appendChild(content);

wrapperContainer.appendChild(contentWrapper);
wrapperContainer.appendChild(footerContainer);
if (footerAtTop) {
wrapperContainer.appendChild(footerContainer);
wrapperContainer.appendChild(contentWrapper);
} else {
wrapperContainer.appendChild(contentWrapper);
wrapperContainer.appendChild(footerContainer);
}

rootElement.appendChild(wrapperContainer);
rootElement.appendChild(ariaLiveRegion);
Expand Down Expand Up @@ -117,6 +127,33 @@ export class DOMManager {
root.className = `${classes} theme-${theme}`;
}

/** Reorders footer vs content when `footerPosition` changes after mount. */
syncFooterPosition(footerPosition: SimpleTableConfig["footerPosition"]): void {
if (!this.elements) return;

const { rootElement, wrapperContainer, contentWrapper, footerContainer } = this.elements;
const atTop = footerPosition === "top";
rootElement.classList.toggle("st-footer-position-top", atTop);

const scrollbar = wrapperContainer.querySelector(".st-horizontal-scrollbar-container");

if (atTop) {
if (footerContainer !== wrapperContainer.firstElementChild) {
wrapperContainer.insertBefore(footerContainer, contentWrapper);
}
if (scrollbar) {
wrapperContainer.appendChild(scrollbar);
}
return;
}

if (scrollbar && scrollbar.parentElement === wrapperContainer) {
wrapperContainer.insertBefore(footerContainer, scrollbar);
} else if (footerContainer !== wrapperContainer.lastElementChild) {
wrapperContainer.appendChild(footerContainer);
}
}

getElements(): DOMElements | null {
return this.elements;
}
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/core/rendering/TableRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,9 @@ export class TableRenderer {
selectedRowCount,
cellUpdateFlash: deps.config.cellUpdateFlash,
useOddColumnBackground: deps.config.useOddColumnBackground,
useHoverRowBackground: deps.config.useHoverRowBackground,
// Defaults to true (documented default) so row hover works out of the box
// when consumers don't explicitly pass the flag. Explicit `false` is honored.
useHoverRowBackground: deps.config.useHoverRowBackground ?? true,
useOddEvenRowBackground: deps.config.useOddEvenRowBackground,
rowGrouping: deps.config.rowGrouping,
headers: deps.effectiveHeaders,
Expand Down Expand Up @@ -902,6 +904,7 @@ export class TableRenderer {
searchPlaceholder: mergedColumnEditorConfig.searchPlaceholder,
searchFunction: mergedColumnEditorConfig.searchFunction,
columnEditorConfig: mergedColumnEditorConfig,
icons: deps.resolvedIcons,
essentialAccessors: deps.essentialAccessors,
setHeaders: (newHeaders: HeaderObject[]) => {
deps.setHeaders(newHeaders);
Expand All @@ -927,6 +930,7 @@ export class TableRenderer {
searchPlaceholder: mergedColumnEditorConfig.searchPlaceholder,
searchFunction: mergedColumnEditorConfig.searchFunction,
columnEditorConfig: mergedColumnEditorConfig,
icons: deps.resolvedIcons,
essentialAccessors: deps.essentialAccessors,
setHeaders: (newHeaders: HeaderObject[]) => {
deps.setHeaders(newHeaders);
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import type HeaderDropdownProps from "./types/HeaderDropdownProps";
import type { HeaderDropdown } from "./types/HeaderDropdownProps";
import type { RowButtonProps } from "./types/RowButton";
import type FooterRendererProps from "./types/FooterRendererProps";
import type { FooterPosition } from "./types/FooterPosition";
import type {
LoadingStateRenderer,
ErrorStateRenderer,
Expand Down Expand Up @@ -117,6 +118,7 @@ export type {
ExportValueProps,
FilterCondition,
FooterRendererProps,
FooterPosition,
GetRowId,
GetRowIdParams,
IconsConfig,
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/styles/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,11 @@ input {
gap: var(--st-spacing-medium);
}

.simple-table-root.st-footer-position-top .st-footer {
border-top: none;
border-bottom: var(--st-border-width) solid var(--st-border-color);
}

.st-footer-info {
display: flex;
align-items: center;
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/types/FooterPosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** Where the pagination footer (default or `footerRenderer`) is placed in the table chrome. */
export type FooterPosition = "top" | "bottom";

export default FooterPosition;
3 changes: 3 additions & 0 deletions packages/core/src/types/SimpleTableConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ColumnEditorConfig } from "./ColumnEditorConfig";
import { VanillaIconsConfig } from "./IconsConfig";
import { QuickFilterConfig } from "./QuickFilterTypes";
import { AnimationsConfig } from "./AnimationsConfig";
import type { FooterPosition } from "./FooterPosition";

export interface SimpleTableConfig {
animations?: AnimationsConfig;
Expand All @@ -48,6 +49,8 @@ export interface SimpleTableConfig {
externalFilterHandling?: boolean;
externalSortHandling?: boolean;
footerRenderer?: (props: FooterRendererProps) => HTMLElement | string | null;
/** Placement of the pagination footer. Default `"bottom"`. */
footerPosition?: FooterPosition;
headerDropdown?: VanillaHeaderDropdown;
height?: string | number;
hideFooter?: boolean;
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types/SimpleTableProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ColumnEditorConfig } from "./ColumnEditorConfig";
import { IconsConfig } from "./IconsConfig";
import { QuickFilterConfig } from "./QuickFilterTypes";
import { AnimationsConfig } from "./AnimationsConfig";
import type { FooterPosition } from "./FooterPosition";

export interface SimpleTableProps {
animations?: AnimationsConfig; // Cell animation configuration (FLIP-style on sort and programmatic column reorder). Defaults: enabled=true, duration=240ms, easing=cubic-bezier(0.2, 0.8, 0.2, 1).
Expand All @@ -49,6 +50,7 @@ export interface SimpleTableProps {
externalFilterHandling?: boolean; // Flag to let consumer handle filter logic completely
externalSortHandling?: boolean; // Flag to let consumer handle sort logic completely
footerRenderer?: (props: FooterRendererProps) => HTMLElement | string | null; // Custom footer renderer
footerPosition?: FooterPosition; // Pagination footer placement (default "bottom")
headerDropdown?: HeaderDropdown; // Custom dropdown component for headers
height?: string | number; // Height of the table
hideFooter?: boolean; // Flag for hiding the footer
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/utils/bodyCell/styling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ const calculateBodyCellClasses = (cell: AbsoluteBodyCell, context: CellRenderCon
// Build class names
return [
"st-cell",
// Stable, scroll-independent row position so consumers can target any row
// (e.g. `.st-row-position-3`). Uses the absolute table position rather than
// the virtualized slice index, which changes as rows are reused on scroll.
`st-row-position-${cell.tableRow.position}`,
depth > 0 && header.expandable ? `st-cell-depth-${depth}` : "",
isIndividuallySelected
? isInitialFocused
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/columnEditor/createColumnEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import HeaderObject from "../../types/HeaderObject";
import { ColumnEditorSearchFunction, ColumnEditorConfig } from "../../types/ColumnEditorConfig";
import { createColumnEditorPopout } from "./createColumnEditorPopout";
import { ColumnVisibilityState } from "../../types/ColumnVisibilityTypes";
import { IconsConfig } from "../../types/IconsConfig";
import { COLUMN_EDIT_WIDTH } from "../../consts/general-consts";

export interface CreateColumnEditorOptions {
Expand All @@ -13,6 +14,7 @@ export interface CreateColumnEditorOptions {
searchPlaceholder: string;
searchFunction?: ColumnEditorSearchFunction;
columnEditorConfig: ColumnEditorConfig;
icons?: IconsConfig;
essentialAccessors?: ReadonlySet<string>;
resetColumns?: () => void;
setHeaders: (headers: HeaderObject[]) => void;
Expand All @@ -31,6 +33,7 @@ export const createColumnEditor = (options: CreateColumnEditorOptions) => {
searchPlaceholder,
searchFunction,
columnEditorConfig,
icons,
essentialAccessors,
resetColumns,
setHeaders,
Expand Down Expand Up @@ -69,6 +72,7 @@ export const createColumnEditor = (options: CreateColumnEditorOptions) => {
searchPlaceholder,
searchFunction,
columnEditorConfig,
icons,
essentialAccessors,
resetColumns,
setHeaders,
Expand Down Expand Up @@ -100,6 +104,9 @@ export const createColumnEditor = (options: CreateColumnEditorOptions) => {
if (newOptions.essentialAccessors !== undefined) {
essentialAccessors = newOptions.essentialAccessors;
}
if (newOptions.icons !== undefined) {
icons = newOptions.icons;
}
if (newOptions.resetColumns !== undefined) {
resetColumns = newOptions.resetColumns;
}
Expand All @@ -110,6 +117,7 @@ export const createColumnEditor = (options: CreateColumnEditorOptions) => {
searchPlaceholder: newOptions.searchPlaceholder,
searchFunction: newOptions.searchFunction,
columnEditorConfig: newOptions.columnEditorConfig,
icons: newOptions.icons,
essentialAccessors: newOptions.essentialAccessors,
resetColumns: newOptions.resetColumns,
setHeaders: newOptions.setHeaders,
Expand Down
Loading
Loading