mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
feat(opal): support alignment overrides in Table cells and headers (#13768)
This commit is contained in:
1 parent
0fbb1b8764
commit
a9df89bffa
10 files changed
+106
-25
No files matched your search
@@ -63,7 +63,11 @@ function Calendar(props: CalendarProps) {
|
||||
style={undefined}
|
||||
styles={undefined}
|
||||
formatters={undefined}
|
||||
modifiersClassNames={undefined}
|
||||
modifiersClassNames={{
|
||||
range_preview_start: "opal-calendar-cell--range-preview-start",
|
||||
range_preview_middle: "opal-calendar-cell--range-preview-middle",
|
||||
range_preview_end: "opal-calendar-cell--range-preview-end",
|
||||
}}
|
||||
modifiersStyles={undefined}
|
||||
// Outside days render as empty fixed-width cells, never as numerals.
|
||||
showOutsideDays={false}
|
||||
|
||||
@@ -91,7 +91,10 @@
|
||||
neighbor-facing side. Week edges stay flush and rounded. */
|
||||
.opal-calendar-cell--range-start::before,
|
||||
.opal-calendar-cell--range-middle::before,
|
||||
.opal-calendar-cell--range-end::before {
|
||||
.opal-calendar-cell--range-end::before,
|
||||
.opal-calendar-cell--range-preview-start::before,
|
||||
.opal-calendar-cell--range-preview-middle::before,
|
||||
.opal-calendar-cell--range-preview-end::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -103,7 +106,10 @@
|
||||
|
||||
.opal-calendar-cell--range-start::before,
|
||||
.opal-calendar-cell--range-middle:first-child::before,
|
||||
.opal-calendar-cell--range-end:first-child::before {
|
||||
.opal-calendar-cell--range-end:first-child::before,
|
||||
.opal-calendar-cell--range-preview-start::before,
|
||||
.opal-calendar-cell--range-preview-middle:first-child::before,
|
||||
.opal-calendar-cell--range-preview-end:first-child::before {
|
||||
left: 0;
|
||||
border-top-left-radius: var(--radius-08);
|
||||
border-bottom-left-radius: var(--radius-08);
|
||||
@@ -111,7 +117,10 @@
|
||||
|
||||
.opal-calendar-cell--range-end::before,
|
||||
.opal-calendar-cell--range-middle:last-child::before,
|
||||
.opal-calendar-cell--range-start:last-child::before {
|
||||
.opal-calendar-cell--range-start:last-child::before,
|
||||
.opal-calendar-cell--range-preview-end::before,
|
||||
.opal-calendar-cell--range-preview-middle:last-child::before,
|
||||
.opal-calendar-cell--range-preview-start:last-child::before {
|
||||
right: 0;
|
||||
border-top-right-radius: var(--radius-08);
|
||||
border-bottom-right-radius: var(--radius-08);
|
||||
|
||||
@@ -78,7 +78,7 @@ function DragOverlayRowInner<TData>({
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCell key={cell.id}>
|
||||
<TableCell key={cell.id} alignment={colDef?.alignment}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
);
|
||||
|
||||
@@ -48,23 +48,24 @@ function UsersTable({ users }: { users: User[] }) {
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `data` | `TData[]` | required | Row data array |
|
||||
| `columns` | `OnyxColumnDef<TData>[]` | required | Column definitions from `createTableColumns()` |
|
||||
| `getRowId` | `(row: TData) => string` | required | Unique row identifier |
|
||||
| `pageSize` | `number` | `10` | Rows per page (`Infinity` disables pagination) |
|
||||
| `size` | `"md" \| "lg"` | `"lg"` | Density variant |
|
||||
| `footer` | `DataTableFooterConfig` | — | Footer configuration (mode is derived from `selectionBehavior`) |
|
||||
| `initialSorting` | `SortingState` | — | Initial sort state |
|
||||
| `initialColumnVisibility` | `VisibilityState` | — | Initial column visibility |
|
||||
| `draggable` | `DataTableDraggableConfig` | — | Enable drag-and-drop reordering |
|
||||
| `onSelectionChange` | `(ids: string[]) => void` | — | Selection callback |
|
||||
| `onRowClick` | `(row: TData) => void` | — | Row click handler |
|
||||
| `searchTerm` | `string` | — | Global text filter |
|
||||
| `height` | `number \| string` | — | Max scrollable height |
|
||||
| `serverSide` | `ServerSideConfig` | — | Server-side pagination/sorting/filtering |
|
||||
| `emptyState` | `ReactNode` | — | Empty state content |
|
||||
| Prop | Type | Default | Description |
|
||||
| ------------------------- | -------------------------- | -------- | --------------------------------------------------------------- |
|
||||
| `data` | `TData[]` | required | Row data array |
|
||||
| `columns` | `OnyxColumnDef<TData>[]` | required | Column definitions from `createTableColumns()` |
|
||||
| `getRowId` | `(row: TData) => string` | required | Unique row identifier |
|
||||
| `pageSize` | `number` | `10` | Rows per page (`Infinity` disables pagination) |
|
||||
| `size` | `"md" \| "lg"` | `"lg"` | Density variant |
|
||||
| `footer` | `DataTableFooterConfig` | — | Footer configuration (mode is derived from `selectionBehavior`) |
|
||||
| `initialSorting` | `SortingState` | — | Initial sort state |
|
||||
| `initialColumnVisibility` | `VisibilityState` | — | Initial column visibility |
|
||||
| `draggable` | `DataTableDraggableConfig` | — | Enable drag-and-drop reordering |
|
||||
| `onSelectionChange` | `(ids: string[]) => void` | — | Selection callback |
|
||||
| `onRowClick` | `(row: TData) => void` | — | Row click handler |
|
||||
| `getRowLabel` | `(row: TData) => string` | — | Accessible action label for clickable rows |
|
||||
| `searchTerm` | `string` | — | Global text filter |
|
||||
| `height` | `number \| string` | — | Max scrollable height |
|
||||
| `serverSide` | `ServerSideConfig` | — | Server-side pagination/sorting/filtering |
|
||||
| `emptyState` | `ReactNode` | — | Empty state content |
|
||||
|
||||
## Column Builder
|
||||
|
||||
@@ -78,5 +79,6 @@ function UsersTable({ users }: { users: User[] }) {
|
||||
## Footer
|
||||
|
||||
The footer mode is derived automatically from `selectionBehavior`:
|
||||
|
||||
- **Selection footer** (when `selectionBehavior` is `"single-select"` or `"multi-select"`) — shows selection count, optional view/clear buttons, count pagination
|
||||
- **Summary footer** (when `selectionBehavior` is `"no-select"` or omitted) — shows "Showing X\~Y of Z", list pagination, optional extra element
|
||||
@@ -8,10 +8,18 @@ interface TableCellProps extends WithoutStyles<
|
||||
children: React.ReactNode;
|
||||
/** Explicit pixel width for the cell. */
|
||||
width?: number;
|
||||
alignment?: "left" | "center" | "right";
|
||||
}
|
||||
|
||||
const alignmentJustifyClass = {
|
||||
left: "justify-start",
|
||||
center: "justify-center",
|
||||
right: "justify-end",
|
||||
} as const;
|
||||
|
||||
export default function TableCell({
|
||||
width,
|
||||
alignment = "left",
|
||||
children,
|
||||
...props
|
||||
}: TableCellProps) {
|
||||
@@ -24,7 +32,11 @@ export default function TableCell({
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn("tbl-cell-inner", "flex items-center overflow-hidden")}
|
||||
className={cn(
|
||||
"tbl-cell-inner",
|
||||
"flex items-center overflow-hidden",
|
||||
alignmentJustifyClass[alignment]
|
||||
)}
|
||||
data-size={resolvedSize}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -87,7 +87,13 @@ export default function TableHead({
|
||||
data-size={resolvedSize}
|
||||
data-bottom-border={bottomBorder || undefined}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1",
|
||||
alignment === "right" && "justify-end",
|
||||
alignment === "center" && "justify-center"
|
||||
)}
|
||||
>
|
||||
<div className="table-head-label">
|
||||
<Text
|
||||
font={isSmall ? "secondary-action" : "main-ui-action"}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type CellContext,
|
||||
} from "@tanstack/react-table";
|
||||
import type {
|
||||
ColumnAlignment,
|
||||
ColumnWidth,
|
||||
QualifierContentType,
|
||||
OnyxQualifierColumn,
|
||||
@@ -58,6 +59,7 @@ interface DataColumnConfig<TData, TValue> {
|
||||
weight?: number;
|
||||
/** Explicit column ID. Derived from the accessor key; required for function accessors. */
|
||||
id?: string;
|
||||
alignment?: ColumnAlignment;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -75,6 +77,7 @@ interface DisplayColumnConfig<TData> {
|
||||
width: ColumnWidth;
|
||||
/** Enable hiding. @default true */
|
||||
enableHiding?: boolean;
|
||||
alignment?: ColumnAlignment;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -188,6 +191,7 @@ export function createTableColumns<TData>(): TableColumnsBuilder<TData> {
|
||||
icon,
|
||||
weight = 20,
|
||||
id: explicitId,
|
||||
alignment,
|
||||
} = config;
|
||||
|
||||
const id = explicitId ?? (accessor as string);
|
||||
@@ -210,13 +214,21 @@ export function createTableColumns<TData>(): TableColumnsBuilder<TData> {
|
||||
def,
|
||||
width: { weight, minWidth: Math.max(header.length * 8 + 40, 80) },
|
||||
icon,
|
||||
alignment,
|
||||
};
|
||||
},
|
||||
|
||||
displayColumn(
|
||||
config: DisplayColumnConfig<TData>
|
||||
): OnyxDisplayColumn<TData> {
|
||||
const { id, header, cell, width, enableHiding = true } = config;
|
||||
const {
|
||||
id,
|
||||
header,
|
||||
cell,
|
||||
width,
|
||||
enableHiding = true,
|
||||
alignment,
|
||||
} = config;
|
||||
|
||||
const def: ColumnDef<TData, any> = helper.display({
|
||||
id,
|
||||
@@ -232,6 +244,7 @@ export function createTableColumns<TData>(): TableColumnsBuilder<TData> {
|
||||
id,
|
||||
def,
|
||||
width,
|
||||
alignment,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ export function Table<TData>(props: DataTableProps<TData>) {
|
||||
selectionBehavior = "no-select",
|
||||
onSelectionChange,
|
||||
onRowClick,
|
||||
getRowLabel,
|
||||
searchTerm,
|
||||
height,
|
||||
serverSide,
|
||||
@@ -447,6 +448,7 @@ export function Table<TData>(props: DataTableProps<TData>) {
|
||||
<TableHead
|
||||
key={header.id}
|
||||
width={columnWidths[header.id]}
|
||||
alignment={colDef?.alignment}
|
||||
sorted={
|
||||
canSort ? toOnyxSortDirection(sortDir) : undefined
|
||||
}
|
||||
@@ -511,6 +513,22 @@ export function Table<TData>(props: DataTableProps<TData>) {
|
||||
key={row.id}
|
||||
sortableId={rowId}
|
||||
selected={row.getIsSelected()}
|
||||
data-clickable={onRowClick ? true : undefined}
|
||||
role={onRowClick ? "button" : undefined}
|
||||
tabIndex={onRowClick ? 0 : undefined}
|
||||
aria-label={
|
||||
onRowClick ? getRowLabel?.(row.original) : undefined
|
||||
}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
onRowClick &&
|
||||
event.currentTarget === event.target &&
|
||||
(event.key === "Enter" || event.key === " ")
|
||||
) {
|
||||
event.preventDefault();
|
||||
onRowClick(row.original);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (
|
||||
hasDraggable &&
|
||||
@@ -589,6 +607,9 @@ export function Table<TData>(props: DataTableProps<TData>) {
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
data-column-id={cell.column.id}
|
||||
alignment={
|
||||
columnKindMap.get(cell.column.id)?.alignment
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
|
||||
@@ -128,6 +128,13 @@ table[data-variant="cards"] .tbl-row:has(:focus-visible) > td {
|
||||
@apply bg-action-selection-01;
|
||||
}
|
||||
|
||||
.tbl-row[data-clickable] {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
.tbl-row[data-clickable]:hover > td {
|
||||
@apply bg-background-tint-02;
|
||||
}
|
||||
|
||||
/* ---- QualifierContainer ---- */
|
||||
|
||||
.tbl-qualifier[data-type="head"] {
|
||||
|
||||
@@ -38,6 +38,8 @@ export type OnyxColumnKind = "qualifier" | "data" | "display" | "actions";
|
||||
// Column definitions (discriminated union on `kind`)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type ColumnAlignment = "left" | "center" | "right";
|
||||
|
||||
interface OnyxColumnBase<TData> {
|
||||
kind: OnyxColumnKind;
|
||||
/** Stable column identifier (mirrors the TanStack column ID). */
|
||||
@@ -49,6 +51,7 @@ interface OnyxColumnBase<TData> {
|
||||
/** Qualifier column — leading avatar/icon/checkbox column. */
|
||||
export interface OnyxQualifierColumn<TData> extends OnyxColumnBase<TData> {
|
||||
kind: "qualifier";
|
||||
alignment?: never;
|
||||
/** Content type for body-row `<TableQualifier>`. */
|
||||
content: QualifierContentType;
|
||||
/** Return the icon component to render for a row (for "icon" content). */
|
||||
@@ -66,6 +69,7 @@ export interface OnyxQualifierColumn<TData> extends OnyxColumnBase<TData> {
|
||||
/** Data column — accessor-based column with sorting/resizing. */
|
||||
export interface OnyxDataColumn<TData> extends OnyxColumnBase<TData> {
|
||||
kind: "data";
|
||||
alignment?: ColumnAlignment;
|
||||
/** Override the sort icon for this column. */
|
||||
icon?: (sorted: SortDirection) => IconFunctionComponent;
|
||||
}
|
||||
@@ -73,11 +77,13 @@ export interface OnyxDataColumn<TData> extends OnyxColumnBase<TData> {
|
||||
/** Display column — non-accessor column with custom rendering. */
|
||||
export interface OnyxDisplayColumn<TData> extends OnyxColumnBase<TData> {
|
||||
kind: "display";
|
||||
alignment?: ColumnAlignment;
|
||||
}
|
||||
|
||||
/** Actions column — fixed column with visibility/sorting popovers. */
|
||||
export interface OnyxActionsColumn<TData> extends OnyxColumnBase<TData> {
|
||||
kind: "actions";
|
||||
alignment?: never;
|
||||
/** Show column visibility popover. @default true */
|
||||
showColumnVisibility?: boolean;
|
||||
/** Show sorting popover. @default true */
|
||||
@@ -162,6 +168,7 @@ export interface DataTableProps<TData> {
|
||||
onSelectionChange?: (selectedIds: string[]) => void;
|
||||
/** Called when a row is clicked (replaces the default selection toggle). */
|
||||
onRowClick?: (row: TData) => void;
|
||||
getRowLabel?: (row: TData) => string;
|
||||
/** Search term for global text filtering. When provided, rows are filtered
|
||||
* to those containing the term in any accessor column value (case-insensitive). */
|
||||
searchTerm?: string;
|
||||
|
||||
Reference in new issue
Block a user