diff --git a/apps/www/src/components/datatable-selection-demo.tsx b/apps/www/src/components/datatable-selection-demo.tsx new file mode 100644 index 000000000..6583cfdb4 --- /dev/null +++ b/apps/www/src/components/datatable-selection-demo.tsx @@ -0,0 +1,147 @@ +'use client'; + +import { TransformIcon } from '@radix-ui/react-icons'; +import { + Button, + Checkbox, + Chip, + DataTable, + type DataTableColumnDef, + FloatingActions, + useDataTable +} from '@raystack/apsara'; +import { useEffect, useMemo } from 'react'; + +type Row = { + id: string; + name: string; + email: string; + role: string; + team: string; +}; + +const names = [ + 'Alice Ross', + 'Bob Evans', + 'Clara Patel', + 'David Kim', + 'Elena Moreno', + 'Finn O’Connor', + 'Grace Tan', + 'Henry Novak', + 'Ivy Chen', + 'Jason Park' +]; +const roles = ['Admin', 'Editor', 'Viewer']; +const teams = ['Engineering', 'Design', 'Product', 'Marketing', 'Sales']; + +const selectionColumn: DataTableColumnDef = { + id: 'select', + accessorKey: 'select', + header: '', + cell: ({ row }) => ( + row.toggleSelected(Boolean(value))} + aria-label='Select row' + onClick={e => e.stopPropagation()} + /> + ), + enableColumnFilter: false, + enableSorting: false, + enableHiding: false, + styles: { + cell: { width: 40, flex: 'none' }, + header: { width: 40, flex: 'none' } + } +}; + +const columns: DataTableColumnDef[] = [ + selectionColumn, + { accessorKey: 'name', header: 'Name', enableColumnFilter: true }, + { accessorKey: 'email', header: 'Email', enableColumnFilter: true }, + { accessorKey: 'role', header: 'Role', enableColumnFilter: true }, + { accessorKey: 'team', header: 'Team', enableColumnFilter: true } +]; + +function InitialSelection() { + const { table } = useDataTable(); + useEffect(() => { + table.setRowSelection({ '1': true, '3': true }); + }, [table]); + return null; +} + +function SelectionBar() { + const { table } = useDataTable(); + const selected = table.getSelectedRowModel().rows; + if (selected.length === 0) return null; + + return ( + + } + isDismissible + onDismiss={() => table.resetRowSelection()} + > + {selected.length} selected + + + + + + ); +} + +const DataTableSelectionDemo = () => { + const data = useMemo( + () => + Array.from({ length: 80 }, (_, i) => { + const name = names[i % names.length]; + const handle = name.toLowerCase().replace(/[^a-z]/g, ''); + return { + id: String(i + 1), + name, + email: `${handle}${i + 1}@example.com`, + role: roles[i % roles.length], + team: teams[i % teams.length] + }; + }), + [] + ); + + return ( +
+ row.id} + > + + + + + + +
+ ); +}; + +export default DataTableSelectionDemo; diff --git a/apps/www/src/components/demo/demo.tsx b/apps/www/src/components/demo/demo.tsx index b542cd6b6..c28d4e871 100644 --- a/apps/www/src/components/demo/demo.tsx +++ b/apps/www/src/components/demo/demo.tsx @@ -37,6 +37,7 @@ import { Home, Info, Laugh, X } from 'lucide-react'; import NextLink from 'next/link'; import { Suspense } from 'react'; import DataTableDemo from '../datatable-demo'; +import DataTableSelectionDemo from '../datatable-selection-demo'; import LinearMenuDemo from '../linear-dropdown-demo'; import PopoverColorPicker from '../popover-color-picker'; import DemoPlayground from './demo-playground'; @@ -54,6 +55,7 @@ export default function Demo(props: DemoProps) { OrganizationIcon, SidebarIcon, DataTableDemo, + DataTableSelectionDemo, LinearMenuDemo, PopoverColorPicker, Info, diff --git a/apps/www/src/content/docs/components/datatable/demo.ts b/apps/www/src/content/docs/components/datatable/demo.ts index bc3aa9e87..768299c32 100644 --- a/apps/www/src/content/docs/components/datatable/demo.ts +++ b/apps/www/src/content/docs/components/datatable/demo.ts @@ -4,3 +4,94 @@ export const preview = { type: 'code', code: `` }; + +export const rowSelectionDemo = { + type: 'code', + previewCode: false, + code: ``, + codePreview: [ + { + label: 'index.tsx', + code: `import { + Button, + Checkbox, + Chip, + DataTable, + FloatingActions, + useDataTable, +} from "@raystack/apsara"; +import { TransformIcon } from "@radix-ui/react-icons"; + +// 1. Leading checkbox column that wires TanStack selection. +const selectionColumn = { + id: "select", + header: ({ table }) => ( + table.toggleAllRowsSelected(Boolean(v))} + /> + ), + cell: ({ row }) => ( + row.toggleSelected(Boolean(v))} + onClick={(e) => e.stopPropagation()} + /> + ), + enableSorting: false, + enableColumnFilter: false, + enableHiding: false, +}; + +// 2. Render the bar alongside DataTable, reading selection from context. +// FloatingActions defaults to variant="floating" (position: fixed, +// bottom-center), so no positioning CSS is needed here. +function SelectionBar() { + const { table } = useDataTable(); + const selected = table.getSelectedRowModel().rows; + if (selected.length === 0) return null; + + return ( + + } + isDismissible + onDismiss={() => table.resetRowSelection()} + > + {selected.length} selected + + + + + + ); +} + +// 3. Compose. + + + + + +` + } + ] +}; diff --git a/apps/www/src/content/docs/components/datatable/index.mdx b/apps/www/src/content/docs/components/datatable/index.mdx index e865648c1..0a5040fce 100644 --- a/apps/www/src/content/docs/components/datatable/index.mdx +++ b/apps/www/src/content/docs/components/datatable/index.mdx @@ -4,7 +4,7 @@ description: An advanced React table that supports filtering, sorting, and pagin source: packages/raystack/components/datatable --- -import { preview } from "./demo.ts"; +import { preview, rowSelectionDemo } from "./demo.ts"; @@ -470,6 +470,17 @@ import { OrganizationIcon, FilterIcon } from "@raystack/apsara/icons"; /> ``` +### Row selection + +DataTable does not ship a built-in selection toolbar, but the underlying TanStack table instance is exposed via `useDataTable()`, so you can wire row selection yourself and float an [`FloatingActions`](/docs/components/floating-actions) bar over the table when rows are selected. + + + +Notes: +- `table.resetRowSelection()` clears the selection; wire it to `Chip`'s `onDismiss`. +- `FloatingActions` defaults to `variant="floating"` (`position: fixed`, bottom-center), so no positioning CSS is needed at the call site. To scope the bar to the table region rather than the viewport, give an ancestor `transform`, `filter`, or `contain: paint` so it becomes the containing block for `position: fixed`. Add `padding-bottom` on `DataTable.Content`'s scroll container if rows would otherwise sit behind the bar. +- When DataTable ships first-class row selection, this pattern will migrate to a selection-aware helper; the user-level API (the chip + buttons inside `FloatingActions`) stays the same. + ## Accessibility - Uses semantic `table`, `thead`, `tbody`, `tr`, `th`, and `td` elements diff --git a/apps/www/src/content/docs/components/floating-actions/demo.ts b/apps/www/src/content/docs/components/floating-actions/demo.ts index 23ef0b0a8..59f2d7683 100644 --- a/apps/www/src/content/docs/components/floating-actions/demo.ts +++ b/apps/www/src/content/docs/components/floating-actions/demo.ts @@ -47,22 +47,65 @@ export const inlineDemo = { ` }; -export const floatingDemo = { +export const variantsDemo = { type: 'code', - code: ` - } - isDismissible - > - 2 selected - - - - -` + tabs: [ + { + name: 'Floating', + code: `
+
+
+
+ + } + isDismissible + > + 2 selected + + + + + +
` + }, + { + name: 'Inline', + code: `
+ + } + isDismissible + > + 2 selected + + + + + +
` + } + ] }; export const bulkActionsDemo = { @@ -89,7 +132,11 @@ export const bulkActionsDemo = { export const iconOnlyDemo = { type: 'code', code: `
- + @@ -102,33 +149,25 @@ export const scrollingDemo = { type: 'code', code: ` function ScrollingDemo() { - const rows = Array.from({ length: 20 }, (_, i) => i + 1); return (
- - {rows.map(row => ( -
- Row {row} -
- ))} -
+
+
+
+ `
+
+ + } + isDismissible + > + 2 selected + + + + + +
`; + +export const sideDemo = { + type: 'code', + tabs: [ + { name: 'Bottom', code: placementWrapper('side="bottom"') }, + { name: 'Top', code: placementWrapper('side="top"') } + ] +}; + +export const alignDemo = { + type: 'code', + tabs: [ + { name: 'Start', code: placementWrapper('align="start"') }, + { name: 'Center', code: placementWrapper('align="center"') }, + { name: 'End', code: placementWrapper('align="end"') } + ] +}; + export const groupDemo = { type: 'code', code: `
diff --git a/apps/www/src/content/docs/components/floating-actions/index.mdx b/apps/www/src/content/docs/components/floating-actions/index.mdx index 4b9d7daf8..1dad149b9 100644 --- a/apps/www/src/content/docs/components/floating-actions/index.mdx +++ b/apps/www/src/content/docs/components/floating-actions/index.mdx @@ -5,7 +5,7 @@ source: packages/raystack/components/floating-actions tag: new --- -import { preview, floatingDemo, inlineDemo, bulkActionsDemo, iconOnlyDemo, groupDemo, scrollingDemo } from "./demo.ts"; +import { preview, variantsDemo, sideDemo, alignDemo, bulkActionsDemo, iconOnlyDemo, groupDemo, scrollingDemo } from "./demo.ts"; @@ -48,17 +48,9 @@ A vertical divider sized to the bar's content. Built on top of [`Separator`](/do ## Variants -### Floating +`floating` (default) renders with `position: fixed` anchored to the viewport via `side` and `align` — use it when the bar should follow the user as the page scrolls (bulk-selection toolbars, contextual action trays). `inline` opts out of viewport positioning so the bar flows with surrounding content. -The default. Renders with `position: fixed` and is anchored to the viewport via `side` and `align`. Use this when the bar should follow the user as the page scrolls (bulk-selection toolbars, contextual action trays). - - - -### Inline - -Opt out of viewport positioning when you want the bar to flow with surrounding content (e.g. inside a card, table cell, or storybook frame). - - + ## Examples @@ -74,6 +66,18 @@ Use `IconButton` inside the bar for compact toolbars. +### Side + +`side` controls which vertical edge of the viewport the bar pins to. + + + +### Align + +`align` controls the horizontal alignment of the bar along the chosen `side`. + + + ### Grouped actions Use `FloatingActions.Group` to cluster related items so they navigate as one unit, separated from the rest of the bar by a `Separator`. @@ -86,6 +90,10 @@ The floating variant stays pinned to the viewport (or the nearest containing blo +### Use with DataTable + +For the row-selection pattern — rendering this bar over a `DataTable` and wiring it to selected rows — see [DataTable → Row selection](/docs/components/datatable#row-selection). + ## Accessibility - The root uses `role="toolbar"` (enforced by the Base UI Toolbar primitive) and is announced as a group of interactive controls. Keyboard focus moves between toolbar items with the arrow keys. diff --git a/packages/raystack/components/data-table/data-table.module.css b/packages/raystack/components/data-table/data-table.module.css index a092ca268..c681a36aa 100644 --- a/packages/raystack/components/data-table/data-table.module.css +++ b/packages/raystack/components/data-table/data-table.module.css @@ -1,5 +1,5 @@ .toolbar { - padding: var(--rs-space-3) 0; + padding: var(--rs-space-3); align-self: stretch; border-bottom: 0.5px solid var(--rs-color-border-base-primary);