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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,57 @@

A simple table component for React.

## How to

### Render columns

It is advisable to define a columns variable with `useMemo()` so that it will
Copy link
Contributor

Choose a reason for hiding this comment

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

While of course this is good etiquette, I'm not sure I agree that we should push for memoization.

In my experience, it's almost never worth it in terms of performance, and often causes confusion for our colleagues when they don't quite understand the purpose and effects of it.

In the projects that uses this table, we don't really use memoization because of these points... Maybe we should do a "react hooks deep-dive" tech-talk to cover this? I could prepare one if you'd like.

not be re-generated on each component render.
But take care what value you use to watch for changes!

When working with async data with Tanstack query, it is best to use the `data`
attribute from useQuery.

```tsx
const { data } = useQuery({ /* options */ })
const columns = useMemo(() => [
{
key: "name",
title: (state) => "User name",
/* etc */
},
], [data]); // <-- columns will only rerender when there's new data
```

### Sorting

The data you provide to the table determines the content and order of the rows.
**Any of the `sort`-related props are only intended for displaying a sort state in the table header.**

```tsx
const columns = useMemo(() => [
{
title: (state) => (
<>
{/* Only show an error when it is the sorted column and point it to the right direction */}
User Name {state.isSortedColumn
? state.direction === "Asc"
? "↓"
: "↑"
: ""}
</>
),
/* etc */
},
], [data]);
```

The `onSorted` callback will be fired after you click a sortable header.
Use it to trigger a change of the data you pass to the table.

Note that currently, when selecting a new column it will always default to `"Desc"`.
**This is rather limiting**: a PR will be made to fix this.

## Development

