Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/github-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
run: corepack enable

- name: Install dependencies
run: yarn install
run: yarn install --immutable

- name: Build Storybook
run: yarn build-storybook
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
run: corepack enable

- name: Install dependencies
run: yarn install
run: yarn install --immutable

- name: Build Storybook
run: yarn build-storybook
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
import type { Meta, StoryContext, StoryObj } from '@storybook/react';
import { expect, within } from '@storybook/test';
import { useMemo } from 'react';
import { type LoaderFunctionArgs, useLoaderData, useSearchParams } from 'react-router';
import { columnConfigs, columns } from './data-table-stories.components';
Expand Down Expand Up @@ -252,10 +253,200 @@ function DataTableWithBazzaFilters() {
);
}

// --- DataTableWithScrolling ---
function DataTableWithScrolling() {
// Get the loader data (filtered/paginated/sorted data from server)
const loaderData = useLoaderData<DataResponse>();
const [searchParams, setSearchParams] = useSearchParams();

// Initialize data from loader response
const data = loaderData?.data ?? [];
const pageCount = loaderData?.meta.pageCount ?? 0;
const facetedCounts = loaderData?.facetedCounts ?? {};

// Convert facetedCounts to the correct type for useDataTableFilters (Map-based)
const facetedOptionCounts = useMemo(() => {
const result: Partial<Record<string, Map<string, number>>> = {};
Object.entries(facetedCounts).forEach(([col, valueObj]) => {
result[col] = new Map(Object.entries(valueObj));
});
return result;
}, [facetedCounts]);

// --- Bazza UI Filter Setup ---
// 1. Initialize filters state with useFilterSync (syncs with URL)
const [filters, setFilters] = useFilterSync();

// --- Read pagination and sorting directly from URL ---
// Use larger page size to ensure scrolling is needed
const pageIndex = Number.parseInt(searchParams.get('page') ?? '0', 10);
const pageSize = Number.parseInt(searchParams.get('pageSize') ?? '20', 10);
const sortField = searchParams.get('sortField');
const sortOrder = (searchParams.get('sortOrder') || 'asc') as 'asc' | 'desc';

// --- Pagination and Sorting State ---
const pagination = { pageIndex, pageSize };
const sorting = sortField ? [{ id: sortField, desc: sortOrder === 'desc' }] : [];

// --- Event Handlers: update URL directly ---
const handlePaginationChange: OnChangeFn<PaginationState> = (updaterOrValue) => {
const next = typeof updaterOrValue === 'function' ? updaterOrValue(pagination) : updaterOrValue;
searchParams.set('page', next.pageIndex.toString());
searchParams.set('pageSize', next.pageSize.toString());
setSearchParams(searchParams);
};

const handleSortingChange: OnChangeFn<SortingState> = (updaterOrValue) => {
const next = typeof updaterOrValue === 'function' ? updaterOrValue(sorting) : updaterOrValue;
if (next.length > 0) {
searchParams.set('sortField', next[0].id);
searchParams.set('sortOrder', next[0].desc ? 'desc' : 'asc');
} else {
searchParams.delete('sortField');
searchParams.delete('sortOrder');
}
setSearchParams(searchParams);
};

// --- Bazza UI Filter Setup ---
const bazzaProcessedColumns = useMemo(() => columnConfigs, []);

// Define a filter strategy (replace with your actual strategy if needed)
const filterStrategy = 'server' as const;

// Setup filter actions and strategy (controlled mode)
const {
columns: filterColumns,
actions,
strategy,
} = useDataTableFilters({
columnsConfig: bazzaProcessedColumns,
filters,
onFiltersChange: setFilters,
faceted: facetedOptionCounts,
strategy: filterStrategy,
data,
});

// --- TanStack Table Setup ---
const table = useReactTable({
data,
columns,
pageCount,
state: {
pagination,
sorting,
},
onPaginationChange: handlePaginationChange,
onSortingChange: handleSortingChange,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
manualPagination: true,
manualSorting: true,
});

return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold mb-4">Data Table with Scrolling and Sticky Header</h1>
<p className="text-gray-600 mb-6">
This demonstrates the table with vertical scrolling and a sticky header that remains visible while scrolling
through table rows. The table is contained within a fixed-height container.
</p>
</div>

{/* Bazza UI Filter Interface */}
<DataTableFilter columns={filterColumns} filters={filters} actions={actions} strategy={strategy} />

<div className="h-[500px] overflow-hidden">
{/* Data Table */}
<DataTable table={table} columns={columns.length} pageCount={pageCount} />
</div>
</div>
);
}
Comment on lines +256 to +368
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Scrolling story behavior vs docs and pageSize defaults are slightly inconsistent.

