@crisp-ui-kit/crisp 0.15.0 → 0.16.1
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/multiselect/multiselect.js +16 -17
- package/dist/cjs/components/select/index.js +3 -1
- package/dist/cjs/components/select/listbox-keyboard.js +68 -0
- package/dist/cjs/components/select/select.js +9 -42
- package/dist/cjs/components/tabs/index.js +3 -1
- package/dist/cjs/components/tabs/tabs-keyboard.js +49 -0
- package/dist/cjs/components/tabs/tabs.js +8 -24
- package/dist/cjs/product/inbox/inbox.js +47 -2
- package/dist/esm/components/multiselect/multiselect.js +16 -17
- package/dist/esm/components/select/index.js +1 -0
- package/dist/esm/components/select/listbox-keyboard.js +65 -0
- package/dist/esm/components/select/select.js +9 -42
- package/dist/esm/components/tabs/index.js +1 -0
- package/dist/esm/components/tabs/tabs-keyboard.js +46 -0
- package/dist/esm/components/tabs/tabs.js +8 -24
- package/dist/esm/product/inbox/inbox.js +14 -2
- package/dist/types/components/select/index.d.ts +1 -0
- package/dist/types/components/select/listbox-keyboard.d.ts +38 -0
- package/dist/types/components/tabs/index.d.ts +1 -0
- package/dist/types/components/tabs/tabs-keyboard.d.ts +35 -0
- package/dist/types/product/inbox/inbox.d.ts +5 -2
- package/package.json +1 -1
|
@@ -37,6 +37,7 @@ exports.MultiSelect = MultiSelect;
|
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const lucide_react_1 = require("lucide-react");
|
|
39
39
|
const React = __importStar(require("react"));
|
|
40
|
+
const select_1 = require("../select");
|
|
40
41
|
const multiselect_recipe_1 = require("./multiselect.recipe");
|
|
41
42
|
const TONES = [
|
|
42
43
|
"brand",
|
|
@@ -122,26 +123,24 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, p
|
|
|
122
123
|
setInternalValue(next);
|
|
123
124
|
onValueChange?.(next);
|
|
124
125
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
e.preventDefault();
|
|
126
|
+
// crisp's shared popup-listbox model (#198). Hand-rolling a second one is
|
|
127
|
+
// how MultiSelect ended up with no Home/End, arrows that dead-end instead of
|
|
128
|
+
// wrapping, and a popup left open behind a Tab — while Select, the same
|
|
129
|
+
// widget with single selection, had all three.
|
|
130
|
+
const { onKeyDown: onNavKeys } = (0, select_1.useListboxKeyboard)({
|
|
131
|
+
// every rendered option is selectable: MultiSelect has no disabled state
|
|
132
|
+
enabledIndexes: filtered.map((_, i) => i),
|
|
133
|
+
highlight,
|
|
134
|
+
setHighlight,
|
|
135
|
+
commit: () => {
|
|
136
136
|
const opt = filtered[highlight];
|
|
137
137
|
if (opt)
|
|
138
138
|
toggle(opt.value);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
};
|
|
139
|
+
},
|
|
140
|
+
close,
|
|
141
|
+
// Tab is already moving focus out; `close` would drag it back to the trigger
|
|
142
|
+
dismiss: () => setOpen(false),
|
|
143
|
+
});
|
|
145
144
|
React.useEffect(() => {
|
|
146
145
|
if (!open)
|
|
147
146
|
return;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.selectRecipe = exports.Select = void 0;
|
|
3
|
+
exports.selectRecipe = exports.Select = exports.useListboxKeyboard = void 0;
|
|
4
|
+
var listbox_keyboard_1 = require("./listbox-keyboard");
|
|
5
|
+
Object.defineProperty(exports, "useListboxKeyboard", { enumerable: true, get: function () { return listbox_keyboard_1.useListboxKeyboard; } });
|
|
4
6
|
var select_1 = require("./select");
|
|
5
7
|
Object.defineProperty(exports, "Select", { enumerable: true, get: function () { return select_1.Select; } });
|
|
6
8
|
var select_recipe_1 = require("./select.recipe");
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useListboxKeyboard = useListboxKeyboard;
|
|
4
|
+
/**
|
|
5
|
+
* The WAI-ARIA listbox keyboard model for a POPUP listbox: ↑/↓ to move the
|
|
6
|
+
* highlight (wrapping, skipping disabled options), Home/End to jump, Enter to
|
|
7
|
+
* choose, Escape to close, Tab to dismiss.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from `Select`, whose set was the complete one, so `MultiSelect` —
|
|
10
|
+
* the same widget with multiple selection — stops being a second partial copy
|
|
11
|
+
* that had no Home/End and stayed open on Tab (#198). The same reason
|
|
12
|
+
* `useMenuKeyboard` and `useTablistKeyboard` exist.
|
|
13
|
+
*
|
|
14
|
+
* NOT for `WheelPicker`: that is an always-visible multi-column wheel that
|
|
15
|
+
* commits on move, so Enter, Escape and Tab have no meaning in it.
|
|
16
|
+
*/
|
|
17
|
+
function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, close, dismiss = close, }) {
|
|
18
|
+
const move = (dir) => {
|
|
19
|
+
if (enabledIndexes.length === 0)
|
|
20
|
+
return;
|
|
21
|
+
const pos = enabledIndexes.indexOf(highlight);
|
|
22
|
+
// nothing highlighted yet: ↓ enters at the top, ↑ at the bottom
|
|
23
|
+
const nextPos = pos < 0
|
|
24
|
+
? dir > 0
|
|
25
|
+
? 0
|
|
26
|
+
: enabledIndexes.length - 1
|
|
27
|
+
: (pos + dir + enabledIndexes.length) % enabledIndexes.length;
|
|
28
|
+
setHighlight(enabledIndexes[nextPos] ?? 0);
|
|
29
|
+
};
|
|
30
|
+
const onKeyDown = (e) => {
|
|
31
|
+
switch (e.key) {
|
|
32
|
+
case "ArrowDown":
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
move(1);
|
|
35
|
+
break;
|
|
36
|
+
case "ArrowUp":
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
move(-1);
|
|
39
|
+
break;
|
|
40
|
+
case "Home":
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
// the `?? 0` these used to end in armed option 0 on a list with no
|
|
43
|
+
// options — the arrows guarded against that and Home/End did not
|
|
44
|
+
if (enabledIndexes.length)
|
|
45
|
+
setHighlight(enabledIndexes[0]);
|
|
46
|
+
break;
|
|
47
|
+
case "End":
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
if (enabledIndexes.length)
|
|
50
|
+
setHighlight(enabledIndexes[enabledIndexes.length - 1]);
|
|
51
|
+
break;
|
|
52
|
+
case "Enter":
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
commit();
|
|
55
|
+
break;
|
|
56
|
+
case "Escape":
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
close();
|
|
59
|
+
break;
|
|
60
|
+
case "Tab":
|
|
61
|
+
// not preventDefault: Tab must still move focus, it just must not
|
|
62
|
+
// leave an open popup floating behind it
|
|
63
|
+
dismiss();
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
return { onKeyDown };
|
|
68
|
+
}
|
|
@@ -37,6 +37,7 @@ exports.Select = Select;
|
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const lucide_react_1 = require("lucide-react");
|
|
39
39
|
const React = __importStar(require("react"));
|
|
40
|
+
const listbox_keyboard_1 = require("./listbox-keyboard");
|
|
40
41
|
const select_recipe_1 = require("./select.recipe");
|
|
41
42
|
function optionText(o) {
|
|
42
43
|
if (o.keywords)
|
|
@@ -116,48 +117,14 @@ function Select({ options, value, defaultValue, onValueChange, placeholder = "Se
|
|
|
116
117
|
setOpen(false);
|
|
117
118
|
triggerRef.current?.focus({ preventScroll: true });
|
|
118
119
|
};
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
: (pos + dir + enabledIndexes.length) % enabledIndexes.length;
|
|
128
|
-
setHighlight(enabledIndexes[nextPos] ?? 0);
|
|
129
|
-
};
|
|
130
|
-
const onNavKeys = (e) => {
|
|
131
|
-
switch (e.key) {
|
|
132
|
-
case "ArrowDown":
|
|
133
|
-
e.preventDefault();
|
|
134
|
-
moveHighlight(1);
|
|
135
|
-
break;
|
|
136
|
-
case "ArrowUp":
|
|
137
|
-
e.preventDefault();
|
|
138
|
-
moveHighlight(-1);
|
|
139
|
-
break;
|
|
140
|
-
case "Home":
|
|
141
|
-
e.preventDefault();
|
|
142
|
-
setHighlight(enabledIndexes[0] ?? 0);
|
|
143
|
-
break;
|
|
144
|
-
case "End":
|
|
145
|
-
e.preventDefault();
|
|
146
|
-
setHighlight(enabledIndexes[enabledIndexes.length - 1] ?? 0);
|
|
147
|
-
break;
|
|
148
|
-
case "Enter":
|
|
149
|
-
e.preventDefault();
|
|
150
|
-
pick(filtered[highlight]);
|
|
151
|
-
break;
|
|
152
|
-
case "Escape":
|
|
153
|
-
e.preventDefault();
|
|
154
|
-
close();
|
|
155
|
-
break;
|
|
156
|
-
case "Tab":
|
|
157
|
-
setOpen(false);
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
160
|
-
};
|
|
120
|
+
const { onKeyDown: onNavKeys } = (0, listbox_keyboard_1.useListboxKeyboard)({
|
|
121
|
+
enabledIndexes,
|
|
122
|
+
highlight,
|
|
123
|
+
setHighlight,
|
|
124
|
+
commit: () => pick(filtered[highlight]),
|
|
125
|
+
close,
|
|
126
|
+
dismiss: () => setOpen(false),
|
|
127
|
+
});
|
|
161
128
|
const onTriggerKeyDown = (e) => {
|
|
162
129
|
if (!open) {
|
|
163
130
|
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.tabsRecipe = exports.Tabs = void 0;
|
|
3
|
+
exports.useTablistKeyboard = exports.tabsRecipe = exports.Tabs = void 0;
|
|
4
4
|
var tabs_1 = require("./tabs");
|
|
5
5
|
Object.defineProperty(exports, "Tabs", { enumerable: true, get: function () { return tabs_1.Tabs; } });
|
|
6
6
|
var tabs_recipe_1 = require("./tabs.recipe");
|
|
7
7
|
Object.defineProperty(exports, "tabsRecipe", { enumerable: true, get: function () { return tabs_recipe_1.tabsRecipe; } });
|
|
8
|
+
var tabs_keyboard_1 = require("./tabs-keyboard");
|
|
9
|
+
Object.defineProperty(exports, "useTablistKeyboard", { enumerable: true, get: function () { return tabs_keyboard_1.useTablistKeyboard; } });
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useTablistKeyboard = useTablistKeyboard;
|
|
4
|
+
/**
|
|
5
|
+
* The WAI-ARIA tablist keyboard model: ←/→ to move (wrapping), Home/End to
|
|
6
|
+
* jump, disabled tabs skipped, focus following selection.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from `Tabs` so a surface that renders its own measured tab chrome —
|
|
9
|
+
* Inbox's flyout tabs, and whatever comes next — gets the identical model
|
|
10
|
+
* rather than a second partial one. The same reason `useMenuKeyboard` exists.
|
|
11
|
+
*
|
|
12
|
+
* Automatic activation (arrow selects, rather than arrow-then-Enter) is APG's
|
|
13
|
+
* recommendation when switching tabs is cheap, and it is what `Tabs` already
|
|
14
|
+
* did before this was extracted, so it stays the behaviour.
|
|
15
|
+
*/
|
|
16
|
+
function useTablistKeyboard({ tabs, active, select, listRef, }) {
|
|
17
|
+
const onKeyDown = (e) => {
|
|
18
|
+
const enabled = tabs.filter((t) => !t.disabled);
|
|
19
|
+
if (!enabled.length)
|
|
20
|
+
return;
|
|
21
|
+
const idx = enabled.findIndex((t) => t.value === active);
|
|
22
|
+
let next = -1;
|
|
23
|
+
if (e.key === "ArrowRight")
|
|
24
|
+
next = (idx + 1) % enabled.length;
|
|
25
|
+
else if (e.key === "ArrowLeft")
|
|
26
|
+
next = (idx - 1 + enabled.length) % enabled.length;
|
|
27
|
+
else if (e.key === "Home")
|
|
28
|
+
next = 0;
|
|
29
|
+
else if (e.key === "End")
|
|
30
|
+
next = enabled.length - 1;
|
|
31
|
+
if (next < 0)
|
|
32
|
+
return;
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
const nextValue = enabled[next]?.value;
|
|
35
|
+
if (!nextValue)
|
|
36
|
+
return;
|
|
37
|
+
select(nextValue);
|
|
38
|
+
listRef.current
|
|
39
|
+
?.querySelector(`[data-value="${CSS.escape(nextValue)}"]`)
|
|
40
|
+
?.focus();
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* A tablist is a SINGLE tab stop: Tab enters the group and leaves it, and the
|
|
44
|
+
* arrows move within. Without this a keyboard user walks through every tab
|
|
45
|
+
* one at a time, and the number of stops grows with the tab set.
|
|
46
|
+
*/
|
|
47
|
+
const tabIndexFor = (value) => (value === active ? 0 : -1);
|
|
48
|
+
return { onKeyDown, tabIndexFor };
|
|
49
|
+
}
|
|
@@ -37,6 +37,7 @@ exports.Tabs = Tabs;
|
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const React = __importStar(require("react"));
|
|
39
39
|
const tabs_recipe_1 = require("./tabs.recipe");
|
|
40
|
+
const tabs_keyboard_1 = require("./tabs-keyboard");
|
|
40
41
|
/**
|
|
41
42
|
* A row of text tabs over a sliding ink underline — the record-view tab bar,
|
|
42
43
|
* measured from Attio. Roving-tabindex keyboard model (←/→ move + activate,
|
|
@@ -70,32 +71,15 @@ function Tabs({ tabs, value, defaultValue, onValueChange, leading, children, cla
|
|
|
70
71
|
ro.observe(list);
|
|
71
72
|
return () => ro.disconnect();
|
|
72
73
|
}, [active, tabs]);
|
|
73
|
-
const onKeyDown = (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
else if (e.key === "ArrowLeft")
|
|
80
|
-
next = (idx - 1 + enabled.length) % enabled.length;
|
|
81
|
-
else if (e.key === "Home")
|
|
82
|
-
next = 0;
|
|
83
|
-
else if (e.key === "End")
|
|
84
|
-
next = enabled.length - 1;
|
|
85
|
-
if (next < 0)
|
|
86
|
-
return;
|
|
87
|
-
e.preventDefault();
|
|
88
|
-
const nextValue = enabled[next]?.value;
|
|
89
|
-
if (!nextValue)
|
|
90
|
-
return;
|
|
91
|
-
select(nextValue);
|
|
92
|
-
listRef.current
|
|
93
|
-
?.querySelector(`[data-value="${CSS.escape(nextValue)}"]`)
|
|
94
|
-
?.focus();
|
|
95
|
-
};
|
|
74
|
+
const { onKeyDown, tabIndexFor } = (0, tabs_keyboard_1.useTablistKeyboard)({
|
|
75
|
+
tabs,
|
|
76
|
+
active,
|
|
77
|
+
select,
|
|
78
|
+
listRef,
|
|
79
|
+
});
|
|
96
80
|
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, tabs_recipe_1.tabsRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { ref: listRef, className: "tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && (0, jsx_runtime_1.jsx)("span", { className: "tabs-leading", children: leading }), tabs.map((t) => {
|
|
97
81
|
const selected = t.value === active;
|
|
98
|
-
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "tab", id: `tab-${t.value}`, "aria-selected": selected, "aria-controls": `panel-${t.value}`, tabIndex:
|
|
82
|
+
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "tab", id: `tab-${t.value}`, "aria-selected": selected, "aria-controls": `panel-${t.value}`, tabIndex: tabIndexFor(t.value), "data-value": t.value, "data-selected": selected ? "" : undefined, disabled: t.disabled, className: "tabs-tab", onClick: () => select(t.value), children: [t.icon, (0, jsx_runtime_1.jsx)("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && ((0, jsx_runtime_1.jsx)("span", { className: "tabs-tab-badge", children: t.badge }))] }, t.value));
|
|
99
83
|
}), indicator && ((0, jsx_runtime_1.jsx)("span", { className: "tabs-indicator", style: {
|
|
100
84
|
width: indicator.width,
|
|
101
85
|
transform: `translateX(${indicator.left}px)`,
|
|
@@ -1,8 +1,43 @@
|
|
|
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.Inbox = Inbox;
|
|
4
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
38
|
const lucide_react_1 = require("lucide-react");
|
|
39
|
+
const React = __importStar(require("react"));
|
|
40
|
+
const tabs_1 = require("../../components/tabs");
|
|
6
41
|
const inbox_recipe_1 = require("./inbox.recipe");
|
|
7
42
|
/**
|
|
8
43
|
* A notifications inbox panel — measured from Attio's notification flyout. A
|
|
@@ -10,7 +45,17 @@ const inbox_recipe_1 = require("./inbox.recipe");
|
|
|
10
45
|
* (avatar + message + time + unread dot) or a centred empty state. Put it inside
|
|
11
46
|
* a Popover for the flyout.
|
|
12
47
|
*/
|
|
13
|
-
function Inbox({ notifications = [], title = "Notifications", tabs, activeTab, onTabChange, emptyTitle = "No notifications", emptySubtitle = "Notifications such as mentions and important updates will be displayed here.", emptyAction, className, }) {
|
|
14
|
-
|
|
48
|
+
function Inbox({ notifications = [], title = "Notifications", tabs, activeTab, onTabChange, emptyTitle = "No notifications", emptySubtitle = "Notifications such as mentions and important updates will be displayed here.", emptyAction, tabsLabel = "Notification views", className, }) {
|
|
49
|
+
const listRef = React.useRef(null);
|
|
50
|
+
// crisp's shared tablist model, the one `Tabs` runs (#195). Hand-rolling a
|
|
51
|
+
// second one is how Inbox ended up with no arrow keys, no roving tabindex
|
|
52
|
+
// and no name while `Tabs` had all three.
|
|
53
|
+
const { onKeyDown, tabIndexFor } = (0, tabs_1.useTablistKeyboard)({
|
|
54
|
+
tabs: (tabs ?? []).map((t) => ({ value: t.id, disabled: t.disabled })),
|
|
55
|
+
active: activeTab ?? "",
|
|
56
|
+
select: (id) => onTabChange?.(id),
|
|
57
|
+
listRef,
|
|
58
|
+
});
|
|
59
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, inbox_recipe_1.inboxRecipe)({ className }), children: [(0, jsx_runtime_1.jsxs)("div", { className: "inbox-head", children: [(0, jsx_runtime_1.jsx)("div", { className: "inbox-title", children: title }), tabs && tabs.length > 0 && ((0, jsx_runtime_1.jsx)("div", { ref: listRef, className: "inbox-tabs", role: "tablist", "aria-label": tabsLabel, onKeyDown: onKeyDown, children: tabs.map((t) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "tab", "aria-selected": t.id === activeTab, tabIndex: tabIndexFor(t.id), "data-value": t.id, disabled: t.disabled, className: "inbox-tab", "data-active": t.id === activeTab ? "" : undefined, onClick: () => onTabChange?.(t.id), children: [t.label, t.count != null && ((0, jsx_runtime_1.jsxs)("span", { className: "inbox-tab-count", children: ["(", t.count, ")"] }))] }, t.id))) }))] }), notifications.length === 0 ? ((0, jsx_runtime_1.jsxs)("div", { className: "inbox-empty", children: [(0, jsx_runtime_1.jsx)("span", { className: "inbox-empty-icon", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Bell, { size: 40, strokeWidth: 1.25 }) }), (0, jsx_runtime_1.jsx)("div", { className: "inbox-empty-title", children: emptyTitle }), emptySubtitle != null && ((0, jsx_runtime_1.jsx)("div", { className: "inbox-empty-sub", children: emptySubtitle })), emptyAction != null && ((0, jsx_runtime_1.jsx)("div", { className: "inbox-empty-action", children: emptyAction }))] })) : ((0, jsx_runtime_1.jsx)("div", { className: "inbox-list", children: notifications.map((n) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "inbox-row", onClick: n.onClick, children: [n.avatar != null && ((0, jsx_runtime_1.jsx)("span", { className: "inbox-row-avatar", children: n.avatar })), (0, jsx_runtime_1.jsxs)("span", { className: "inbox-row-body", children: [(0, jsx_runtime_1.jsx)("span", { className: "inbox-row-msg", children: n.message }), n.time != null && ((0, jsx_runtime_1.jsx)("span", { className: "inbox-row-time", children: n.time }))] }), n.unread && (0, jsx_runtime_1.jsx)("span", { className: "inbox-row-unread" })] }, n.id))) }))] }));
|
|
15
60
|
}
|
|
16
61
|
Inbox.displayName = "Inbox";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useListboxKeyboard } from "../select/index.js";
|
|
4
5
|
import { multiselectRecipe } from "./multiselect.recipe.js";
|
|
5
6
|
const TONES = [
|
|
6
7
|
"brand",
|
|
@@ -86,26 +87,24 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
86
87
|
setInternalValue(next);
|
|
87
88
|
onValueChange?.(next);
|
|
88
89
|
};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
e.preventDefault();
|
|
90
|
+
// crisp's shared popup-listbox model (#198). Hand-rolling a second one is
|
|
91
|
+
// how MultiSelect ended up with no Home/End, arrows that dead-end instead of
|
|
92
|
+
// wrapping, and a popup left open behind a Tab — while Select, the same
|
|
93
|
+
// widget with single selection, had all three.
|
|
94
|
+
const { onKeyDown: onNavKeys } = useListboxKeyboard({
|
|
95
|
+
// every rendered option is selectable: MultiSelect has no disabled state
|
|
96
|
+
enabledIndexes: filtered.map((_, i) => i),
|
|
97
|
+
highlight,
|
|
98
|
+
setHighlight,
|
|
99
|
+
commit: () => {
|
|
100
100
|
const opt = filtered[highlight];
|
|
101
101
|
if (opt)
|
|
102
102
|
toggle(opt.value);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
};
|
|
103
|
+
},
|
|
104
|
+
close,
|
|
105
|
+
// Tab is already moving focus out; `close` would drag it back to the trigger
|
|
106
|
+
dismiss: () => setOpen(false),
|
|
107
|
+
});
|
|
109
108
|
React.useEffect(() => {
|
|
110
109
|
if (!open)
|
|
111
110
|
return;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WAI-ARIA listbox keyboard model for a POPUP listbox: ↑/↓ to move the
|
|
3
|
+
* highlight (wrapping, skipping disabled options), Home/End to jump, Enter to
|
|
4
|
+
* choose, Escape to close, Tab to dismiss.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from `Select`, whose set was the complete one, so `MultiSelect` —
|
|
7
|
+
* the same widget with multiple selection — stops being a second partial copy
|
|
8
|
+
* that had no Home/End and stayed open on Tab (#198). The same reason
|
|
9
|
+
* `useMenuKeyboard` and `useTablistKeyboard` exist.
|
|
10
|
+
*
|
|
11
|
+
* NOT for `WheelPicker`: that is an always-visible multi-column wheel that
|
|
12
|
+
* commits on move, so Enter, Escape and Tab have no meaning in it.
|
|
13
|
+
*/
|
|
14
|
+
export function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, close, dismiss = close, }) {
|
|
15
|
+
const move = (dir) => {
|
|
16
|
+
if (enabledIndexes.length === 0)
|
|
17
|
+
return;
|
|
18
|
+
const pos = enabledIndexes.indexOf(highlight);
|
|
19
|
+
// nothing highlighted yet: ↓ enters at the top, ↑ at the bottom
|
|
20
|
+
const nextPos = pos < 0
|
|
21
|
+
? dir > 0
|
|
22
|
+
? 0
|
|
23
|
+
: enabledIndexes.length - 1
|
|
24
|
+
: (pos + dir + enabledIndexes.length) % enabledIndexes.length;
|
|
25
|
+
setHighlight(enabledIndexes[nextPos] ?? 0);
|
|
26
|
+
};
|
|
27
|
+
const onKeyDown = (e) => {
|
|
28
|
+
switch (e.key) {
|
|
29
|
+
case "ArrowDown":
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
move(1);
|
|
32
|
+
break;
|
|
33
|
+
case "ArrowUp":
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
move(-1);
|
|
36
|
+
break;
|
|
37
|
+
case "Home":
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
// the `?? 0` these used to end in armed option 0 on a list with no
|
|
40
|
+
// options — the arrows guarded against that and Home/End did not
|
|
41
|
+
if (enabledIndexes.length)
|
|
42
|
+
setHighlight(enabledIndexes[0]);
|
|
43
|
+
break;
|
|
44
|
+
case "End":
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
if (enabledIndexes.length)
|
|
47
|
+
setHighlight(enabledIndexes[enabledIndexes.length - 1]);
|
|
48
|
+
break;
|
|
49
|
+
case "Enter":
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
commit();
|
|
52
|
+
break;
|
|
53
|
+
case "Escape":
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
close();
|
|
56
|
+
break;
|
|
57
|
+
case "Tab":
|
|
58
|
+
// not preventDefault: Tab must still move focus, it just must not
|
|
59
|
+
// leave an open popup floating behind it
|
|
60
|
+
dismiss();
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return { onKeyDown };
|
|
65
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useListboxKeyboard } from "./listbox-keyboard.js";
|
|
4
5
|
import { selectRecipe } from "./select.recipe.js";
|
|
5
6
|
function optionText(o) {
|
|
6
7
|
if (o.keywords)
|
|
@@ -80,48 +81,14 @@ export function Select({ options, value, defaultValue, onValueChange, placeholde
|
|
|
80
81
|
setOpen(false);
|
|
81
82
|
triggerRef.current?.focus({ preventScroll: true });
|
|
82
83
|
};
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
: (pos + dir + enabledIndexes.length) % enabledIndexes.length;
|
|
92
|
-
setHighlight(enabledIndexes[nextPos] ?? 0);
|
|
93
|
-
};
|
|
94
|
-
const onNavKeys = (e) => {
|
|
95
|
-
switch (e.key) {
|
|
96
|
-
case "ArrowDown":
|
|
97
|
-
e.preventDefault();
|
|
98
|
-
moveHighlight(1);
|
|
99
|
-
break;
|
|
100
|
-
case "ArrowUp":
|
|
101
|
-
e.preventDefault();
|
|
102
|
-
moveHighlight(-1);
|
|
103
|
-
break;
|
|
104
|
-
case "Home":
|
|
105
|
-
e.preventDefault();
|
|
106
|
-
setHighlight(enabledIndexes[0] ?? 0);
|
|
107
|
-
break;
|
|
108
|
-
case "End":
|
|
109
|
-
e.preventDefault();
|
|
110
|
-
setHighlight(enabledIndexes[enabledIndexes.length - 1] ?? 0);
|
|
111
|
-
break;
|
|
112
|
-
case "Enter":
|
|
113
|
-
e.preventDefault();
|
|
114
|
-
pick(filtered[highlight]);
|
|
115
|
-
break;
|
|
116
|
-
case "Escape":
|
|
117
|
-
e.preventDefault();
|
|
118
|
-
close();
|
|
119
|
-
break;
|
|
120
|
-
case "Tab":
|
|
121
|
-
setOpen(false);
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
};
|
|
84
|
+
const { onKeyDown: onNavKeys } = useListboxKeyboard({
|
|
85
|
+
enabledIndexes,
|
|
86
|
+
highlight,
|
|
87
|
+
setHighlight,
|
|
88
|
+
commit: () => pick(filtered[highlight]),
|
|
89
|
+
close,
|
|
90
|
+
dismiss: () => setOpen(false),
|
|
91
|
+
});
|
|
125
92
|
const onTriggerKeyDown = (e) => {
|
|
126
93
|
if (!open) {
|
|
127
94
|
if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WAI-ARIA tablist keyboard model: ←/→ to move (wrapping), Home/End to
|
|
3
|
+
* jump, disabled tabs skipped, focus following selection.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `Tabs` so a surface that renders its own measured tab chrome —
|
|
6
|
+
* Inbox's flyout tabs, and whatever comes next — gets the identical model
|
|
7
|
+
* rather than a second partial one. The same reason `useMenuKeyboard` exists.
|
|
8
|
+
*
|
|
9
|
+
* Automatic activation (arrow selects, rather than arrow-then-Enter) is APG's
|
|
10
|
+
* recommendation when switching tabs is cheap, and it is what `Tabs` already
|
|
11
|
+
* did before this was extracted, so it stays the behaviour.
|
|
12
|
+
*/
|
|
13
|
+
export function useTablistKeyboard({ tabs, active, select, listRef, }) {
|
|
14
|
+
const onKeyDown = (e) => {
|
|
15
|
+
const enabled = tabs.filter((t) => !t.disabled);
|
|
16
|
+
if (!enabled.length)
|
|
17
|
+
return;
|
|
18
|
+
const idx = enabled.findIndex((t) => t.value === active);
|
|
19
|
+
let next = -1;
|
|
20
|
+
if (e.key === "ArrowRight")
|
|
21
|
+
next = (idx + 1) % enabled.length;
|
|
22
|
+
else if (e.key === "ArrowLeft")
|
|
23
|
+
next = (idx - 1 + enabled.length) % enabled.length;
|
|
24
|
+
else if (e.key === "Home")
|
|
25
|
+
next = 0;
|
|
26
|
+
else if (e.key === "End")
|
|
27
|
+
next = enabled.length - 1;
|
|
28
|
+
if (next < 0)
|
|
29
|
+
return;
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
const nextValue = enabled[next]?.value;
|
|
32
|
+
if (!nextValue)
|
|
33
|
+
return;
|
|
34
|
+
select(nextValue);
|
|
35
|
+
listRef.current
|
|
36
|
+
?.querySelector(`[data-value="${CSS.escape(nextValue)}"]`)
|
|
37
|
+
?.focus();
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* A tablist is a SINGLE tab stop: Tab enters the group and leaves it, and the
|
|
41
|
+
* arrows move within. Without this a keyboard user walks through every tab
|
|
42
|
+
* one at a time, and the number of stops grows with the tab set.
|
|
43
|
+
*/
|
|
44
|
+
const tabIndexFor = (value) => (value === active ? 0 : -1);
|
|
45
|
+
return { onKeyDown, tabIndexFor };
|
|
46
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { tabsRecipe } from "./tabs.recipe.js";
|
|
4
|
+
import { useTablistKeyboard } from "./tabs-keyboard.js";
|
|
4
5
|
/**
|
|
5
6
|
* A row of text tabs over a sliding ink underline — the record-view tab bar,
|
|
6
7
|
* measured from Attio. Roving-tabindex keyboard model (←/→ move + activate,
|
|
@@ -34,32 +35,15 @@ export function Tabs({ tabs, value, defaultValue, onValueChange, leading, childr
|
|
|
34
35
|
ro.observe(list);
|
|
35
36
|
return () => ro.disconnect();
|
|
36
37
|
}, [active, tabs]);
|
|
37
|
-
const onKeyDown = (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
else if (e.key === "ArrowLeft")
|
|
44
|
-
next = (idx - 1 + enabled.length) % enabled.length;
|
|
45
|
-
else if (e.key === "Home")
|
|
46
|
-
next = 0;
|
|
47
|
-
else if (e.key === "End")
|
|
48
|
-
next = enabled.length - 1;
|
|
49
|
-
if (next < 0)
|
|
50
|
-
return;
|
|
51
|
-
e.preventDefault();
|
|
52
|
-
const nextValue = enabled[next]?.value;
|
|
53
|
-
if (!nextValue)
|
|
54
|
-
return;
|
|
55
|
-
select(nextValue);
|
|
56
|
-
listRef.current
|
|
57
|
-
?.querySelector(`[data-value="${CSS.escape(nextValue)}"]`)
|
|
58
|
-
?.focus();
|
|
59
|
-
};
|
|
38
|
+
const { onKeyDown, tabIndexFor } = useTablistKeyboard({
|
|
39
|
+
tabs,
|
|
40
|
+
active,
|
|
41
|
+
select,
|
|
42
|
+
listRef,
|
|
43
|
+
});
|
|
60
44
|
return (_jsxs("div", { className: tabsRecipe({ className }), ...rest, children: [_jsxs("div", { ref: listRef, className: "tabs-list", role: "tablist", "aria-label": ariaLabel, onKeyDown: onKeyDown, children: [leading && _jsx("span", { className: "tabs-leading", children: leading }), tabs.map((t) => {
|
|
61
45
|
const selected = t.value === active;
|
|
62
|
-
return (_jsxs("button", { type: "button", role: "tab", id: `tab-${t.value}`, "aria-selected": selected, "aria-controls": `panel-${t.value}`, tabIndex:
|
|
46
|
+
return (_jsxs("button", { type: "button", role: "tab", id: `tab-${t.value}`, "aria-selected": selected, "aria-controls": `panel-${t.value}`, tabIndex: tabIndexFor(t.value), "data-value": t.value, "data-selected": selected ? "" : undefined, disabled: t.disabled, className: "tabs-tab", onClick: () => select(t.value), children: [t.icon, _jsx("span", { className: "tabs-tab-label", children: t.label }), t.badge != null && (_jsx("span", { className: "tabs-tab-badge", children: t.badge }))] }, t.value));
|
|
63
47
|
}), indicator && (_jsx("span", { className: "tabs-indicator", style: {
|
|
64
48
|
width: indicator.width,
|
|
65
49
|
transform: `translateX(${indicator.left}px)`,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Bell } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useTablistKeyboard } from "../../components/tabs/index.js";
|
|
3
5
|
import { inboxRecipe } from "./inbox.recipe.js";
|
|
4
6
|
/**
|
|
5
7
|
* A notifications inbox panel — measured from Attio's notification flyout. A
|
|
@@ -7,7 +9,17 @@ import { inboxRecipe } from "./inbox.recipe.js";
|
|
|
7
9
|
* (avatar + message + time + unread dot) or a centred empty state. Put it inside
|
|
8
10
|
* a Popover for the flyout.
|
|
9
11
|
*/
|
|
10
|
-
export function Inbox({ notifications = [], title = "Notifications", tabs, activeTab, onTabChange, emptyTitle = "No notifications", emptySubtitle = "Notifications such as mentions and important updates will be displayed here.", emptyAction, className, }) {
|
|
11
|
-
|
|
12
|
+
export function Inbox({ notifications = [], title = "Notifications", tabs, activeTab, onTabChange, emptyTitle = "No notifications", emptySubtitle = "Notifications such as mentions and important updates will be displayed here.", emptyAction, tabsLabel = "Notification views", className, }) {
|
|
13
|
+
const listRef = React.useRef(null);
|
|
14
|
+
// crisp's shared tablist model, the one `Tabs` runs (#195). Hand-rolling a
|
|
15
|
+
// second one is how Inbox ended up with no arrow keys, no roving tabindex
|
|
16
|
+
// and no name while `Tabs` had all three.
|
|
17
|
+
const { onKeyDown, tabIndexFor } = useTablistKeyboard({
|
|
18
|
+
tabs: (tabs ?? []).map((t) => ({ value: t.id, disabled: t.disabled })),
|
|
19
|
+
active: activeTab ?? "",
|
|
20
|
+
select: (id) => onTabChange?.(id),
|
|
21
|
+
listRef,
|
|
22
|
+
});
|
|
23
|
+
return (_jsxs("div", { className: inboxRecipe({ className }), children: [_jsxs("div", { className: "inbox-head", children: [_jsx("div", { className: "inbox-title", children: title }), tabs && tabs.length > 0 && (_jsx("div", { ref: listRef, className: "inbox-tabs", role: "tablist", "aria-label": tabsLabel, onKeyDown: onKeyDown, children: tabs.map((t) => (_jsxs("button", { type: "button", role: "tab", "aria-selected": t.id === activeTab, tabIndex: tabIndexFor(t.id), "data-value": t.id, disabled: t.disabled, className: "inbox-tab", "data-active": t.id === activeTab ? "" : undefined, onClick: () => onTabChange?.(t.id), children: [t.label, t.count != null && (_jsxs("span", { className: "inbox-tab-count", children: ["(", t.count, ")"] }))] }, t.id))) }))] }), notifications.length === 0 ? (_jsxs("div", { className: "inbox-empty", children: [_jsx("span", { className: "inbox-empty-icon", children: _jsx(Bell, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "inbox-empty-title", children: emptyTitle }), emptySubtitle != null && (_jsx("div", { className: "inbox-empty-sub", children: emptySubtitle })), emptyAction != null && (_jsx("div", { className: "inbox-empty-action", children: emptyAction }))] })) : (_jsx("div", { className: "inbox-list", children: notifications.map((n) => (_jsxs("button", { type: "button", className: "inbox-row", onClick: n.onClick, children: [n.avatar != null && (_jsx("span", { className: "inbox-row-avatar", children: n.avatar })), _jsxs("span", { className: "inbox-row-body", children: [_jsx("span", { className: "inbox-row-msg", children: n.message }), n.time != null && (_jsx("span", { className: "inbox-row-time", children: n.time }))] }), n.unread && _jsx("span", { className: "inbox-row-unread" })] }, n.id))) }))] }));
|
|
12
24
|
}
|
|
13
25
|
Inbox.displayName = "Inbox";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
export type UseListboxKeyboardOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Indexes into the rendered option list that may be highlighted, in order.
|
|
5
|
+
* Disabled options are left out, so the arrows step over them rather than
|
|
6
|
+
* landing on something that cannot be chosen.
|
|
7
|
+
*/
|
|
8
|
+
enabledIndexes: number[];
|
|
9
|
+
/** The highlighted index, or -1 when nothing is highlighted yet. */
|
|
10
|
+
highlight: number;
|
|
11
|
+
setHighlight: (index: number) => void;
|
|
12
|
+
/** Choose the highlighted option. Enter. */
|
|
13
|
+
commit: () => void;
|
|
14
|
+
/** Close the popup. Escape. */
|
|
15
|
+
close: () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Close WITHOUT the focus handling `close` does, for Tab — the browser is
|
|
18
|
+
* already moving focus to the next element, so anything that pulls it back
|
|
19
|
+
* lands the user in the wrong place. Defaults to `close`.
|
|
20
|
+
*/
|
|
21
|
+
dismiss?: () => void;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The WAI-ARIA listbox keyboard model for a POPUP listbox: ↑/↓ to move the
|
|
25
|
+
* highlight (wrapping, skipping disabled options), Home/End to jump, Enter to
|
|
26
|
+
* choose, Escape to close, Tab to dismiss.
|
|
27
|
+
*
|
|
28
|
+
* Extracted from `Select`, whose set was the complete one, so `MultiSelect` —
|
|
29
|
+
* the same widget with multiple selection — stops being a second partial copy
|
|
30
|
+
* that had no Home/End and stayed open on Tab (#198). The same reason
|
|
31
|
+
* `useMenuKeyboard` and `useTablistKeyboard` exist.
|
|
32
|
+
*
|
|
33
|
+
* NOT for `WheelPicker`: that is an always-visible multi-column wheel that
|
|
34
|
+
* commits on move, so Enter, Escape and Tab have no meaning in it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function useListboxKeyboard({ enabledIndexes, highlight, setHighlight, commit, close, dismiss, }: UseListboxKeyboardOptions): {
|
|
37
|
+
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
38
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* One entry in a tablist, as far as the keyboard model cares. Callers pass
|
|
4
|
+
* whatever richer shape they render — only these two fields are read.
|
|
5
|
+
*/
|
|
6
|
+
export type TablistEntry = {
|
|
7
|
+
value: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type UseTablistKeyboardOptions<T extends TablistEntry> = {
|
|
11
|
+
/** Every tab, in rendered order, including the disabled ones. */
|
|
12
|
+
tabs: T[];
|
|
13
|
+
/** The selected tab's value. */
|
|
14
|
+
active: string;
|
|
15
|
+
/** Select a tab. Arrow keys activate immediately, as APG's automatic mode. */
|
|
16
|
+
select: (value: string) => void;
|
|
17
|
+
/** The element carrying `role="tablist"`; used to move focus. */
|
|
18
|
+
listRef: React.RefObject<HTMLElement | null>;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The WAI-ARIA tablist keyboard model: ←/→ to move (wrapping), Home/End to
|
|
22
|
+
* jump, disabled tabs skipped, focus following selection.
|
|
23
|
+
*
|
|
24
|
+
* Extracted from `Tabs` so a surface that renders its own measured tab chrome —
|
|
25
|
+
* Inbox's flyout tabs, and whatever comes next — gets the identical model
|
|
26
|
+
* rather than a second partial one. The same reason `useMenuKeyboard` exists.
|
|
27
|
+
*
|
|
28
|
+
* Automatic activation (arrow selects, rather than arrow-then-Enter) is APG's
|
|
29
|
+
* recommendation when switching tabs is cheap, and it is what `Tabs` already
|
|
30
|
+
* did before this was extracted, so it stays the behaviour.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useTablistKeyboard<T extends TablistEntry>({ tabs, active, select, listRef, }: UseTablistKeyboardOptions<T>): {
|
|
33
|
+
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
34
|
+
tabIndexFor: (value: string) => -1 | 0;
|
|
35
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as React from "react";
|
|
2
2
|
export type InboxNotification = {
|
|
3
3
|
id: string;
|
|
4
4
|
/** Leading avatar or icon. */
|
|
@@ -14,6 +14,7 @@ export type InboxTab = {
|
|
|
14
14
|
id: string;
|
|
15
15
|
label: React.ReactNode;
|
|
16
16
|
count?: number;
|
|
17
|
+
disabled?: boolean;
|
|
17
18
|
};
|
|
18
19
|
export type InboxProps = {
|
|
19
20
|
notifications?: InboxNotification[];
|
|
@@ -27,6 +28,8 @@ export type InboxProps = {
|
|
|
27
28
|
emptySubtitle?: React.ReactNode;
|
|
28
29
|
/** Optional action under the empty state (e.g. a Settings button). */
|
|
29
30
|
emptyAction?: React.ReactNode;
|
|
31
|
+
/** Accessible name for the tab list. */
|
|
32
|
+
tabsLabel?: string;
|
|
30
33
|
className?: string;
|
|
31
34
|
};
|
|
32
35
|
/**
|
|
@@ -35,7 +38,7 @@ export type InboxProps = {
|
|
|
35
38
|
* (avatar + message + time + unread dot) or a centred empty state. Put it inside
|
|
36
39
|
* a Popover for the flyout.
|
|
37
40
|
*/
|
|
38
|
-
export declare function Inbox({ notifications, title, tabs, activeTab, onTabChange, emptyTitle, emptySubtitle, emptyAction, className, }: InboxProps): React.JSX.Element;
|
|
41
|
+
export declare function Inbox({ notifications, title, tabs, activeTab, onTabChange, emptyTitle, emptySubtitle, emptyAction, tabsLabel, className, }: InboxProps): React.JSX.Element;
|
|
39
42
|
export declare namespace Inbox {
|
|
40
43
|
var displayName: string;
|
|
41
44
|
}
|