This component was developed [using Bun for running & package management](https://bun.sh), [BiomeJS for formatting and linting](https://biomejs.dev) [and `vite` for hosting](https://vite.dev). Run it locally using `bun run dev`.
8 changes: 3 additions & 5 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": ["node_modules", "dist"]
"includes": ["**", "!**/node_modules", "!**/dist"]
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"organizeImports": {
"enabled": true
},
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"linter": {
"enabled": true,
"rules": {
Expand Down
Binary file modified bun.lockb
100644 → 100755
Binary file not shown.
5 changes: 1 addition & 4 deletions dist/components/table/table-sort-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import '../../assets/table-sort-button.css';const n = "_button_158ku_1", r = "_s
sorted: r,
desc: d,
asc: _
}, i = ({
state: o,
children: s
}) => /* @__PURE__ */ c(
}, i = ({ state: o, children: s }) => /* @__PURE__ */ c(
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this is biome acting up again, but there shouldn't be any formatting changes in generated files from dist/*

"div",
{
className: e([
Expand Down
2 changes: 1 addition & 1 deletion dist/components/table/table.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ type TableProps<T> = Readonly<{
/** An element that renders its `data` in a grid-like fashion, with native
* HTML table elements underneath to keep it accessible.
*/
declare const Table: <T extends {}>({ data, columns, makeKey, className, empty }: TableProps<T>) => import("react/jsx-runtime").JSX.Element;
declare const Table: <T extends {}>({ data, columns, makeKey, className, empty, }: TableProps<T>) => import("react/jsx-runtime").JSX.Element;
export default Table;
64 changes: 31 additions & 33 deletions dist/components/table/table.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,82 @@
import { jsxs as p, jsx as r } from "react/jsx-runtime";
import b from "classnames";
import { useState as u } from "react";
import '../../assets/table.css';const k = "_table_1hpbm_1", _ = "_sticky_1hpbm_39", x = "_header_1hpbm_45", a = {
table: k,
sticky: _,
header: x
}, C = ({
data: i,
import { jsxs as b, jsx as n } from "react/jsx-runtime";
import k from "classnames";
import { useState as _ } from "react";
import '../../assets/table.css';const x = "_table_1hpbm_1", v = "_sticky_1hpbm_39", D = "_header_1hpbm_45", a = {
table: x,
sticky: v,
header: D
}, j = ({
data: r,
columns: t,
makeKey: n,
makeKey: d,
className: o,
empty: f
}) => {
const [d, h] = m(t);
return /* @__PURE__ */ p(
const [i, S] = N(t);
return /* @__PURE__ */ b(
"table",
{
cellSpacing: "0",
className: b([a.table, o]),
className: k([a.table, o]),
style: {
// @ts-ignore Typescript doesnt like custom css vars
"--columns": t.length
},
children: [
/* @__PURE__ */ r("thead", { children: /* @__PURE__ */ r("tr", { children: t.map((e, c) => {
const s = c === (d == null ? void 0 : d.index), S = d == null ? void 0 : d.direction, y = e.onSorted ? "button" : "div";
return /* @__PURE__ */ r(
/* @__PURE__ */ n("thead", { children: /* @__PURE__ */ n("tr", { children: t.map((e, c) => {
const s = c === (i == null ? void 0 : i.index), y = i == null ? void 0 : i.direction, p = e.onSorted ? "button" : "div";
return /* @__PURE__ */ n(
"th",
{
scope: "col",
className: e.sticky ? a.sticky : void 0,
children: /* @__PURE__ */ r(
y,
children: /* @__PURE__ */ n(
p,
{
type: e.onSorted && "button",
className: a.header,
onClick: e.onSorted ? async () => {
const l = !s || (d == null ? void 0 : d.direction) !== "Desc" ? "Desc" : "Asc";
await e.onSorted(
l
), h({
var h;
const l = !s || (i == null ? void 0 : i.direction) !== "Desc" ? "Desc" : "Asc";
await ((h = e.onSorted) == null ? void 0 : h.call(e, l)), S({
index: c,
direction: l
});
} : void 0,
children: typeof e.title == "function" ? e.title({
isSortedColumn: s,
direction: S
direction: y
}) : e.title
}
)
},
e.key
);
}) }) }),
/* @__PURE__ */ r("tbody", { children: i != null && i.length ? i.map((e, c) => /* @__PURE__ */ r("tr", { children: t.map((s) => /* @__PURE__ */ r(
/* @__PURE__ */ n("tbody", { children: r != null && r.length ? r.map((e, c) => /* @__PURE__ */ n("tr", { children: t.map((s) => /* @__PURE__ */ n(
"td",
{
className: s.sticky ? a.sticky : void 0,
children: s.render(e)
},
s.key
)) }, n(e, c))) : f })
)) }, d(e, c))) : f })
]
}
);
}, m = (i) => u(() => {
const t = i.findIndex(
}, N = (r) => _(() => {
const t = r.findIndex(
(o) => o.defaultSorted
);
if (t === -1) return;
const n = i[t];
return n.onSorted ? {
direction: n.defaultSorted === !0 ? "Desc" : n.defaultSorted || "Desc",
const d = r[t];
return d.onSorted ? {
direction: d.defaultSorted === !0 ? "Desc" : d.defaultSorted || "Desc",
index: t
} : {
index: t,
direction: typeof n.defaultSorted == "string" ? n.defaultSorted : void 0
direction: typeof d.defaultSorted == "string" ? d.defaultSorted : void 0
};
});
export {
C as default
j as default
};
2 changes: 1 addition & 1 deletion dist/main.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { default as Table, type ColumnState, type OrderDirection, type TableColumn } from './components/table/table';
export { type ColumnState, default as Table, type OrderDirection, type TableColumn, } from './components/table/table';
export { default as TableSortButton } from './components/table/table-sort-button';
7 changes: 7 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pre-commit:
parallel: true
jobs:
- run: bun run biome check --no-errors-on-unmatched --files-ignore-unknown=true {staged_files}
- run: bun run prettier --check {staged_files}
glob: "**/*.{s,}css"

12 changes: 6 additions & 6 deletions lib/components/table/table-sort-button.module.scss
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
.button {
transition: all 0.1s ease;
opacity: 0.5;
display: inline-block;
transition: all 0.1s ease;
opacity: 0.5;
display: inline-block;
}

.sorted {
opacity: 1;
opacity: 1;
}

.desc {
transform: rotateX(0deg);
transform: rotateX(0deg);
}

.asc {
transform: rotateX(180deg);
transform: rotateX(180deg);
}
15 changes: 6 additions & 9 deletions lib/components/table/table-sort-button.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import classNames from "classnames";
import { ReactNode } from "react";
import { ColumnState } from "./table";
import type { ReactNode } from "react";
import type { ColumnState } from "./table";
import style from "./table-sort-button.module.scss";

type TableSortButtonProps = Readonly<{
state: ColumnState;
children?: ReactNode;
}>;

const TableSortButton = ({
state,
children
}: TableSortButtonProps) => (
const TableSortButton = ({ state, children }: TableSortButtonProps) => (
<div
className={classNames([
style.button,
state.isSortedColumn && style.sorted,
state.direction &&
style[state.direction.toLowerCase()]
])}>
state.direction && style[state.direction.toLowerCase()],
])}
>
{children}
</div>
);
Expand Down
105 changes: 51 additions & 54 deletions lib/components/table/table.module.scss
Original file line number Diff line number Diff line change
@@ -1,62 +1,59 @@
.table {
display: grid;
grid-template-columns: repeat(
var(--columns),
minmax(max-content, 1fr)
);
place-items: center;
overflow-x: auto;
grid-auto-rows: min-content;

// Prevent depth/z-plane fighting
isolation: isolate;

thead,
tbody {
display: contents;
}

tr {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;

&:hover td {
background-color: var(--color-primary-095);
}
}

td {
padding: var(--space-x-small) var(--space-small);
display: flex;
flex-direction: row;
align-items: center;
}

th {
padding: var(--space-small) 0;
font-weight: unset;
background-color: var(--color-primary-095);
border-bottom: 2px solid var(--color-neutral-080);
}

td {
background: var(--color-white);
border-bottom: 2px solid var(--color-neutral-090);
font: var(--font-body-small-regular);
}
display: grid;
grid-template-columns: repeat(var(--columns), minmax(max-content, 1fr));
place-items: center;
overflow-x: auto;
grid-auto-rows: min-content;

// Prevent depth/z-plane fighting
isolation: isolate;

thead,
tbody {
display: contents;
}

tr {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;

&:hover td {
background-color: var(--color-primary-095);
}
}

td {
padding: var(--space-x-small) var(--space-small);
display: flex;
flex-direction: row;
align-items: center;
}

th {
padding: var(--space-small) 0;
font-weight: unset;
background-color: var(--color-primary-095);
border-bottom: 2px solid var(--color-neutral-080);
}

td {
background: var(--color-white);
border-bottom: 2px solid var(--color-neutral-090);
font: var(--font-body-small-regular);
}
}

.sticky {
position: sticky;
inset-inline-start: 0;
z-index: 1;
position: sticky;
inset-inline-start: 0;
z-index: 1;
}

.header {
width: 100%;
border: none;
background: none;
text-align: start;
padding: 0;
width: 100%;
border: none;
background: none;
text-align: start;
padding: 0;
}
Loading