The DataTableWithScrolling + WithScrolling story wiring looks correct overall (fixed 500px container, same loader and filter plumbing as the server-driven story), but there are two small inconsistencies:

  1. Docs vs actual initial page size

    • The story description and DataTableWithScrolling comment mention using a larger page size (20 rows) to ensure scrolling.
    • However, withReactRouterStubDecorator still seeds ?page=0&pageSize=10, so the story actually starts at 10 rows unless the URL is overridden.
    • Consider either:
      • Passing an explicit initialPath: '/?page=0&pageSize=20' into withReactRouterStubDecorator for WithScrolling, or
      • Dropping the “20 rows” detail from the description and keeping everything at 10 for consistency.
  2. Default pageSize divergence when URL lacks a value

    • DataTableWithScrolling falls back to pageSize = 20, while DataTablePagination falls back to pageSize = 10.
    • In a context where no pageSize query param is present, the table’s pagination state and the pagination UI could disagree.
    • It would be safer to centralize this default (e.g., via dataTableRouterParsers.pageSize or a shared constant) and use the same default in both the table state and DataTablePagination.

These are minor, but aligning the defaults and story description will make the scrolling story more predictable and easier to reason about.

Also applies to: 627-654

🤖 Prompt for AI Agents
In apps/docs/src/remix-hook-form/data-table/data-table-server-driven.stories.tsx
around lines 256 to 368, the story claims a 20-row initial pageSize but the
router decorator seeds ?pageSize=10 and this file falls back to 20 while
DataTablePagination falls back to 10; fix by either updating the WithScrolling
story decorator to use initialPath='/?page=0&pageSize=20' to match the
description, and update the story text/comment to reflect the chosen value, and
centralize the pageSize default by importing/using the shared constant (e.g.,
dataTableRouterParsers.pageSize or a new exported DEFAULT_PAGE_SIZE) in this
file and in DataTablePagination so both fallbacks agree.


// --- Test Functions ---
const testInitialRenderServerSide = testInitialRender('Issues Table (Bazza UI Server Filters via Loader)');
const testPaginationServerSide = testPagination({ serverSide: true });

/**
* Test scrolling functionality and sticky header
*/
const testScrolling = async ({ canvasElement }: StoryContext) => {
const canvas = within(canvasElement);

// Wait for table to render
await new Promise((resolve) => setTimeout(resolve, 500));

// Find the table container
const tableContainer = canvasElement.querySelector('[class*="rounded-md border"]');
expect(tableContainer).toBeInTheDocument();

// Find the scrollable area (the div inside Table component)
const scrollableArea = tableContainer?.querySelector('[class*="overflow-auto"]') as HTMLElement | null;
expect(scrollableArea).toBeInTheDocument();

// Verify scrollable area exists and has content
if (!scrollableArea) {
throw new Error('Scrollable area not found');
}

// Get initial scroll position
const initialScrollTop = scrollableArea.scrollTop;
expect(initialScrollTop).toBe(0);

// Verify scroll height is greater than client height (content is scrollable)
const isScrollable = scrollableArea.scrollHeight > scrollableArea.clientHeight;
expect(isScrollable).toBe(true);

// Find the table header
const header = canvasElement.querySelector('thead');
expect(header).toBeInTheDocument();

if (!header) {
throw new Error('Table header not found');
}

// Get header position before scrolling
const headerBeforeScroll = header.getBoundingClientRect();
const headerTopBefore = headerBeforeScroll.top;

// Scroll down
scrollableArea.scrollTop = 200;
await new Promise((resolve) => setTimeout(resolve, 100));

// Verify that we scrolled (browser may round the scroll position, so check for reasonable scroll amount)
expect(scrollableArea.scrollTop).toBeGreaterThan(0);
expect(scrollableArea.scrollTop).toBeGreaterThan(100); // Verify we scrolled a reasonable amount

// Verify header is still visible and sticky
const headerAfterScroll = header.getBoundingClientRect();
expect(headerAfterScroll).toBeDefined();

// The header should have sticky positioning
const headerStyles = window.getComputedStyle(header);
expect(headerStyles.position).toBe('sticky');
expect(headerStyles.top).toBe('0px');

// Verify header position relative to container hasn't changed (it's sticky)
const headerTopAfter = headerAfterScroll.top;
// Header should remain at the top of the scrollable container
expect(headerTopAfter).toBeGreaterThanOrEqual(headerTopBefore - 10); // Allow small margin for rounding

// Scroll back to top
scrollableArea.scrollTop = 0;
await new Promise((resolve) => setTimeout(resolve, 100));

expect(scrollableArea.scrollTop).toBe(0);
};

/**
* Test initial render for scrolling story
*/
const testInitialRenderScrolling = testInitialRender('Data Table with Scrolling and Sticky Header');

// --- Story Configuration ---
const meta: Meta<typeof DataTableWithBazzaFilters> = {
title: 'Data Table/Server Driven Filters',
Expand Down Expand Up @@ -432,3 +623,32 @@ function DataTableWithBazzaFilters() {
await testFiltering(context);
},
};

export const WithScrolling: Story = {
args: {},
parameters: {
docs: {
description: {
story:
'Demonstrates the data table with vertical scrolling and a sticky header. The table is contained within a fixed-height container (500px) and uses a larger page size (20 rows) to ensure scrolling is needed. The header remains visible while scrolling through table rows.',
},
},
},
render: () => <DataTableWithScrolling />,
decorators: [
withReactRouterStubDecorator({
routes: [
{
path: '/',
Component: DataTableWithScrolling,
loader: handleDataFetch,
},
],
}),
],
play: async (context) => {
// Run the tests in sequence
await testInitialRenderScrolling(context);
await testScrolling(context);
},
};
2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lambdacurry/forms",
"version": "0.22.5",
"version": "0.23.0-beta.1",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Version bump looks good; verify monorepo consistency.

