@apliteni/apliteni-ui 0.33.1 → 0.34.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/package.json +1 -1
- package/react/README.md +87 -1
- package/react/dist/index.d.ts +122 -2
- package/react/dist/index.js +442 -11
- package/src/components/dropdown.js +36 -15
- package/src/components/shell.js +124 -37
- package/src/components/tooltip.js +3 -0
- package/src/styles/back.css +12 -1
- package/src/styles/dropdown.css +37 -10
- package/src/styles/layout.css +124 -6
- package/src/styles/topbar.css +1 -1
package/react/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ var cx = (...a) => a.filter(Boolean).join(" ");
|
|
|
20
20
|
function Button({
|
|
21
21
|
variant = "secondary",
|
|
22
22
|
size = "md",
|
|
23
|
-
icon:
|
|
23
|
+
icon: icon4,
|
|
24
24
|
iconRight,
|
|
25
25
|
iconOnly,
|
|
26
26
|
block,
|
|
@@ -38,7 +38,7 @@ function Button({
|
|
|
38
38
|
iconOnly && "ui-btn--icon"
|
|
39
39
|
);
|
|
40
40
|
const labelled = rest["aria-label"] != null || rest["aria-labelledby"] != null;
|
|
41
|
-
const fallback = typeof children === "string" && children.trim() ? children.trim() :
|
|
41
|
+
const fallback = typeof children === "string" && children.trim() ? children.trim() : icon4;
|
|
42
42
|
const named = iconOnly && !labelled && fallback ? { "aria-label": fallback, title: rest.title ?? fallback } : {};
|
|
43
43
|
return /* @__PURE__ */ jsxs(
|
|
44
44
|
"button",
|
|
@@ -51,7 +51,7 @@ function Button({
|
|
|
51
51
|
...rest,
|
|
52
52
|
...named,
|
|
53
53
|
children: [
|
|
54
|
-
|
|
54
|
+
icon4 && /* @__PURE__ */ jsx2(Icon, { name: icon4 }),
|
|
55
55
|
!iconOnly && children != null && /* @__PURE__ */ jsx2("span", { children }),
|
|
56
56
|
iconRight && /* @__PURE__ */ jsx2(Icon, { name: iconRight }),
|
|
57
57
|
busy && /* @__PURE__ */ jsxs("span", { className: "ui-btn__bars", children: [
|
|
@@ -87,8 +87,8 @@ function Card({ title, sub, level = 2, children }) {
|
|
|
87
87
|
import { useId } from "react";
|
|
88
88
|
import { Fragment, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
89
89
|
var GLYPH = { up: "arrowUp", down: "arrowDown", flat: "minus" };
|
|
90
|
-
var directionOf = (
|
|
91
|
-
const t =
|
|
90
|
+
var directionOf = (text2) => {
|
|
91
|
+
const t = text2.trim();
|
|
92
92
|
return /^[-−–(]/.test(t) ? "down" : /^\+/.test(t) ? "up" : "flat";
|
|
93
93
|
};
|
|
94
94
|
var hasChange = (d) => !!d && d.value != null && d.value !== "";
|
|
@@ -683,10 +683,10 @@ function Pagination({
|
|
|
683
683
|
was.focus();
|
|
684
684
|
return;
|
|
685
685
|
}
|
|
686
|
-
const
|
|
687
|
-
const from2 =
|
|
688
|
-
for (let d = 1; d <
|
|
689
|
-
const near = [
|
|
686
|
+
const strip2 = [...nav.current.querySelectorAll(".ui-pager__step")];
|
|
687
|
+
const from2 = strip2.indexOf(was);
|
|
688
|
+
for (let d = 1; d < strip2.length; d += 1) {
|
|
689
|
+
const near = [strip2[from2 - d], strip2[from2 + d]].find((el) => el && !el.disabled);
|
|
690
690
|
if (near) {
|
|
691
691
|
near.focus();
|
|
692
692
|
return;
|
|
@@ -1024,12 +1024,12 @@ function Denied({
|
|
|
1024
1024
|
title = "You don\u2019t have access",
|
|
1025
1025
|
sub,
|
|
1026
1026
|
need,
|
|
1027
|
-
icon:
|
|
1027
|
+
icon: icon4 = "lock",
|
|
1028
1028
|
className,
|
|
1029
1029
|
children
|
|
1030
1030
|
}) {
|
|
1031
1031
|
return /* @__PURE__ */ jsxs9("div", { className: cx4("ui-denied", className), children: [
|
|
1032
|
-
/* @__PURE__ */ jsx11("div", { className: "ui-denied__seal", children: /* @__PURE__ */ jsx11(Icon, { name:
|
|
1032
|
+
/* @__PURE__ */ jsx11("div", { className: "ui-denied__seal", children: /* @__PURE__ */ jsx11(Icon, { name: icon4 }) }),
|
|
1033
1033
|
/* @__PURE__ */ jsx11("div", { className: "ui-denied__title", children: title }),
|
|
1034
1034
|
sub && /* @__PURE__ */ jsx11("div", { className: "ui-denied__sub", children: sub }),
|
|
1035
1035
|
need && /* @__PURE__ */ jsxs9("div", { className: "ui-denied__need", children: [
|
|
@@ -1040,10 +1040,440 @@ function Denied({
|
|
|
1040
1040
|
] });
|
|
1041
1041
|
}
|
|
1042
1042
|
|
|
1043
|
+
// src/Dropdown.tsx
|
|
1044
|
+
import {
|
|
1045
|
+
Fragment as Fragment3,
|
|
1046
|
+
useCallback as useCallback2,
|
|
1047
|
+
useEffect as useEffect4,
|
|
1048
|
+
useId as useId5,
|
|
1049
|
+
useRef as useRef6,
|
|
1050
|
+
useState as useState5
|
|
1051
|
+
} from "react";
|
|
1052
|
+
import { icon as icon3, dropdownMatch, dropdownFiltering } from "@apliteni/apliteni-ui";
|
|
1053
|
+
import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1054
|
+
var cx5 = (...a) => a.filter(Boolean).join(" ");
|
|
1055
|
+
var isRow = (it) => Boolean(it) && it !== "---" && !it.separator;
|
|
1056
|
+
var Glyph2 = ({ name, className, hidden }) => /* @__PURE__ */ jsx12(
|
|
1057
|
+
"span",
|
|
1058
|
+
{
|
|
1059
|
+
className,
|
|
1060
|
+
"aria-hidden": hidden ? "true" : void 0,
|
|
1061
|
+
dangerouslySetInnerHTML: { __html: icon3(name) }
|
|
1062
|
+
}
|
|
1063
|
+
);
|
|
1064
|
+
function RowBadge({ badge }) {
|
|
1065
|
+
const text2 = typeof badge === "string" ? badge : badge.text;
|
|
1066
|
+
const given = typeof badge === "string" ? "" : badge.tone || "";
|
|
1067
|
+
const tone = given || (/^live$/i.test(String(text2)) ? "live" : "neutral");
|
|
1068
|
+
return /* @__PURE__ */ jsx12("span", { className: `ui-dropdown__badge is-${tone}`, children: text2 });
|
|
1069
|
+
}
|
|
1070
|
+
var keyOf = (it) => it.value != null ? `v:${it.value}` : `l:${it.label}`;
|
|
1071
|
+
var itemsIn = (panel) => Array.from(panel?.querySelectorAll("[data-dd-item]") ?? []).filter((el) => el.getAttribute("aria-disabled") !== "true" && !el.hidden);
|
|
1072
|
+
var openNow = /* @__PURE__ */ new Set();
|
|
1073
|
+
var composing = (e) => e.isComposing || e.keyCode === 229;
|
|
1074
|
+
var SEARCH_DEFAULTS = {
|
|
1075
|
+
placeholder: "Search",
|
|
1076
|
+
empty: "No match for \u201C{q}\u201D",
|
|
1077
|
+
hint: "Check the spelling, or try fewer letters."
|
|
1078
|
+
};
|
|
1079
|
+
function Dropdown({
|
|
1080
|
+
label,
|
|
1081
|
+
value,
|
|
1082
|
+
placeholder = "Select\u2026",
|
|
1083
|
+
variant,
|
|
1084
|
+
items,
|
|
1085
|
+
sections,
|
|
1086
|
+
header,
|
|
1087
|
+
footer,
|
|
1088
|
+
triggerContent,
|
|
1089
|
+
triggerClass = "",
|
|
1090
|
+
chevron = true,
|
|
1091
|
+
align = "start",
|
|
1092
|
+
direction = "down",
|
|
1093
|
+
scroll = false,
|
|
1094
|
+
search = false,
|
|
1095
|
+
ariaLabel,
|
|
1096
|
+
id,
|
|
1097
|
+
panelClass = "",
|
|
1098
|
+
open: openProp,
|
|
1099
|
+
defaultOpen = false,
|
|
1100
|
+
onOpenChange,
|
|
1101
|
+
onSelect,
|
|
1102
|
+
row
|
|
1103
|
+
}) {
|
|
1104
|
+
const [selfOpen, setSelfOpen] = useState5(defaultOpen);
|
|
1105
|
+
const open = openProp ?? selfOpen;
|
|
1106
|
+
const [pick, setPick] = useState5(null);
|
|
1107
|
+
const [query, setQuery] = useState5(() => search && search !== true ? search.query || "" : "");
|
|
1108
|
+
const [activeKey, setActiveKey] = useState5(null);
|
|
1109
|
+
const root = useRef6(null);
|
|
1110
|
+
const panel = useRef6(null);
|
|
1111
|
+
const trigger = useRef6(null);
|
|
1112
|
+
const field = useRef6(null);
|
|
1113
|
+
const landOn = useRef6(null);
|
|
1114
|
+
const at = useRef6("");
|
|
1115
|
+
const auto = useId5().replace(/:/g, "");
|
|
1116
|
+
const uid = id ?? auto;
|
|
1117
|
+
const setOpen = useCallback2((next) => {
|
|
1118
|
+
if (openProp === void 0) setSelfOpen(next);
|
|
1119
|
+
onOpenChange?.(next);
|
|
1120
|
+
}, [openProp, onOpenChange]);
|
|
1121
|
+
const close = useRef6(setOpen);
|
|
1122
|
+
useIsoLayoutEffect(() => {
|
|
1123
|
+
close.current = setOpen;
|
|
1124
|
+
});
|
|
1125
|
+
const sx = search ? { ...SEARCH_DEFAULTS, ...strip(search === true ? {} : search) } : null;
|
|
1126
|
+
const entries = sections?.length ? sections.flatMap((s) => s.items || []) : items || [];
|
|
1127
|
+
const rows = entries.filter(isRow);
|
|
1128
|
+
const isSelect = variant === "select" || variant == null && rows.some((it) => it.selected || it.value != null);
|
|
1129
|
+
const listRole = isSelect ? "listbox" : "menu";
|
|
1130
|
+
const asOption = isSelect || Boolean(sx);
|
|
1131
|
+
const name = ariaLabel || (label ? String(label).replace(/:\s*$/, "") : "") || "Options";
|
|
1132
|
+
const given = rows.find((it) => it.selected);
|
|
1133
|
+
const givenKey = given ? keyOf(given) : null;
|
|
1134
|
+
const pickedKey = pick && pick.against === givenKey ? pick.key : null;
|
|
1135
|
+
useEffect4(() => {
|
|
1136
|
+
if (pick && pick.against !== givenKey) setPick(null);
|
|
1137
|
+
}, [givenKey, pick]);
|
|
1138
|
+
const picked = pickedKey != null ? rows.find((it) => keyOf(it) === pickedKey) : void 0;
|
|
1139
|
+
const shown = value != null ? value : isSelect ? picked?.label ?? given?.label : null;
|
|
1140
|
+
const selectedOf = (it) => pickedKey != null ? it.disabled ? !!it.selected : keyOf(it) === pickedKey : !!it.selected;
|
|
1141
|
+
const hiddenBy = (it) => Boolean(sx) && !dropdownMatch(it.label, query);
|
|
1142
|
+
const filtering = Boolean(sx) && dropdownFiltering(query);
|
|
1143
|
+
const visible = rows.filter((it) => !hiddenBy(it));
|
|
1144
|
+
const ring = visible.filter((it) => !it.disabled);
|
|
1145
|
+
const active = ring.find((it) => keyOf(it) === activeKey) ?? ring[0];
|
|
1146
|
+
const rowIds = /* @__PURE__ */ new Map();
|
|
1147
|
+
rows.forEach((it, i) => rowIds.set(it, `${uid}-opt-${i}`));
|
|
1148
|
+
const listId = `${uid}-list`;
|
|
1149
|
+
useEffect4(() => {
|
|
1150
|
+
if (!open) return;
|
|
1151
|
+
const mine = () => close.current(false);
|
|
1152
|
+
for (const other of [...openNow]) other();
|
|
1153
|
+
openNow.add(mine);
|
|
1154
|
+
return () => {
|
|
1155
|
+
openNow.delete(mine);
|
|
1156
|
+
};
|
|
1157
|
+
}, [open]);
|
|
1158
|
+
useEffect4(() => {
|
|
1159
|
+
if (!open) return;
|
|
1160
|
+
const doc = root.current?.ownerDocument ?? document;
|
|
1161
|
+
const onClick = (e) => {
|
|
1162
|
+
if (!root.current?.contains(e.target)) close.current(false);
|
|
1163
|
+
};
|
|
1164
|
+
const onKey2 = (e) => {
|
|
1165
|
+
if (e.key !== "Escape" || composing(e)) return;
|
|
1166
|
+
close.current(false);
|
|
1167
|
+
trigger.current?.focus();
|
|
1168
|
+
};
|
|
1169
|
+
doc.addEventListener("click", onClick);
|
|
1170
|
+
doc.addEventListener("keydown", onKey2);
|
|
1171
|
+
return () => {
|
|
1172
|
+
doc.removeEventListener("click", onClick);
|
|
1173
|
+
doc.removeEventListener("keydown", onKey2);
|
|
1174
|
+
};
|
|
1175
|
+
}, [open]);
|
|
1176
|
+
useIsoLayoutEffect(() => {
|
|
1177
|
+
const want = landOn.current;
|
|
1178
|
+
landOn.current = null;
|
|
1179
|
+
if (!open) return;
|
|
1180
|
+
if (sx) {
|
|
1181
|
+
setQuery("");
|
|
1182
|
+
const enabled = rows.filter((it) => !it.disabled);
|
|
1183
|
+
const start = enabled.find(selectedOf) ?? enabled[0];
|
|
1184
|
+
setActiveKey(start ? keyOf(start) : null);
|
|
1185
|
+
if (panel.current) {
|
|
1186
|
+
panel.current.style.minWidth = "";
|
|
1187
|
+
if (panel.current.offsetWidth) panel.current.style.minWidth = `${panel.current.offsetWidth}px`;
|
|
1188
|
+
}
|
|
1189
|
+
field.current?.focus();
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
if (want == null) return;
|
|
1193
|
+
const els = itemsIn(panel.current);
|
|
1194
|
+
if (!els.length) return;
|
|
1195
|
+
const sel = els.findIndex((el) => el.getAttribute("aria-selected") === "true");
|
|
1196
|
+
const to = want === "selected" ? sel >= 0 ? sel : 0 : want;
|
|
1197
|
+
(els[to] || els[0]).focus();
|
|
1198
|
+
}, [open]);
|
|
1199
|
+
useIsoLayoutEffect(() => {
|
|
1200
|
+
if (!sx || !open || !active) return;
|
|
1201
|
+
const el = panel.current?.querySelector("[data-dd-item].is-active");
|
|
1202
|
+
const list = el?.closest(".ui-dropdown__list");
|
|
1203
|
+
if (!el || !list || !list.clientHeight) return;
|
|
1204
|
+
const top = el.offsetTop - list.offsetTop;
|
|
1205
|
+
if (top < list.scrollTop) list.scrollTop = top;
|
|
1206
|
+
else if (top + el.offsetHeight > list.scrollTop + list.clientHeight) {
|
|
1207
|
+
list.scrollTop = top + el.offsetHeight - list.clientHeight;
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
const choose = (item, e) => {
|
|
1211
|
+
if (item.disabled) {
|
|
1212
|
+
e.preventDefault();
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (isSelect) setPick({ key: keyOf(item), against: givenKey });
|
|
1216
|
+
onSelect?.(item.value, item);
|
|
1217
|
+
setOpen(false);
|
|
1218
|
+
trigger.current?.focus();
|
|
1219
|
+
};
|
|
1220
|
+
const onKeyDown = (e) => {
|
|
1221
|
+
const onTrigger = e.target === trigger.current;
|
|
1222
|
+
const onField = e.target === field.current;
|
|
1223
|
+
if (onField && composing(e)) return;
|
|
1224
|
+
if (sx && open) {
|
|
1225
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
1226
|
+
e.preventDefault();
|
|
1227
|
+
if (!ring.length) return;
|
|
1228
|
+
const i = active ? ring.indexOf(active) : -1;
|
|
1229
|
+
const next = e.key === "ArrowDown" ? (i + 1) % ring.length : i <= 0 ? ring.length - 1 : i - 1;
|
|
1230
|
+
setActiveKey(keyOf(ring[next]));
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (onField && e.key === "Enter") {
|
|
1234
|
+
e.preventDefault();
|
|
1235
|
+
panel.current?.querySelector("[data-dd-item].is-active")?.click();
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (onField && (e.key === "Home" || e.key === "End")) return;
|
|
1239
|
+
}
|
|
1240
|
+
if ((e.key === "ArrowDown" || e.key === "ArrowUp") && (onTrigger || open)) {
|
|
1241
|
+
e.preventDefault();
|
|
1242
|
+
if (!open) {
|
|
1243
|
+
landOn.current = e.key === "ArrowDown" ? 0 : "selected";
|
|
1244
|
+
setOpen(true);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const els = itemsIn(panel.current);
|
|
1248
|
+
if (!els.length) return;
|
|
1249
|
+
const i = els.indexOf(document.activeElement);
|
|
1250
|
+
const next = e.key === "ArrowDown" ? (i + 1) % els.length : (i - 1 + els.length) % els.length;
|
|
1251
|
+
els[next < 0 ? 0 : next]?.focus();
|
|
1252
|
+
} else if (e.key === "Home" && open) {
|
|
1253
|
+
e.preventDefault();
|
|
1254
|
+
itemsIn(panel.current)[0]?.focus();
|
|
1255
|
+
} else if (e.key === "End" && open) {
|
|
1256
|
+
e.preventDefault();
|
|
1257
|
+
const els = itemsIn(panel.current);
|
|
1258
|
+
els[els.length - 1]?.focus();
|
|
1259
|
+
} else if ((e.key === "Enter" || e.key === " ") && open && e.target.matches?.("[data-dd-item]")) {
|
|
1260
|
+
e.preventDefault();
|
|
1261
|
+
e.target.click();
|
|
1262
|
+
} else if (e.key === "Escape" && open) {
|
|
1263
|
+
e.preventDefault();
|
|
1264
|
+
e.stopPropagation();
|
|
1265
|
+
setOpen(false);
|
|
1266
|
+
trigger.current?.focus();
|
|
1267
|
+
} else if (e.key === "Tab" && open) {
|
|
1268
|
+
setOpen(false);
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
const onMouseMove = (e) => {
|
|
1272
|
+
if (!sx) return;
|
|
1273
|
+
const here = `${e.clientX},${e.clientY}`;
|
|
1274
|
+
if (here === at.current) return;
|
|
1275
|
+
at.current = here;
|
|
1276
|
+
const el = e.target.closest?.("[data-dd-item]");
|
|
1277
|
+
if (!el || el.getAttribute("aria-disabled") === "true" || el.classList.contains("is-active")) return;
|
|
1278
|
+
const item = rows.find((it) => rowIds.get(it) === el.id);
|
|
1279
|
+
if (item) setActiveKey(keyOf(item));
|
|
1280
|
+
};
|
|
1281
|
+
function renderRow(entry, key) {
|
|
1282
|
+
if (!isRow(entry)) {
|
|
1283
|
+
return /* @__PURE__ */ jsx12("div", { className: "ui-dropdown__sep", role: "separator", hidden: filtering || void 0 }, key);
|
|
1284
|
+
}
|
|
1285
|
+
const disabled = !!entry.disabled;
|
|
1286
|
+
const selected = selectedOf(entry);
|
|
1287
|
+
const asLink = !!entry.href && !disabled && !asOption;
|
|
1288
|
+
const props = {
|
|
1289
|
+
className: cx5(
|
|
1290
|
+
"ui-dropdown__item",
|
|
1291
|
+
selected && "is-selected",
|
|
1292
|
+
disabled && "is-disabled",
|
|
1293
|
+
entry.danger && "is-danger",
|
|
1294
|
+
Boolean(sx) && open && entry === active && "is-active"
|
|
1295
|
+
),
|
|
1296
|
+
"data-dd-item": "",
|
|
1297
|
+
role: asOption ? "option" : "menuitem",
|
|
1298
|
+
tabIndex: -1,
|
|
1299
|
+
...entry.value != null ? { "data-value": String(entry.value) } : null,
|
|
1300
|
+
...asOption ? { "aria-selected": selected } : null,
|
|
1301
|
+
...disabled ? { "aria-disabled": true } : null,
|
|
1302
|
+
...asLink ? { href: entry.href, ...entry.target ? { target: entry.target } : null } : null,
|
|
1303
|
+
...sx ? { id: rowIds.get(entry), hidden: hiddenBy(entry) || void 0 } : null,
|
|
1304
|
+
onClick: (e) => choose(entry, e),
|
|
1305
|
+
children: /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
1306
|
+
entry.icon && /* @__PURE__ */ jsx12(Glyph2, { name: entry.icon, className: "ui-dropdown__ic" }),
|
|
1307
|
+
/* @__PURE__ */ jsxs10("span", { className: "ui-dropdown__main", children: [
|
|
1308
|
+
/* @__PURE__ */ jsx12("span", { className: "ui-dropdown__label", children: entry.label }),
|
|
1309
|
+
entry.description && /* @__PURE__ */ jsx12("span", { className: "ui-dropdown__desc", children: entry.description })
|
|
1310
|
+
] }),
|
|
1311
|
+
entry.badge ? /* @__PURE__ */ jsx12(RowBadge, { badge: entry.badge }) : null,
|
|
1312
|
+
asOption && /* @__PURE__ */ jsx12(Glyph2, { name: "check", className: "ui-dropdown__tick", hidden: true })
|
|
1313
|
+
] })
|
|
1314
|
+
};
|
|
1315
|
+
if (row) return /* @__PURE__ */ jsx12(Fragment3, { children: row(entry, props) }, key);
|
|
1316
|
+
const Tag = asLink ? "a" : "div";
|
|
1317
|
+
return /* @__PURE__ */ jsx12(Tag, { ...props }, key);
|
|
1318
|
+
}
|
|
1319
|
+
const body = sections?.length ? sections.map((section, si) => {
|
|
1320
|
+
const gone = filtering && !(section.items || []).some((it) => isRow(it) && !hiddenBy(it));
|
|
1321
|
+
return /* @__PURE__ */ jsxs10(
|
|
1322
|
+
"div",
|
|
1323
|
+
{
|
|
1324
|
+
className: "ui-dropdown__section",
|
|
1325
|
+
role: "group",
|
|
1326
|
+
"aria-label": section.label || void 0,
|
|
1327
|
+
hidden: gone || void 0,
|
|
1328
|
+
children: [
|
|
1329
|
+
section.label && /* @__PURE__ */ jsx12("div", { className: "ui-dropdown__group", role: "presentation", children: section.label }),
|
|
1330
|
+
(section.items || []).map((entry, i) => renderRow(entry, `s${si}-${i}`))
|
|
1331
|
+
]
|
|
1332
|
+
},
|
|
1333
|
+
section.label ?? `s${si}`
|
|
1334
|
+
);
|
|
1335
|
+
}) : (items || []).map((entry, i) => renderRow(entry, `i${i}`));
|
|
1336
|
+
const cap = scroll && scroll !== true ? { maxHeight: typeof scroll === "number" ? `${scroll}px` : scroll } : void 0;
|
|
1337
|
+
const none = sx && filtering && !visible.length ? /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
1338
|
+
/* @__PURE__ */ jsx12("span", { className: "ui-dropdown__none-title", children: sx.empty.replace("{q}", () => query.trim()) }),
|
|
1339
|
+
/* @__PURE__ */ jsx12("span", { className: "ui-dropdown__none-hint", children: sx.hint })
|
|
1340
|
+
] }) : null;
|
|
1341
|
+
return /* @__PURE__ */ jsxs10("div", { className: cx5("ui-dropdown", open && "open"), id, ref: root, onKeyDown, children: [
|
|
1342
|
+
/* @__PURE__ */ jsxs10(
|
|
1343
|
+
"button",
|
|
1344
|
+
{
|
|
1345
|
+
type: "button",
|
|
1346
|
+
className: cx5("ui-dropdown__trigger", triggerClass),
|
|
1347
|
+
"data-dropdown-trigger": "",
|
|
1348
|
+
"aria-haspopup": sx ? "dialog" : listRole,
|
|
1349
|
+
"aria-expanded": open,
|
|
1350
|
+
"aria-label": ariaLabel && triggerContent != null ? ariaLabel : void 0,
|
|
1351
|
+
ref: trigger,
|
|
1352
|
+
onClick: (e) => {
|
|
1353
|
+
e.stopPropagation();
|
|
1354
|
+
setOpen(!open);
|
|
1355
|
+
},
|
|
1356
|
+
children: [
|
|
1357
|
+
triggerContent != null ? triggerContent : /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
1358
|
+
label && /* @__PURE__ */ jsx12("span", { className: "ui-dropdown__pre", children: label }),
|
|
1359
|
+
/* @__PURE__ */ jsx12("span", { className: "ui-dropdown__value", children: shown != null ? shown : placeholder })
|
|
1360
|
+
] }),
|
|
1361
|
+
chevron && /* @__PURE__ */ jsx12("span", { className: "ui-dropdown__chevron", "aria-hidden": "true" })
|
|
1362
|
+
]
|
|
1363
|
+
}
|
|
1364
|
+
),
|
|
1365
|
+
/* @__PURE__ */ jsxs10(
|
|
1366
|
+
"div",
|
|
1367
|
+
{
|
|
1368
|
+
className: cx5(
|
|
1369
|
+
"ui-dropdown__panel",
|
|
1370
|
+
align === "end" && "is-end",
|
|
1371
|
+
direction === "up" && "is-up",
|
|
1372
|
+
Boolean(scroll) && !sx && "is-scroll",
|
|
1373
|
+
Boolean(sx) && "ui-dropdown__panel--search",
|
|
1374
|
+
panelClass
|
|
1375
|
+
),
|
|
1376
|
+
"data-dropdown-panel": "",
|
|
1377
|
+
role: sx ? "dialog" : listRole,
|
|
1378
|
+
"aria-label": sx ? name : ariaLabel,
|
|
1379
|
+
style: sx ? void 0 : cap,
|
|
1380
|
+
ref: panel,
|
|
1381
|
+
onClick: (e) => e.stopPropagation(),
|
|
1382
|
+
onMouseMove,
|
|
1383
|
+
children: [
|
|
1384
|
+
header,
|
|
1385
|
+
sx ? /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
1386
|
+
/* @__PURE__ */ jsxs10("div", { className: "ui-dropdown__search", children: [
|
|
1387
|
+
/* @__PURE__ */ jsx12(Glyph2, { name: "search", className: "ui-dropdown__search-ic", hidden: true }),
|
|
1388
|
+
/* @__PURE__ */ jsx12(
|
|
1389
|
+
"input",
|
|
1390
|
+
{
|
|
1391
|
+
className: "ui-dropdown__search-input",
|
|
1392
|
+
type: "text",
|
|
1393
|
+
role: "combobox",
|
|
1394
|
+
"aria-autocomplete": "list",
|
|
1395
|
+
"aria-expanded": true,
|
|
1396
|
+
"aria-controls": listId,
|
|
1397
|
+
"aria-label": sx.label || `Search ${name}`,
|
|
1398
|
+
"aria-activedescendant": open && active ? rowIds.get(active) : void 0,
|
|
1399
|
+
placeholder: sx.placeholder,
|
|
1400
|
+
autoComplete: "off",
|
|
1401
|
+
spellCheck: false,
|
|
1402
|
+
"data-dd-search": "",
|
|
1403
|
+
value: query,
|
|
1404
|
+
ref: field,
|
|
1405
|
+
onChange: (e) => {
|
|
1406
|
+
setQuery(e.target.value);
|
|
1407
|
+
setActiveKey(null);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
)
|
|
1411
|
+
] }),
|
|
1412
|
+
/* @__PURE__ */ jsx12("div", { className: "ui-dropdown__list", role: "listbox", id: listId, "aria-label": name, style: cap, children: body }),
|
|
1413
|
+
/* @__PURE__ */ jsx12(
|
|
1414
|
+
"div",
|
|
1415
|
+
{
|
|
1416
|
+
className: "ui-dropdown__none",
|
|
1417
|
+
role: "status",
|
|
1418
|
+
"data-dd-none": "",
|
|
1419
|
+
"data-dd-empty": sx.empty,
|
|
1420
|
+
"data-dd-hint": sx.hint,
|
|
1421
|
+
children: none
|
|
1422
|
+
}
|
|
1423
|
+
)
|
|
1424
|
+
] }) : body,
|
|
1425
|
+
footer
|
|
1426
|
+
]
|
|
1427
|
+
}
|
|
1428
|
+
)
|
|
1429
|
+
] });
|
|
1430
|
+
}
|
|
1431
|
+
function strip(o) {
|
|
1432
|
+
return Object.fromEntries(
|
|
1433
|
+
Object.entries(o).filter(([, v]) => v != null && v !== "")
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// src/BackLink.tsx
|
|
1438
|
+
import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1439
|
+
var BARE = "Back";
|
|
1440
|
+
var SCRIPTED = /^javascript:/i;
|
|
1441
|
+
var LEADING = /^[\u0000-\u0020]+/;
|
|
1442
|
+
var TAB_OR_NEWLINE = /[\t\n\r]/g;
|
|
1443
|
+
var SAID = /^back\s+to(?:\s+|$)/i;
|
|
1444
|
+
var text = (v) => typeof v === "string" || typeof v === "number" ? String(v).trim() : "";
|
|
1445
|
+
function BackLink({
|
|
1446
|
+
href,
|
|
1447
|
+
label,
|
|
1448
|
+
as,
|
|
1449
|
+
className,
|
|
1450
|
+
...rest
|
|
1451
|
+
}) {
|
|
1452
|
+
const to = text(href);
|
|
1453
|
+
if (!to || SCRIPTED.test(to.replace(LEADING, "").replace(TAB_OR_NEWLINE, ""))) return null;
|
|
1454
|
+
const name = text(label).replace(SAID, "");
|
|
1455
|
+
const bare = !name || name.toLowerCase() === BARE.toLowerCase();
|
|
1456
|
+
const Tag = as ?? "a";
|
|
1457
|
+
return /* @__PURE__ */ jsxs11(
|
|
1458
|
+
Tag,
|
|
1459
|
+
{
|
|
1460
|
+
className: ["ui-back", className].filter(Boolean).join(" "),
|
|
1461
|
+
href: to,
|
|
1462
|
+
"aria-label": bare ? void 0 : `${BARE} to ${name}`,
|
|
1463
|
+
...rest,
|
|
1464
|
+
children: [
|
|
1465
|
+
/* @__PURE__ */ jsx13(Icon, { name: "chevronLeft" }),
|
|
1466
|
+
/* @__PURE__ */ jsx13("span", { className: "ui-back__label", children: bare ? BARE : name })
|
|
1467
|
+
]
|
|
1468
|
+
}
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1043
1472
|
// src/index.ts
|
|
1044
1473
|
var PAGE_SIZES = KIT_PAGE_SIZES;
|
|
1045
1474
|
var DEFAULT_PAGE_SIZE3 = KIT_DEFAULT_PAGE_SIZE;
|
|
1046
1475
|
export {
|
|
1476
|
+
BackLink,
|
|
1047
1477
|
Badge,
|
|
1048
1478
|
BusyRegion,
|
|
1049
1479
|
Button,
|
|
@@ -1053,6 +1483,7 @@ export {
|
|
|
1053
1483
|
DataTable,
|
|
1054
1484
|
Denied,
|
|
1055
1485
|
Drawer,
|
|
1486
|
+
Dropdown,
|
|
1056
1487
|
Icon,
|
|
1057
1488
|
Modal,
|
|
1058
1489
|
PAGE_SIZES,
|
|
@@ -63,7 +63,8 @@ function ddBody({ items, sections }, listbox, sx) {
|
|
|
63
63
|
if (sections && sections.length) {
|
|
64
64
|
return sections.map((s) => {
|
|
65
65
|
const head = s.label ? `<div class="ui-dropdown__group" role="presentation">${esc(s.label)}</div>` : '';
|
|
66
|
-
const gone = sx &&
|
|
66
|
+
const gone = sx && dropdownFiltering(sx.q)
|
|
67
|
+
&& !(s.items || []).some((it) => ddIsRow(it) && dropdownMatch(it.label, sx.q));
|
|
67
68
|
return `<div class="ui-dropdown__section" role="group"${s.label ? ` aria-label="${esc(s.label)}"` : ''}${gone ? ' hidden' : ''}>${head}${(s.items || []).map(one).join('')}</div>`;
|
|
68
69
|
}).join('');
|
|
69
70
|
}
|
|
@@ -73,15 +74,26 @@ function ddBody({ items, sections }, listbox, sx) {
|
|
|
73
74
|
// ---- Search --------------------------------------------------------------
|
|
74
75
|
// The match is a substring of the label, anywhere in it, ignoring case and
|
|
75
76
|
// accents; rows keep their order. The factory and the wiring both ask
|
|
76
|
-
//
|
|
77
|
+
// dropdownMatch(), so a preset query and a typed one hide the same rows.
|
|
77
78
|
// why: docs/specification.md#a-dropdown-with-a-search-field
|
|
78
79
|
// NFD takes the mark off é or ö; ł, ø, đ and the rest are letters of their own
|
|
79
80
|
// with nothing to take off, so they are mapped by hand.
|
|
80
81
|
const FOLD = { ł: 'l', ø: 'o', đ: 'd', ð: 'd', ß: 'ss', æ: 'ae', œ: 'oe', ı: 'i', þ: 'th' };
|
|
81
82
|
const fold = (s) => String(s == null ? '' : s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase()
|
|
82
83
|
.replace(/[łøđðßæœıþ]/g, (c) => FOLD[c]);
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Is this query narrowing anything? A query of spaces, or of marks with no letter
|
|
87
|
+
* under them, is not — it leaves every row showing, and the separators with them.
|
|
88
|
+
*
|
|
89
|
+
* Public because a second implementation of this dropdown cannot write its own: the
|
|
90
|
+
* React <Dropdown> asks these two, the way <CommandPalette> asks rankGroups(), so a
|
|
91
|
+
* list a server rendered and the same list after a keystroke hide the same rows.
|
|
92
|
+
*/
|
|
93
|
+
export const dropdownFiltering = (query) => fold(query).trim() !== '';
|
|
94
|
+
|
|
95
|
+
/** Does `label` match `query`? A substring, anywhere, ignoring case and accents. */
|
|
96
|
+
export const dropdownMatch = (label, query) => fold(label).includes(fold(query).trim());
|
|
85
97
|
const ddIsRow = (it) => it && it !== '---' && !it.separator;
|
|
86
98
|
|
|
87
99
|
let _ddSeq = 0;
|
|
@@ -100,8 +112,8 @@ function ddSearchContext(search, id) {
|
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
function ddRowExt(it, sx) {
|
|
103
|
-
if (!ddIsRow(it)) return { filtering:
|
|
104
|
-
return { id: `${sx.base}-opt-${sx.n++}`, hidden: !
|
|
115
|
+
if (!ddIsRow(it)) return { filtering: dropdownFiltering(sx.q) };
|
|
116
|
+
return { id: `${sx.base}-opt-${sx.n++}`, hidden: !dropdownMatch(it.label, sx.q) };
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
// The no-match state. A function replacer, so a `$&` typed into the field is
|
|
@@ -118,7 +130,7 @@ function ddSearchBody({ items, sections }, sx, name, scroll) {
|
|
|
118
130
|
const listId = `${sx.base}-list`;
|
|
119
131
|
const rows = ddBody({ items, sections }, true, sx);
|
|
120
132
|
const flat = (sections ? sections.flatMap((s) => s.items || []) : (items || [])).filter(ddIsRow);
|
|
121
|
-
const shown = flat.some((it) =>
|
|
133
|
+
const shown = flat.some((it) => dropdownMatch(it.label, sx.q));
|
|
122
134
|
const cap = scroll && scroll !== true ? ` style="max-height:${typeof scroll === 'number' ? scroll + 'px' : esc(scroll)}"` : '';
|
|
123
135
|
const input = [
|
|
124
136
|
'class="ui-dropdown__search-input"', 'type="text"', 'role="combobox"',
|
|
@@ -131,7 +143,7 @@ function ddSearchBody({ items, sections }, sx, name, scroll) {
|
|
|
131
143
|
+ `<span class="ui-dropdown__search-ic" aria-hidden="true">${icon('search')}</span><input ${input}></div>`
|
|
132
144
|
+ `<div class="ui-dropdown__list" role="listbox" id="${esc(listId)}" aria-label="${esc(name)}"${cap}>${rows}</div>`
|
|
133
145
|
+ `<div class="ui-dropdown__none" role="status" data-dd-none data-dd-empty="${esc(sx.empty)}" data-dd-hint="${esc(sx.hint)}">`
|
|
134
|
-
+ `${shown || !
|
|
146
|
+
+ `${shown || !dropdownFiltering(sx.q) ? '' : ddNone(sx.empty, sx.hint, sx.q)}</div>`;
|
|
135
147
|
}
|
|
136
148
|
|
|
137
149
|
/**
|
|
@@ -144,8 +156,10 @@ function ddSearchBody({ items, sections }, sx, name, scroll) {
|
|
|
144
156
|
* @param {string} [o.variant] 'select' (listbox) | 'menu' (inferred from items)
|
|
145
157
|
* @param {Array} [o.items] [{ label, value?, description?, icon?, badge?, selected?, disabled?, href?, danger? }]
|
|
146
158
|
* @param {Array} [o.sections] [{ label, items }] — grouped alternative to items
|
|
147
|
-
* @param {string} [o.
|
|
148
|
-
*
|
|
159
|
+
* @param {string} [o.foot] content for a pinned block at the panel's bottom edge,
|
|
160
|
+
* drawn as `.ui-dropdown__foot` — raw HTML, so escape your own text
|
|
161
|
+
* @param {string} [o.header] raw HTML pinned to the top of the panel, unwrapped
|
|
162
|
+
* @param {string} [o.footer] raw HTML pinned to the bottom of the panel, unwrapped
|
|
149
163
|
* @param {string} [o.align] 'start' (default) | 'end' — the edge the panel hugs
|
|
150
164
|
* @param {string} [o.direction] 'down' (default) | 'up' | 'auto' — the way it opens
|
|
151
165
|
* @param {boolean} [o.portal] mount the panel on <body>, for a clipping or sticky ancestor
|
|
@@ -158,7 +172,7 @@ function ddSearchBody({ items, sections }, sx, name, scroll) {
|
|
|
158
172
|
*/
|
|
159
173
|
export function dropdown({
|
|
160
174
|
label, value, placeholder = 'Select…', variant, items, sections,
|
|
161
|
-
header = '', footer = '', triggerContent, triggerClass = '', chevron = true,
|
|
175
|
+
foot = '', header = '', footer = '', triggerContent, triggerClass = '', chevron = true,
|
|
162
176
|
align = 'start', direction = 'down', portal = false,
|
|
163
177
|
scroll = false, open = false, ariaLabel, id, panelClass = '', search = false,
|
|
164
178
|
} = {}) {
|
|
@@ -202,6 +216,11 @@ export function dropdown({
|
|
|
202
216
|
scroll && scroll !== true && !sx ? `style="max-height:${typeof scroll === 'number' ? scroll + 'px' : esc(scroll)}"` : '',
|
|
203
217
|
].filter(Boolean).join(' ');
|
|
204
218
|
|
|
219
|
+
// The block the sheet bleeds to the panel's bottom edge. It sits OUTSIDE the
|
|
220
|
+
// unwrapped `footer`, because the block that bleeds is the one that has to
|
|
221
|
+
// touch the edge. why: docs/specification.md#the-dropdown-panel
|
|
222
|
+
const footBlock = foot ? `<div class="ui-dropdown__foot">${foot}</div>` : '';
|
|
223
|
+
|
|
205
224
|
const ddAttrs = 'data-dropdown'
|
|
206
225
|
+ (isSelect ? ' data-dropdown-select' : '')
|
|
207
226
|
+ (direction === 'auto' ? ' data-dropdown-direction="auto"' : '')
|
|
@@ -209,7 +228,9 @@ export function dropdown({
|
|
|
209
228
|
|
|
210
229
|
return `<div class="${cx('ui-dropdown', open && 'open')}" ${ddAttrs}${id ? ` id="${esc(id)}"` : ''}>` +
|
|
211
230
|
`<button ${triggerAttrs}>${trig}${chevron ? '<span class="ui-dropdown__chevron" aria-hidden="true"></span>' : ''}</button>` +
|
|
212
|
-
`<div ${panelAttrs}>${header}
|
|
231
|
+
`<div ${panelAttrs}>${header}`
|
|
232
|
+
+ `${sx ? ddSearchBody({ items, sections }, sx, name, scroll) : ddBody({ items, sections }, isSelect)}`
|
|
233
|
+
+ `${footer}${footBlock}</div>` +
|
|
213
234
|
`</div>`;
|
|
214
235
|
}
|
|
215
236
|
|
|
@@ -294,14 +315,14 @@ function ddFilter(dd) {
|
|
|
294
315
|
const q = search.value;
|
|
295
316
|
let shown = 0;
|
|
296
317
|
panel.querySelectorAll('[data-dd-item]').forEach((row) => {
|
|
297
|
-
row.hidden = !
|
|
318
|
+
row.hidden = !dropdownMatch((row.querySelector('.ui-dropdown__label') || row).textContent, q);
|
|
298
319
|
if (!row.hidden) shown += 1;
|
|
299
320
|
});
|
|
300
|
-
panel.querySelectorAll('.ui-dropdown__sep').forEach((el) => { el.hidden =
|
|
321
|
+
panel.querySelectorAll('.ui-dropdown__sep').forEach((el) => { el.hidden = dropdownFiltering(q); });
|
|
301
322
|
panel.querySelectorAll('.ui-dropdown__section').forEach((el) => { el.hidden = !el.querySelector('[data-dd-item]:not([hidden])'); });
|
|
302
323
|
const none = panel.querySelector('[data-dd-none]');
|
|
303
324
|
if (none) {
|
|
304
|
-
none.innerHTML = shown || !
|
|
325
|
+
none.innerHTML = shown || !dropdownFiltering(q) ? ''
|
|
305
326
|
: ddNone(none.getAttribute('data-dd-empty') || '', none.getAttribute('data-dd-hint') || '', q);
|
|
306
327
|
}
|
|
307
328
|
ddSetActive(dd, ddItemsOf(dd)[0] || null);
|