@rackbops/ui-react 0.2.32 → 0.2.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DataTable.d.ts +10 -0
- package/dist/DataTable.js +4 -1
- package/dist/Tabs.d.ts +16 -2
- package/dist/Tabs.js +25 -8
- package/package.json +1 -1
package/dist/DataTable.d.ts
CHANGED
|
@@ -6,6 +6,16 @@ export interface DataTableColumn<T> {
|
|
|
6
6
|
render: (row: T) => ReactNode;
|
|
7
7
|
/** Right-aligns the cell (rb-num, matching the existing numeric-column class). */
|
|
8
8
|
numeric?: boolean;
|
|
9
|
+
/** CSS width for the column (`"20%"`, `"12rem"`), rendered as `<col style="width: ...">` in a
|
|
10
|
+
* `<colgroup>` ahead of `<thead>`. Declaring a width on any column also puts the table in
|
|
11
|
+
* `table-layout: fixed`, so the widths are honoured and stay identical across table instances
|
|
12
|
+
* instead of each auto-sizing from its own content (kenzen#70). Columns without a width share
|
|
13
|
+
* the remaining space. An empty string is treated as no width, not a zero-width column.
|
|
14
|
+
* Combined with `sticky`, `table-layout: fixed` also stops the table from growing past its
|
|
15
|
+
* scroll wrapper, so content that needed horizontal scrolling under `sticky` alone will instead
|
|
16
|
+
* wrap once a width is declared (no theme sets `white-space: nowrap` or hides overflow on a
|
|
17
|
+
* cell, so it never clips) -- pick column widths wide enough for their content. */
|
|
18
|
+
width?: string;
|
|
9
19
|
/** Omit to make the column unsortable. Returning a number sorts numerically; a string sorts
|
|
10
20
|
* lexicographically (case-insensitive). */
|
|
11
21
|
sortValue?: (row: T) => string | number;
|
package/dist/DataTable.js
CHANGED
|
@@ -82,7 +82,10 @@ export function DataTable({ columns, rows, rowKey, groupBy, groupOrder, defaultS
|
|
|
82
82
|
});
|
|
83
83
|
})()
|
|
84
84
|
: [{ label: null, rows: sortRows(rows, columns, sort) }];
|
|
85
|
-
|
|
85
|
+
// Truthy, not `!== undefined`: an explicitly empty width ("") is "no width", not a
|
|
86
|
+
// zero-width column, so it must not flip the table into fixed layout on its own.
|
|
87
|
+
const hasWidths = columns.some((c) => c.width);
|
|
88
|
+
const table = (_jsxs("table", { className: "rb-table", style: hasWidths ? { tableLayout: "fixed" } : undefined, children: [hasWidths && (_jsx("colgroup", { children: columns.map((column) => (_jsx("col", { style: { width: column.width } }, column.key))) })), _jsx("thead", { children: _jsx("tr", { children: columns.map((column) => {
|
|
86
89
|
const isSorted = sort?.key === column.key;
|
|
87
90
|
// `aria-sort` claims a table-wide order; grouping only sorts within each group, so
|
|
88
91
|
// asserting it there would tell a screen-reader user something false.
|
package/dist/Tabs.d.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { type HTMLAttributes, type ReactNode, type RefAttributes } from "react";
|
|
2
2
|
export interface TabItem {
|
|
3
|
+
/** Unique among this tablist's items -- a duplicate id renders two "selected" tabs at once,
|
|
4
|
+
* breaking the "exactly one tab is always selected" guarantee below. */
|
|
3
5
|
id: string;
|
|
4
6
|
label: ReactNode;
|
|
5
7
|
content: ReactNode;
|
|
6
8
|
}
|
|
7
|
-
export interface TabsProps extends HTMLAttributes<HTMLDivElement>, RefAttributes<HTMLDivElement> {
|
|
9
|
+
export interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange">, RefAttributes<HTMLDivElement> {
|
|
8
10
|
items: TabItem[];
|
|
9
|
-
/**
|
|
11
|
+
/** Uncontrolled mode: the initially active tab id; defaults to the first item. Ignored when
|
|
12
|
+
* `activeId` is set. */
|
|
10
13
|
defaultId?: string;
|
|
14
|
+
/** Controlled mode, mirroring NavRail's `activeId`: the active tab id, owned by the caller.
|
|
15
|
+
* When set the component keeps no selection state of its own -- `onChange` reports what the
|
|
16
|
+
* user picked and the parent re-renders with the new id (so it can live in a router or URL).
|
|
17
|
+
* An id matching no item selects the first tab, so exactly one tab is always selected.
|
|
18
|
+
* Switching between controlled and uncontrolled across the component's lifetime is not
|
|
19
|
+
* supported: the internal selection kept for uncontrolled mode is frozen (not updated) while
|
|
20
|
+
* `activeId` is set, so removing `activeId` later reverts to whatever it was last. */
|
|
21
|
+
activeId?: string;
|
|
22
|
+
/** Called with the id the user selected (click, arrow keys, Home/End) in both modes; in
|
|
23
|
+
* uncontrolled mode the internal selection also updates. */
|
|
24
|
+
onChange?: (id: string) => void;
|
|
11
25
|
}
|
|
12
26
|
export declare const Tabs: import("react").ForwardRefExoticComponent<Omit<TabsProps, "ref"> & RefAttributes<HTMLDivElement>>;
|
package/dist/Tabs.js
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useId, useRef, useState, } from "react";
|
|
3
3
|
import { cx } from "./cx.js";
|
|
4
|
-
export const Tabs = forwardRef(function Tabs({ items, defaultId, className, ...rest }, ref) {
|
|
4
|
+
export const Tabs = forwardRef(function Tabs({ items, defaultId, activeId: activeIdProp, onChange, className, ...rest }, ref) {
|
|
5
5
|
const base = useId();
|
|
6
|
-
const [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const [internal, setInternal] = useState(defaultId ?? items[0]?.id);
|
|
7
|
+
const controlled = activeIdProp !== undefined;
|
|
8
|
+
const requested = controlled ? activeIdProp : internal;
|
|
9
|
+
// Fall back to the first tab if `requested` names no item, so exactly one tab is
|
|
10
|
+
// always selected (a defaultId or activeId that matches nothing still highlights a tab).
|
|
11
|
+
const activeId = items.some((t) => t.id === requested) ? requested : items[0]?.id;
|
|
12
|
+
const select = (id) => {
|
|
13
|
+
if (!controlled)
|
|
14
|
+
setInternal(id);
|
|
15
|
+
onChange?.(id);
|
|
16
|
+
};
|
|
10
17
|
const tabRefs = useRef([]);
|
|
11
18
|
const onKeyDown = (e) => {
|
|
12
|
-
|
|
19
|
+
// Index from the tab that received the event, not from `activeId`: a keydown is delivered
|
|
20
|
+
// to document.activeElement, and this handler is bound to the <button> itself, so
|
|
21
|
+
// e.currentTarget IS the focused tab on every press, the first included -- regardless of
|
|
22
|
+
// whether selection and focus currently agree. A controlled parent that commits the new
|
|
23
|
+
// activeId asynchronously (a router transition, a debounce) would otherwise leave `activeId`
|
|
24
|
+
// unchanged between keystrokes, so every consecutive press would recompute the same "next"
|
|
25
|
+
// tab from the same stale index instead of advancing (#169 review round 1, MAJOR finding).
|
|
26
|
+
const i = tabRefs.current.indexOf(e.currentTarget);
|
|
13
27
|
let next = i;
|
|
14
28
|
if (e.key === "ArrowRight" || e.key === "ArrowDown")
|
|
15
29
|
next = (i + 1) % items.length;
|
|
@@ -22,7 +36,10 @@ export const Tabs = forwardRef(function Tabs({ items, defaultId, className, ...r
|
|
|
22
36
|
else
|
|
23
37
|
return;
|
|
24
38
|
e.preventDefault();
|
|
25
|
-
|
|
39
|
+
// Safe: onKeyDown only fires from a rendered tab button, so items is non-empty and next is
|
|
40
|
+
// always a valid index into it (arithmetic above is mod items.length, or an explicit 0 /
|
|
41
|
+
// items.length - 1 for Home/End).
|
|
42
|
+
select(items[next].id);
|
|
26
43
|
// Move focus with the selection — the whole point of the roving tabindex.
|
|
27
44
|
tabRefs.current[next]?.focus();
|
|
28
45
|
};
|
|
@@ -30,7 +47,7 @@ export const Tabs = forwardRef(function Tabs({ items, defaultId, className, ...r
|
|
|
30
47
|
const selected = t.id === activeId;
|
|
31
48
|
return (_jsx("button", { ref: (el) => {
|
|
32
49
|
tabRefs.current[idx] = el;
|
|
33
|
-
}, type: "button", role: "tab", id: `${base}-tab-${t.id}`, "aria-selected": selected, "aria-controls": `${base}-panel-${t.id}`, tabIndex: selected ? 0 : -1, className: cx("rb-tab", selected && "rb-tab--active"), onClick: () =>
|
|
50
|
+
}, type: "button", role: "tab", id: `${base}-tab-${t.id}`, "aria-selected": selected, "aria-controls": `${base}-panel-${t.id}`, tabIndex: selected ? 0 : -1, className: cx("rb-tab", selected && "rb-tab--active"), onClick: () => select(t.id), onKeyDown: onKeyDown, children: t.label }, t.id));
|
|
34
51
|
}) }), items.map((t) => {
|
|
35
52
|
const selected = t.id === activeId;
|
|
36
53
|
return (_jsx("div", { className: "rb-tabpanel", role: "tabpanel", id: `${base}-panel-${t.id}`, "aria-labelledby": `${base}-tab-${t.id}`, hidden: !selected, tabIndex: 0, children: t.content }, t.id));
|