The prerelease bump to "0.23.0-beta.1" is reasonable for the new DataTable behavior. Please just ensure other package manifests, changelogs, and publish scripts are aligned with this version.


🏁 Script executed:

#!/bin/bash
# From repo root, check for other versions that may need to align.
echo 'Package versions referencing @lambdacurry/forms:'
rg -n '"@lambdacurry/forms"' apps packages

echo
echo 'All version fields in package.json files:'
fd package.json -a apps packages | xargs -I{} jq -r '.name + "@" + .version' {}

Length of output: 278



---

🏁 Script executed:

```shell
#!/bin/bash
# Check for CHANGELOG files and publish scripts
echo "=== CHANGELOG files in monorepo ==="
fd -i 'changelog|release|history' -e md -e txt

echo
echo "=== Publish/release scripts ==="
fd 'publish|release' -e sh -e js -e ts packages apps

echo
echo "=== Check for version references in root ==="
cat package.json 2>/dev/null | jq -r '.version // "no root version"' || echo "no root package.json"

Length of output: 153



---

🏁 Script executed:

```shell
#!/bin/bash
# Check CHANGELOG.md for 0.23.0-beta.1 entry
echo "=== First 50 lines of packages/components/CHANGELOG.md ==="
head -50 packages/components/CHANGELOG.md

echo
echo "=== Search for 0.23.0 in CHANGELOG ==="
grep -n "0.23.0" packages/components/CHANGELOG.md || echo "No 0.23.0 found in CHANGELOG"

Length of output: 565


Update CHANGELOG.md with 0.23.0-beta.1 entry.

The version bump to 0.23.0-beta.1 in package.json is correct, and dependency references are properly aligned. However, packages/components/CHANGELOG.md is missing an entry for this version—the most recent entry is 0.22.2. Add a new section documenting the changes for 0.23.0-beta.1.

🤖 Prompt for AI Agents
In packages/components/package.json around line 3, the version was bumped to
0.23.0-beta.1 but packages/components/CHANGELOG.md lacks an entry for that
version; open packages/components/CHANGELOG.md and add a new top section for
0.23.0-beta.1 with date and a brief list of changes (copy relevant release notes
or summarize commits since 0.22.2), following the existing changelog format and
ordering so the new entry appears above 0.22.2.

"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ChevronLeftIcon, ChevronRightIcon, DoubleArrowLeftIcon, DoubleArrowRigh
import { useSearchParams } from 'react-router';
import { Button } from '../button';
import { Select } from '../select';
import { cn } from '../utils';

interface DataTablePaginationProps {
pageCount: number;
Expand All @@ -24,9 +25,13 @@ export function DataTablePagination({ pageCount, onPaginationChange }: DataTable
return (
<nav
aria-label="Data table pagination"
className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between px-2 py-2"
className={cn(
"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between px-2 py-2",
"h-[var(--lc-datatable-pagination-height,140px)]",
"sm:h-[var(--lc-datatable-pagination-height,96px)]",
)}
>
<div className="flex-1 text-sm text-muted-foreground">{pageSize} rows per page</div>
<div className="max-sm:hidden flex-1 text-sm text-muted-foreground">{pageSize} rows per page</div>
<div className="flex flex-col sm:flex-row items-center gap-4 sm:gap-6 lg:gap-8">
<div className="flex items-center gap-2">
<p className="text-sm font-medium whitespace-nowrap">Rows per page</p>
Expand Down
14 changes: 7 additions & 7 deletions packages/components/src/ui/data-table/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ export function DataTable<TData>({
className,
}: DataTableProps<TData>) {
return (
<div className={cn('space-y-4', className)}>
<div className="rounded-md border">
<Table>
<TableHeader>
<div className={cn('space-y-4 h-full [--lc-datatable-pagination-height:140px] sm:[--lc-datatable-pagination-height:96px]', className)}>
<div className="rounded-md border flex flex-col max-h-[calc(100%-var(--lc-datatable-pagination-height,140px)-1rem)] sm:max-h-[calc(100%-var(--lc-datatable-pagination-height,96px)-1rem)] overflow-hidden">
<Table className="[&>div]:h-full [&>div>table]:h-full">
<TableHeader className="sticky top-0 z-10 bg-background">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
Expand All @@ -35,7 +35,7 @@ export function DataTable<TData>({
</TableRow>
))}
</TableHeader>
<TableBody>
<TableBody className={cn(!table.getRowModel().rows?.length && 'min-h-[calc(100%-3rem)]')}>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
Expand All @@ -45,8 +45,8 @@ export function DataTable<TData>({
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns} className="h-24 text-center">
<TableRow className="h-full">
<TableCell colSpan={columns} className="h-full text-center">
No results.
</TableCell>
</TableRow>
Expand Down
Loading