@crisp-ui-kit/crisp 0.23.5 → 0.25.0
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/cjs/components/bulkbar/bulkbar.js +2 -2
- package/dist/cjs/components/radiogroup/radiogroup-keyboard.js +68 -2
- package/dist/cjs/components/radiogroup/radiogroup.js +2 -2
- package/dist/cjs/components/segmented/segmented.js +1 -1
- package/dist/cjs/components/tabs/tabs-keyboard.js +10 -1
- package/dist/cjs/product/recordstable/recordstable.js +14 -1
- package/dist/cjs/product/recordstable/recordstable.recipe.js +6 -0
- package/dist/esm/components/bulkbar/bulkbar.js +3 -3
- package/dist/esm/components/radiogroup/radiogroup-keyboard.js +35 -2
- package/dist/esm/components/radiogroup/radiogroup.js +2 -2
- package/dist/esm/components/segmented/segmented.js +1 -1
- package/dist/esm/components/tabs/tabs-keyboard.js +10 -1
- package/dist/esm/product/recordstable/recordstable.js +14 -1
- package/dist/esm/product/recordstable/recordstable.recipe.js +6 -0
- package/dist/recordstable.css +6 -0
- package/dist/styles.css +6 -0
- package/dist/types/components/bulkbar/bulkbar.d.ts +9 -1
- package/dist/types/components/radiogroup/radiogroup-keyboard.d.ts +9 -2
- package/dist/types/product/recordstable/recordstable.d.ts +8 -0
- package/package.json +1 -1
|
@@ -44,8 +44,8 @@ const bulkbar_recipe_1 = require("./bulkbar.recipe");
|
|
|
44
44
|
* positioned ancestor when a selection is active; render it conditionally
|
|
45
45
|
* (e.g. `count > 0 && <BulkBar …/>`). Measured from Attio's records table.
|
|
46
46
|
*/
|
|
47
|
-
function BulkBar({ count, onClear, countLabel = "selected", children, className, clearLabel = "Clear selection", ...rest }) {
|
|
48
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: (0, bulkbar_recipe_1.bulkbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "bulkbar-bar",
|
|
47
|
+
function BulkBar({ count, onClear, countLabel = "selected", countText = (count, label) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
|
|
48
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: (0, bulkbar_recipe_1.bulkbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "bulkbar-bar", children: [count != null && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
|
|
49
49
|
}
|
|
50
50
|
BulkBar.displayName = "BulkBar";
|
|
51
51
|
/** A styled action button for use inside a `<BulkBar>` (or as a `<Menu>` trigger). */
|
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.useRadioGroupKeyboard = useRadioGroupKeyboard;
|
|
37
|
+
const React = __importStar(require("react"));
|
|
4
38
|
/**
|
|
5
39
|
* The WAI-ARIA radio group keyboard model: ←/→/↑/↓ move focus to the next or
|
|
6
40
|
* previous enabled radio, wrapping, and check it on arrival. Home/End jump to
|
|
@@ -25,7 +59,39 @@ exports.useRadioGroupKeyboard = useRadioGroupKeyboard;
|
|
|
25
59
|
* `Segmented` already offered them, and the shared model should be the union
|
|
26
60
|
* rather than quietly dropping behaviour one surface had.
|
|
27
61
|
*/
|
|
28
|
-
function useRadioGroupKeyboard({ select, }) {
|
|
62
|
+
function useRadioGroupKeyboard({ select, groupRef: externalRef, }) {
|
|
63
|
+
/**
|
|
64
|
+
* The group's one tab stop. `tabIndex={checked ? 0 : -1}` in the markup is
|
|
65
|
+
* right for every case but the one that matters most: a group with NOTHING
|
|
66
|
+
* selected gives every radio -1, so Tab skips the whole control and an
|
|
67
|
+
* unanswered question can never be answered from the keyboard (#237). APG
|
|
68
|
+
* says the first radio is tabbable when none is checked, and RadioGroup's
|
|
69
|
+
* own comment said so too — "the selected (or first, when none) radio is
|
|
70
|
+
* tabbable" — while the code did only the first half.
|
|
71
|
+
*
|
|
72
|
+
* Owned here rather than in the markup because the group renders arbitrary
|
|
73
|
+
* children: only the DOM knows which radio comes first, which is the same
|
|
74
|
+
* reason the arrow keys read the radios out of the DOM.
|
|
75
|
+
*
|
|
76
|
+
* It runs after EVERY render, and sets both ends. React will not undo a
|
|
77
|
+
* mutation it did not make — when the selection moves, the newly checked
|
|
78
|
+
* radio changes 0 from React's side while the stale one keeps the 0 written
|
|
79
|
+
* here — so this has to own the whole row, not just add to it.
|
|
80
|
+
*/
|
|
81
|
+
const ownRef = React.useRef(null);
|
|
82
|
+
const groupRef = externalRef ?? ownRef;
|
|
83
|
+
React.useLayoutEffect(() => {
|
|
84
|
+
const root = groupRef.current;
|
|
85
|
+
if (!root)
|
|
86
|
+
return;
|
|
87
|
+
const radios = Array.from(root.querySelectorAll('[role="radio"]:not([disabled])'));
|
|
88
|
+
if (!radios.length)
|
|
89
|
+
return;
|
|
90
|
+
const checked = radios.find((r) => r.getAttribute("aria-checked") === "true");
|
|
91
|
+
const stop = checked ?? radios[0];
|
|
92
|
+
for (const r of radios)
|
|
93
|
+
r.tabIndex = r === stop ? 0 : -1;
|
|
94
|
+
});
|
|
29
95
|
const onKeyDown = (e) => {
|
|
30
96
|
const KEYS = [
|
|
31
97
|
"ArrowDown",
|
|
@@ -63,5 +129,5 @@ function useRadioGroupKeyboard({ select, }) {
|
|
|
63
129
|
if (value !== undefined)
|
|
64
130
|
select(value);
|
|
65
131
|
};
|
|
66
|
-
return { onKeyDown };
|
|
132
|
+
return { onKeyDown, groupRef };
|
|
67
133
|
}
|
|
@@ -56,8 +56,8 @@ function RadioGroup({ value, defaultValue, onValueChange, variant = "default", n
|
|
|
56
56
|
setInternal(v);
|
|
57
57
|
onValueChange?.(v);
|
|
58
58
|
}, [value, onValueChange]);
|
|
59
|
-
const { onKeyDown } = (0, radiogroup_keyboard_1.useRadioGroupKeyboard)({ select });
|
|
60
|
-
return ((0, jsx_runtime_1.jsx)(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: (0, jsx_runtime_1.jsx)("div", { role: "radiogroup", "aria-label": ariaLabel, "data-variant": variant, className: (0, radiogroup_recipe_1.radiogroupRecipe)({ className }), ...rest, onKeyDown: onKeyDown, children: children }) }));
|
|
59
|
+
const { onKeyDown, groupRef } = (0, radiogroup_keyboard_1.useRadioGroupKeyboard)({ select });
|
|
60
|
+
return ((0, jsx_runtime_1.jsx)(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: (0, jsx_runtime_1.jsx)("div", { ref: groupRef, role: "radiogroup", "aria-label": ariaLabel, "data-variant": variant, className: (0, radiogroup_recipe_1.radiogroupRecipe)({ className }), ...rest, onKeyDown: onKeyDown, children: children }) }));
|
|
61
61
|
}
|
|
62
62
|
RadioGroup.displayName = "RadioGroup";
|
|
63
63
|
function Radio({ value, label, children, disabled }) {
|
|
@@ -68,7 +68,7 @@ function Segmented({ options, value, defaultValue, onValueChange, "aria-label":
|
|
|
68
68
|
// crisp's shared radio group model (#200). The per-button handler this
|
|
69
69
|
// replaces closed over its own index, so it selected a neighbour without
|
|
70
70
|
// moving focus and every press after the first recomputed the same step.
|
|
71
|
-
const { onKeyDown } = (0, radiogroup_1.useRadioGroupKeyboard)({ select });
|
|
71
|
+
const { onKeyDown } = (0, radiogroup_1.useRadioGroupKeyboard)({ select, groupRef: rootRef });
|
|
72
72
|
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, segmented_recipe_1.segmentedRecipe)({ className }), ...rest, role: "radiogroup", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [thumb && ((0, jsx_runtime_1.jsx)("span", { className: "segmented-thumb", "aria-hidden": "true", style: {
|
|
73
73
|
width: thumb.width,
|
|
74
74
|
transform: `translateX(${thumb.left}px)`,
|
|
@@ -44,6 +44,15 @@ function useTablistKeyboard({ tabs, active, select, listRef, }) {
|
|
|
44
44
|
* arrows move within. Without this a keyboard user walks through every tab
|
|
45
45
|
* one at a time, and the number of stops grows with the tab set.
|
|
46
46
|
*/
|
|
47
|
-
const tabIndexFor = (value) =>
|
|
47
|
+
const tabIndexFor = (value) => {
|
|
48
|
+
// ...and a group with no tab stop at all is worse than one with too many:
|
|
49
|
+
// a controlled `value` that matches no tab gave every tab -1, so Tab
|
|
50
|
+
// skipped the whole strip (#237). Fall back to the first enabled tab,
|
|
51
|
+
// which is where focus would land anyway.
|
|
52
|
+
const stop = tabs.some((t) => t.value === active && !t.disabled)
|
|
53
|
+
? active
|
|
54
|
+
: tabs.find((t) => !t.disabled)?.value;
|
|
55
|
+
return value === stop ? 0 : -1;
|
|
56
|
+
};
|
|
48
57
|
return { onKeyDown, tabIndexFor };
|
|
49
58
|
}
|
|
@@ -130,6 +130,7 @@ exports.recordsTableLabels = {
|
|
|
130
130
|
searchRecordsPlaceholder: "Search records…",
|
|
131
131
|
searchOptionsPlaceholder: "Search options…",
|
|
132
132
|
selectedCount: "Selected",
|
|
133
|
+
selectionAnnouncement: (count) => count === 0 ? "Selection cleared" : `${count} selected`,
|
|
133
134
|
delete: "Delete",
|
|
134
135
|
sendEmail: "Send email",
|
|
135
136
|
exportCsv: "Export CSV",
|
|
@@ -562,6 +563,18 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
|
|
|
562
563
|
// selection — controlled if `selectedIds` is provided, else internal
|
|
563
564
|
const [internalSel, setInternalSel] = React.useState(() => new Set());
|
|
564
565
|
const selected = React.useMemo(() => (selectedIds ? new Set(selectedIds) : internalSel), [selectedIds, internalSel]);
|
|
566
|
+
// The selection is announced from a region that is part of the table and
|
|
567
|
+
// outlives every selection, because the bulk bar the consumer mounts on the
|
|
568
|
+
// first pick cannot announce its own arrival (#234). Derived by adjusting
|
|
569
|
+
// state during render rather than in an effect: it must not fire on mount —
|
|
570
|
+
// nobody has selected anything yet — and it has to cover a `selectedIds`
|
|
571
|
+
// change the consumer makes as well as a click in the table.
|
|
572
|
+
const prevSelectedCount = React.useRef(selected.size);
|
|
573
|
+
const [selectionAnnouncement, setSelectionAnnouncement] = React.useState("");
|
|
574
|
+
if (prevSelectedCount.current !== selected.size) {
|
|
575
|
+
prevSelectedCount.current = selected.size;
|
|
576
|
+
setSelectionAnnouncement(t.selectionAnnouncement(selected.size));
|
|
577
|
+
}
|
|
565
578
|
const setSelected = React.useCallback((next) => {
|
|
566
579
|
onSelectionChange?.([...next]);
|
|
567
580
|
if (!selectedIds)
|
|
@@ -1094,7 +1107,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
|
|
|
1094
1107
|
(0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { ref: menuRef, className: "recordstable-menu recordstable-menu--fixed recordstable-menu--up", role: "menu", "aria-label": `${col.label} summary`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: aggOptions(col).map((opt) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitemradio", tabIndex: -1, "aria-checked": a === opt, "data-on": a === opt ? "" : undefined, onClick: () => setAggValue(col.key, opt), children: [t.agg[opt], a === opt ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "recordstable-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, opt))) }), rootRef.current)] }) }, col.key));
|
|
1095
1108
|
}), (0, jsx_runtime_1.jsx)("td", { className: "recordstable-sum recordstable-addcol" })] }) })] }), visibleRows.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "recordstable-empty", children: (0, jsx_runtime_1.jsx)(emptystate_1.EmptyState, { variant: "page", icon: activeFilters ? ((0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Inbox, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })), title: activeFilters ? "No records match that filter" : t.noRecords, description: activeFilters
|
|
1096
1109
|
? "None of your records match the filters that are set."
|
|
1097
|
-
: `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] })) }) })) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-addrow", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), selected.size > 0 && ((0, jsx_runtime_1.jsxs)(bulkbar_1.BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [(0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.moreActions, trigger: (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), (0, jsx_runtime_1.jsx)(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
|
|
1110
|
+
: `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : ((0, jsx_runtime_1.jsxs)(button_1.Button, { intent: "primary", size: "sm", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] })) }) })) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-addrow", onClick: addRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), selected.size > 0 && ((0, jsx_runtime_1.jsxs)(bulkbar_1.BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [(0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.moreActions, trigger: (0, jsx_runtime_1.jsx)(bulkbar_1.BulkBarButton, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), (0, jsx_runtime_1.jsx)(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
|
|
1098
1111
|
(0, react_dom_1.createPortal)((0, jsx_runtime_1.jsxs)("div", { ref: ctxRef, className: "recordstable-menu recordstable-ctx", role: "menu", "aria-label": t.recordActions, style: { position: "fixed", top: ctxMenu.y, left: ctxMenu.x }, onKeyDown: onCtxKeyDown, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => copyRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), t.copy] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => duplicateRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CopyPlus, { size: 15, strokeWidth: 1.75 }), t.duplicate] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => pinRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), t.addToList] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordstable-menu-item recordstable-menu-item--danger", role: "menuitem", tabIndex: -1, onClick: () => deleteRow(ctxMenu.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), t.delete] })] }), rootRef.current ?? document.body)] }));
|
|
1099
1112
|
}
|
|
1100
1113
|
exports.defaultColumns = [
|
|
@@ -149,6 +149,12 @@ ${(0, tabletoolbar_chrome_1.toolbarChromeBody)("recordstable")}
|
|
|
149
149
|
& .recordstable-bulk-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; color: ${tokens_1.contract.fgMuted}; cursor: pointer; }
|
|
150
150
|
& .recordstable-bulk-close:hover { background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; }
|
|
151
151
|
|
|
152
|
+
/* announced, never drawn */
|
|
153
|
+
& .recordstable-live {
|
|
154
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
155
|
+
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
152
158
|
${core_1.rtlFlipCss}
|
|
153
159
|
`,
|
|
154
160
|
variants: {},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { X } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { bulkbarRecipe } from "./bulkbar.recipe.js";
|
|
@@ -7,8 +7,8 @@ import { bulkbarRecipe } from "./bulkbar.recipe.js";
|
|
|
7
7
|
* positioned ancestor when a selection is active; render it conditionally
|
|
8
8
|
* (e.g. `count > 0 && <BulkBar …/>`). Measured from Attio's records table.
|
|
9
9
|
*/
|
|
10
|
-
export function BulkBar({ count, onClear, countLabel = "selected", children, className, clearLabel = "Clear selection", ...rest }) {
|
|
11
|
-
return (_jsx("div", { className: bulkbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "bulkbar-bar",
|
|
10
|
+
export function BulkBar({ count, onClear, countLabel = "selected", countText = (count, label) => (_jsxs(_Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
|
|
11
|
+
return (_jsx("div", { className: bulkbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "bulkbar-bar", children: [count != null && (_jsxs("button", { type: "button", className: "bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), _jsx(X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
|
|
12
12
|
}
|
|
13
13
|
BulkBar.displayName = "BulkBar";
|
|
14
14
|
/** A styled action button for use inside a `<BulkBar>` (or as a `<Menu>` trigger). */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
/**
|
|
2
3
|
* The WAI-ARIA radio group keyboard model: ←/→/↑/↓ move focus to the next or
|
|
3
4
|
* previous enabled radio, wrapping, and check it on arrival. Home/End jump to
|
|
@@ -22,7 +23,39 @@
|
|
|
22
23
|
* `Segmented` already offered them, and the shared model should be the union
|
|
23
24
|
* rather than quietly dropping behaviour one surface had.
|
|
24
25
|
*/
|
|
25
|
-
export function useRadioGroupKeyboard({ select, }) {
|
|
26
|
+
export function useRadioGroupKeyboard({ select, groupRef: externalRef, }) {
|
|
27
|
+
/**
|
|
28
|
+
* The group's one tab stop. `tabIndex={checked ? 0 : -1}` in the markup is
|
|
29
|
+
* right for every case but the one that matters most: a group with NOTHING
|
|
30
|
+
* selected gives every radio -1, so Tab skips the whole control and an
|
|
31
|
+
* unanswered question can never be answered from the keyboard (#237). APG
|
|
32
|
+
* says the first radio is tabbable when none is checked, and RadioGroup's
|
|
33
|
+
* own comment said so too — "the selected (or first, when none) radio is
|
|
34
|
+
* tabbable" — while the code did only the first half.
|
|
35
|
+
*
|
|
36
|
+
* Owned here rather than in the markup because the group renders arbitrary
|
|
37
|
+
* children: only the DOM knows which radio comes first, which is the same
|
|
38
|
+
* reason the arrow keys read the radios out of the DOM.
|
|
39
|
+
*
|
|
40
|
+
* It runs after EVERY render, and sets both ends. React will not undo a
|
|
41
|
+
* mutation it did not make — when the selection moves, the newly checked
|
|
42
|
+
* radio changes 0 from React's side while the stale one keeps the 0 written
|
|
43
|
+
* here — so this has to own the whole row, not just add to it.
|
|
44
|
+
*/
|
|
45
|
+
const ownRef = React.useRef(null);
|
|
46
|
+
const groupRef = externalRef ?? ownRef;
|
|
47
|
+
React.useLayoutEffect(() => {
|
|
48
|
+
const root = groupRef.current;
|
|
49
|
+
if (!root)
|
|
50
|
+
return;
|
|
51
|
+
const radios = Array.from(root.querySelectorAll('[role="radio"]:not([disabled])'));
|
|
52
|
+
if (!radios.length)
|
|
53
|
+
return;
|
|
54
|
+
const checked = radios.find((r) => r.getAttribute("aria-checked") === "true");
|
|
55
|
+
const stop = checked ?? radios[0];
|
|
56
|
+
for (const r of radios)
|
|
57
|
+
r.tabIndex = r === stop ? 0 : -1;
|
|
58
|
+
});
|
|
26
59
|
const onKeyDown = (e) => {
|
|
27
60
|
const KEYS = [
|
|
28
61
|
"ArrowDown",
|
|
@@ -60,5 +93,5 @@ export function useRadioGroupKeyboard({ select, }) {
|
|
|
60
93
|
if (value !== undefined)
|
|
61
94
|
select(value);
|
|
62
95
|
};
|
|
63
|
-
return { onKeyDown };
|
|
96
|
+
return { onKeyDown, groupRef };
|
|
64
97
|
}
|
|
@@ -19,8 +19,8 @@ export function RadioGroup({ value, defaultValue, onValueChange, variant = "defa
|
|
|
19
19
|
setInternal(v);
|
|
20
20
|
onValueChange?.(v);
|
|
21
21
|
}, [value, onValueChange]);
|
|
22
|
-
const { onKeyDown } = useRadioGroupKeyboard({ select });
|
|
23
|
-
return (_jsx(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: _jsx("div", { role: "radiogroup", "aria-label": ariaLabel, "data-variant": variant, className: radiogroupRecipe({ className }), ...rest, onKeyDown: onKeyDown, children: children }) }));
|
|
22
|
+
const { onKeyDown, groupRef } = useRadioGroupKeyboard({ select });
|
|
23
|
+
return (_jsx(RadioContext.Provider, { value: { value: selected, select, name: groupName, variant }, children: _jsx("div", { ref: groupRef, role: "radiogroup", "aria-label": ariaLabel, "data-variant": variant, className: radiogroupRecipe({ className }), ...rest, onKeyDown: onKeyDown, children: children }) }));
|
|
24
24
|
}
|
|
25
25
|
RadioGroup.displayName = "RadioGroup";
|
|
26
26
|
export function Radio({ value, label, children, disabled }) {
|
|
@@ -32,7 +32,7 @@ export function Segmented({ options, value, defaultValue, onValueChange, "aria-l
|
|
|
32
32
|
// crisp's shared radio group model (#200). The per-button handler this
|
|
33
33
|
// replaces closed over its own index, so it selected a neighbour without
|
|
34
34
|
// moving focus and every press after the first recomputed the same step.
|
|
35
|
-
const { onKeyDown } = useRadioGroupKeyboard({ select });
|
|
35
|
+
const { onKeyDown } = useRadioGroupKeyboard({ select, groupRef: rootRef });
|
|
36
36
|
return (_jsxs("div", { ref: rootRef, className: segmentedRecipe({ className }), ...rest, role: "radiogroup", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [thumb && (_jsx("span", { className: "segmented-thumb", "aria-hidden": "true", style: {
|
|
37
37
|
width: thumb.width,
|
|
38
38
|
transform: `translateX(${thumb.left}px)`,
|
|
@@ -41,6 +41,15 @@ export function useTablistKeyboard({ tabs, active, select, listRef, }) {
|
|
|
41
41
|
* arrows move within. Without this a keyboard user walks through every tab
|
|
42
42
|
* one at a time, and the number of stops grows with the tab set.
|
|
43
43
|
*/
|
|
44
|
-
const tabIndexFor = (value) =>
|
|
44
|
+
const tabIndexFor = (value) => {
|
|
45
|
+
// ...and a group with no tab stop at all is worse than one with too many:
|
|
46
|
+
// a controlled `value` that matches no tab gave every tab -1, so Tab
|
|
47
|
+
// skipped the whole strip (#237). Fall back to the first enabled tab,
|
|
48
|
+
// which is where focus would land anyway.
|
|
49
|
+
const stop = tabs.some((t) => t.value === active && !t.disabled)
|
|
50
|
+
? active
|
|
51
|
+
: tabs.find((t) => !t.disabled)?.value;
|
|
52
|
+
return value === stop ? 0 : -1;
|
|
53
|
+
};
|
|
45
54
|
return { onKeyDown, tabIndexFor };
|
|
46
55
|
}
|
|
@@ -90,6 +90,7 @@ export const recordsTableLabels = {
|
|
|
90
90
|
searchRecordsPlaceholder: "Search records…",
|
|
91
91
|
searchOptionsPlaceholder: "Search options…",
|
|
92
92
|
selectedCount: "Selected",
|
|
93
|
+
selectionAnnouncement: (count) => count === 0 ? "Selection cleared" : `${count} selected`,
|
|
93
94
|
delete: "Delete",
|
|
94
95
|
sendEmail: "Send email",
|
|
95
96
|
exportCsv: "Export CSV",
|
|
@@ -522,6 +523,18 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
|
|
|
522
523
|
// selection — controlled if `selectedIds` is provided, else internal
|
|
523
524
|
const [internalSel, setInternalSel] = React.useState(() => new Set());
|
|
524
525
|
const selected = React.useMemo(() => (selectedIds ? new Set(selectedIds) : internalSel), [selectedIds, internalSel]);
|
|
526
|
+
// The selection is announced from a region that is part of the table and
|
|
527
|
+
// outlives every selection, because the bulk bar the consumer mounts on the
|
|
528
|
+
// first pick cannot announce its own arrival (#234). Derived by adjusting
|
|
529
|
+
// state during render rather than in an effect: it must not fire on mount —
|
|
530
|
+
// nobody has selected anything yet — and it has to cover a `selectedIds`
|
|
531
|
+
// change the consumer makes as well as a click in the table.
|
|
532
|
+
const prevSelectedCount = React.useRef(selected.size);
|
|
533
|
+
const [selectionAnnouncement, setSelectionAnnouncement] = React.useState("");
|
|
534
|
+
if (prevSelectedCount.current !== selected.size) {
|
|
535
|
+
prevSelectedCount.current = selected.size;
|
|
536
|
+
setSelectionAnnouncement(t.selectionAnnouncement(selected.size));
|
|
537
|
+
}
|
|
525
538
|
const setSelected = React.useCallback((next) => {
|
|
526
539
|
onSelectionChange?.([...next]);
|
|
527
540
|
if (!selectedIds)
|
|
@@ -1054,7 +1067,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
|
|
|
1054
1067
|
createPortal(_jsx("div", { ref: menuRef, className: "recordstable-menu recordstable-menu--fixed recordstable-menu--up", role: "menu", "aria-label": `${col.label} summary`, style: menuPos ?? undefined, onKeyDown: onMenuKeyDown, children: aggOptions(col).map((opt) => (_jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitemradio", tabIndex: -1, "aria-checked": a === opt, "data-on": a === opt ? "" : undefined, onClick: () => setAggValue(col.key, opt), children: [t.agg[opt], a === opt ? (_jsx(Check, { className: "recordstable-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, opt))) }), rootRef.current)] }) }, col.key));
|
|
1055
1068
|
}), _jsx("td", { className: "recordstable-sum recordstable-addcol" })] }) })] }), visibleRows.length === 0 ? (_jsx("div", { className: "recordstable-empty", children: _jsx(EmptyState, { variant: "page", icon: activeFilters ? (_jsx(ListFilter, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })) : (_jsx(Inbox, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" })), title: activeFilters ? "No records match that filter" : t.noRecords, description: activeFilters
|
|
1056
1069
|
? "None of your records match the filters that are set."
|
|
1057
|
-
: `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? (_jsxs(Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [_jsx(ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : (_jsxs(Button, { intent: "primary", size: "sm", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] })) }) })) : (_jsxs("button", { type: "button", className: "recordstable-addrow", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), selected.size > 0 && (_jsxs(BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [_jsx(BulkBarButton, { icon: _jsx(ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), _jsx(BulkBarButton, { icon: _jsx(Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), _jsx(BulkBarButton, { icon: _jsx(Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), _jsxs(Menu, { align: "end", "aria-label": t.moreActions, trigger: _jsx(BulkBarButton, { icon: _jsx(MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [_jsx(MenuItem, { icon: _jsx(Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), _jsx(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
|
|
1070
|
+
: `Create your first ${cols.find((c) => c.type === "entity")?.label?.toLowerCase() ?? "record"} to get started.`, action: activeFilters ? (_jsxs(Button, { intent: "primary", size: "sm", onClick: () => setFilterConds([]), children: [_jsx(ListFilter, { size: 15, strokeWidth: 1.75 }), t.clearFilter] })) : (_jsxs(Button, { intent: "primary", size: "sm", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), t.newRecord] })) }) })) : (_jsxs("button", { type: "button", className: "recordstable-addrow", onClick: addRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.newRecord] }))] }), _jsx("div", { className: "recordstable-live", role: "status", "aria-live": "polite", children: selectionAnnouncement }), selected.size > 0 && (_jsxs(BulkBar, { count: selected.size, countLabel: t.selectedCount, onClear: () => setSelected(new Set()), children: [_jsx(BulkBarButton, { icon: _jsx(ListPlus, { size: 15, strokeWidth: 1.75 }), onClick: addToList, children: t.addToList }), _jsx(BulkBarButton, { icon: _jsx(Mail, { size: 15, strokeWidth: 1.75 }), onClick: sendEmail, children: t.sendEmail }), _jsx(BulkBarButton, { icon: _jsx(Workflow, { size: 15, strokeWidth: 1.75 }), onClick: runWorkflow, children: t.runWorkflow }), _jsxs(Menu, { align: "end", "aria-label": t.moreActions, trigger: _jsx(BulkBarButton, { icon: _jsx(MoreHorizontal, { size: 15, strokeWidth: 1.75 }), children: t.more }), children: [_jsx(MenuItem, { icon: _jsx(Download, { size: 15, strokeWidth: 1.75 }), onSelect: exportSelected, children: t.exportCsv }), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, onSelect: deleteSelected, children: t.delete })] })] })), _jsx(CreateRecordDialog, { labels: t, open: creating, onOpenChange: setCreating, columns: cols, entityLabel: cols.find((c) => c.type === "entity")?.label ?? "record", onCreate: (row) => setData((prev) => [...prev, row]) }), ctxMenu &&
|
|
1058
1071
|
createPortal(_jsxs("div", { ref: ctxRef, className: "recordstable-menu recordstable-ctx", role: "menu", "aria-label": t.recordActions, style: { position: "fixed", top: ctxMenu.y, left: ctxMenu.x }, onKeyDown: onCtxKeyDown, children: [_jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => copyRow(ctxMenu.id), children: [_jsx(Copy, { size: 15, strokeWidth: 1.75 }), t.copy] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => duplicateRow(ctxMenu.id), children: [_jsx(CopyPlus, { size: 15, strokeWidth: 1.75 }), t.duplicate] }), _jsxs("button", { type: "button", className: "recordstable-menu-item", role: "menuitem", tabIndex: -1, onClick: () => pinRow(ctxMenu.id), children: [_jsx(Star, { size: 15, strokeWidth: 1.75 }), t.addToList] }), _jsxs("button", { type: "button", className: "recordstable-menu-item recordstable-menu-item--danger", role: "menuitem", tabIndex: -1, onClick: () => deleteRow(ctxMenu.id), children: [_jsx(Trash2, { size: 15, strokeWidth: 1.75 }), t.delete] })] }), rootRef.current ?? document.body)] }));
|
|
1059
1072
|
}
|
|
1060
1073
|
export const defaultColumns = [
|
|
@@ -146,6 +146,12 @@ ${toolbarChromeBody("recordstable")}
|
|
|
146
146
|
& .recordstable-bulk-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: ${c.radius.md}; background: transparent; color: ${c.fgMuted}; cursor: pointer; }
|
|
147
147
|
& .recordstable-bulk-close:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
|
|
148
148
|
|
|
149
|
+
/* announced, never drawn */
|
|
150
|
+
& .recordstable-live {
|
|
151
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
152
|
+
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
149
155
|
${rtlFlipCss}
|
|
150
156
|
`,
|
|
151
157
|
variants: {},
|
package/dist/recordstable.css
CHANGED
|
@@ -201,6 +201,12 @@
|
|
|
201
201
|
& .recordstable-bulk-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: var(--radius-md); background: transparent; color: var(--fg-muted); cursor: pointer; }
|
|
202
202
|
& .recordstable-bulk-close:hover { background: var(--bg-raised); color: var(--fg-primary); }
|
|
203
203
|
|
|
204
|
+
/* announced, never drawn */
|
|
205
|
+
& .recordstable-live {
|
|
206
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
207
|
+
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
204
210
|
[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
|
|
205
211
|
|
|
206
212
|
}
|
package/dist/styles.css
CHANGED
|
@@ -2603,6 +2603,12 @@
|
|
|
2603
2603
|
& .recordstable-bulk-close { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: var(--radius-md); background: transparent; color: var(--fg-muted); cursor: pointer; }
|
|
2604
2604
|
& .recordstable-bulk-close:hover { background: var(--bg-raised); color: var(--fg-primary); }
|
|
2605
2605
|
|
|
2606
|
+
/* announced, never drawn */
|
|
2607
|
+
& .recordstable-live {
|
|
2608
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
2609
|
+
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2606
2612
|
[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
|
|
2607
2613
|
|
|
2608
2614
|
}
|
|
@@ -6,6 +6,14 @@ export type BulkBarProps = Omit<React.ComponentPropsWithoutRef<"div">, "children
|
|
|
6
6
|
onClear?: () => void;
|
|
7
7
|
/** Word after the count in the pill (default "selected"). */
|
|
8
8
|
countLabel?: string;
|
|
9
|
+
/**
|
|
10
|
+
* The whole pill line, given the count and `countLabel`. A formatter,
|
|
11
|
+
* because "3 selected" is a sentence with a hole in it and the count does
|
|
12
|
+
* not sit in the same place in every language. Defaults to
|
|
13
|
+
* `{count} {countLabel}`, so a consumer who only wants the word keeps
|
|
14
|
+
* passing `countLabel`.
|
|
15
|
+
*/
|
|
16
|
+
countText?: (count: number, countLabel: string) => React.ReactNode;
|
|
9
17
|
/** title on the count pill's clear (✕) control (default "Clear selection"). */
|
|
10
18
|
clearLabel?: string;
|
|
11
19
|
/** Action buttons — usually `<BulkBarButton>`s and a `<Menu>` overflow. */
|
|
@@ -17,7 +25,7 @@ export type BulkBarProps = Omit<React.ComponentPropsWithoutRef<"div">, "children
|
|
|
17
25
|
* positioned ancestor when a selection is active; render it conditionally
|
|
18
26
|
* (e.g. `count > 0 && <BulkBar …/>`). Measured from Attio's records table.
|
|
19
27
|
*/
|
|
20
|
-
export declare function BulkBar({ count, onClear, countLabel, children, className, clearLabel, ...rest }: BulkBarProps): React.JSX.Element;
|
|
28
|
+
export declare function BulkBar({ count, onClear, countLabel, countText, children, className, clearLabel, ...rest }: BulkBarProps): React.JSX.Element;
|
|
21
29
|
export declare namespace BulkBar {
|
|
22
30
|
var displayName: string;
|
|
23
31
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as React from "react";
|
|
2
2
|
export type UseRadioGroupKeyboardOptions = {
|
|
3
3
|
/** Select a radio by its `data-value`. */
|
|
4
4
|
select: (value: string) => void;
|
|
5
|
+
/**
|
|
6
|
+
* The element carrying `role="radiogroup"`, when the surface already keeps a
|
|
7
|
+
* ref to it (Segmented measures its thumb from one). Omit it and the hook
|
|
8
|
+
* hands one back to attach.
|
|
9
|
+
*/
|
|
10
|
+
groupRef?: React.RefObject<HTMLElement | null>;
|
|
5
11
|
};
|
|
6
12
|
/**
|
|
7
13
|
* The WAI-ARIA radio group keyboard model: ←/→/↑/↓ move focus to the next or
|
|
@@ -27,6 +33,7 @@ export type UseRadioGroupKeyboardOptions = {
|
|
|
27
33
|
* `Segmented` already offered them, and the shared model should be the union
|
|
28
34
|
* rather than quietly dropping behaviour one surface had.
|
|
29
35
|
*/
|
|
30
|
-
export declare function useRadioGroupKeyboard({ select, }: UseRadioGroupKeyboardOptions): {
|
|
36
|
+
export declare function useRadioGroupKeyboard({ select, groupRef: externalRef, }: UseRadioGroupKeyboardOptions): {
|
|
31
37
|
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
38
|
+
groupRef: React.RefObject<HTMLElement | null>;
|
|
32
39
|
};
|
|
@@ -152,6 +152,14 @@ export type RecordsTableLabels = {
|
|
|
152
152
|
searchOptionsPlaceholder: string;
|
|
153
153
|
/** Prefix on the bulk bar's selected count. */
|
|
154
154
|
selectedCount: string;
|
|
155
|
+
/**
|
|
156
|
+
* Announced when the selection changes. A formatter, not a string: the
|
|
157
|
+
* count moves within the sentence between languages. Return "" to say
|
|
158
|
+
* nothing. The bulk bar cannot carry this itself — the consumer mounts it
|
|
159
|
+
* when the first row is picked, so its live region would be created by the
|
|
160
|
+
* very change it should announce (#234).
|
|
161
|
+
*/
|
|
162
|
+
selectionAnnouncement: (count: number) => string;
|
|
155
163
|
delete: string;
|
|
156
164
|
sendEmail: string;
|
|
157
165
|
exportCsv: string;
|