@crisp-ui-kit/crisp 0.36.0 → 0.37.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/contextmenu/contextmenu.js +25 -8
- package/dist/cjs/components/contextmenu/contextmenu.recipe.js +5 -2
- package/dist/cjs/components/dialog/dialog.js +1 -1
- package/dist/cjs/components/dialog/dialog.recipe.js +1 -1
- package/dist/cjs/components/menu/menu.js +20 -3
- package/dist/cjs/components/menu/menu.recipe.js +4 -3
- package/dist/cjs/components/multiselect/multiselect.js +26 -9
- package/dist/cjs/components/multiselect/multiselect.recipe.js +4 -3
- package/dist/cjs/components/popover/popover.js +25 -8
- package/dist/cjs/components/popover/popover.recipe.js +5 -3
- package/dist/cjs/components/select/select.js +30 -9
- package/dist/cjs/components/select/select.recipe.js +5 -3
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +50 -6
- package/dist/cjs/core/floating-portal.js +28 -0
- package/dist/cjs/core/floating-stack.js +54 -0
- package/dist/cjs/core/index.js +6 -1
- package/dist/cjs/product/recordstable/recordstable.js +15 -4
- package/dist/contextmenu.css +1 -1
- package/dist/contextmenu.layered.css +1 -1
- package/dist/dialog.css +1 -1
- package/dist/dialog.layered.css +1 -1
- package/dist/esm/components/contextmenu/contextmenu.js +25 -8
- package/dist/esm/components/contextmenu/contextmenu.recipe.js +5 -2
- package/dist/esm/components/dialog/dialog.js +1 -1
- package/dist/esm/components/dialog/dialog.recipe.js +1 -1
- package/dist/esm/components/menu/menu.js +21 -4
- package/dist/esm/components/menu/menu.recipe.js +4 -3
- package/dist/esm/components/multiselect/multiselect.js +27 -10
- package/dist/esm/components/multiselect/multiselect.recipe.js +4 -3
- package/dist/esm/components/popover/popover.js +26 -9
- package/dist/esm/components/popover/popover.recipe.js +5 -3
- package/dist/esm/components/select/select.js +31 -10
- package/dist/esm/components/select/select.recipe.js +5 -3
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +51 -7
- package/dist/esm/core/floating-portal.js +25 -0
- package/dist/esm/core/floating-stack.js +50 -0
- package/dist/esm/core/index.js +2 -0
- package/dist/esm/product/recordstable/recordstable.js +16 -5
- package/dist/menu.css +1 -1
- package/dist/menu.layered.css +1 -1
- package/dist/multiselect.css +1 -1
- package/dist/multiselect.layered.css +1 -1
- package/dist/popover.css +1 -1
- package/dist/popover.layered.css +1 -1
- package/dist/select.css +1 -1
- package/dist/select.layered.css +1 -1
- package/dist/styles.css +6 -6
- package/dist/styles.layered.css +6 -6
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +10 -1
- package/dist/types/core/floating-portal.d.ts +25 -0
- package/dist/types/core/floating-stack.d.ts +13 -0
- package/dist/types/core/index.d.ts +2 -0
- package/package.json +1 -1
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.ContextMenu = ContextMenu;
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const React = __importStar(require("react"));
|
|
39
|
+
const core_1 = require("../../core");
|
|
39
40
|
const menu_1 = require("../menu");
|
|
40
41
|
const contextmenu_recipe_1 = require("./contextmenu.recipe");
|
|
41
42
|
/**
|
|
@@ -77,12 +78,28 @@ function ContextMenu({ children, items, menuLabel = "Context menu", className, }
|
|
|
77
78
|
dismiss: () => close(),
|
|
78
79
|
contentRef: menuRef,
|
|
79
80
|
});
|
|
81
|
+
// Registers the (now-portaled) menu on the shared floating-layer stack
|
|
82
|
+
// (#339) so this doesn't get closed out from under itself when it holds
|
|
83
|
+
// another floating layer -- see core/floating-stack.ts.
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
if (!pos || !menuRef.current)
|
|
86
|
+
return;
|
|
87
|
+
return (0, core_1.registerFloatingLayer)(menuRef.current);
|
|
88
|
+
}, [pos]);
|
|
80
89
|
React.useEffect(() => {
|
|
81
90
|
if (!pos)
|
|
82
91
|
return;
|
|
83
92
|
const onDown = (e) => {
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
// menuRef too (#339): the menu content portals to document.body, so
|
|
94
|
+
// it's no longer a descendant of rootRef.
|
|
95
|
+
const target = e.target;
|
|
96
|
+
if (rootRef.current?.contains(target))
|
|
97
|
+
return;
|
|
98
|
+
if (menuRef.current?.contains(target))
|
|
99
|
+
return;
|
|
100
|
+
if ((0, core_1.isInsideLaterFloatingLayer)(target, menuRef.current))
|
|
101
|
+
return;
|
|
102
|
+
close();
|
|
86
103
|
};
|
|
87
104
|
const onKey = (e) => e.key === "Escape" && close(true);
|
|
88
105
|
document.addEventListener("pointerdown", onDown);
|
|
@@ -92,11 +109,11 @@ function ContextMenu({ children, items, menuLabel = "Context menu", className, }
|
|
|
92
109
|
document.removeEventListener("keydown", onKey);
|
|
93
110
|
};
|
|
94
111
|
}, [pos, close]);
|
|
95
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, contextmenu_recipe_1.contextmenuRecipe)({ className }), children: [(0, jsx_runtime_1.jsx)("div", { onContextMenu: open, children: children }), pos && ((0, jsx_runtime_1.jsx)("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-contextmenu-sep" }, i)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, contextmenu_recipe_1.contextmenuRecipe)({ className }), children: [(0, jsx_runtime_1.jsx)("div", { onContextMenu: open, children: children }), pos && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { children: (0, jsx_runtime_1.jsx)("div", { ref: menuRef, className: "crisp-contextmenu-content", role: "menu", "aria-label": menuLabel, style: { left: pos.x, top: pos.y }, onKeyDown: onMenuKeyDown, children: items.map((item, i) => item.separator ? ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-contextmenu-sep" }, i)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", disabled: item.disabled, className: item.danger
|
|
113
|
+
? "crisp-contextmenu-item crisp-contextmenu-item--danger"
|
|
114
|
+
: "crisp-contextmenu-item", tabIndex: -1, onClick: () => {
|
|
115
|
+
item.onSelect?.();
|
|
116
|
+
close(true);
|
|
117
|
+
}, children: [item.icon, item.label, item.shortcut && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-contextmenu-shortcut", children: item.shortcut }))] }, i))) }) }))] }));
|
|
101
118
|
}
|
|
102
119
|
ContextMenu.displayName = "ContextMenu";
|
|
@@ -3,11 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.contextmenuRecipe = void 0;
|
|
4
4
|
const core_1 = require("../../core");
|
|
5
5
|
const tokens_1 = require("../../tokens");
|
|
6
|
-
// A right-click menu on the measured popover surface, positioned at the
|
|
6
|
+
// A right-click menu on the measured popover surface, positioned at the
|
|
7
|
+
// cursor. The content portals to document.body (#339) so no ancestor's
|
|
8
|
+
// transform/backdrop-filter can capture its position: fixed box; z-index 70
|
|
9
|
+
// keeps it above Dialog/Drawer's 60/61.
|
|
7
10
|
const config = (0, core_1.defineRecipe)({
|
|
8
11
|
name: "contextmenu",
|
|
9
12
|
base: `display: contents; font-family: ${tokens_1.contract.font.sans};
|
|
10
|
-
& .crisp-contextmenu-content { position: fixed; z-index:
|
|
13
|
+
& .crisp-contextmenu-content { position: fixed; z-index: 70; min-width: 200px; box-sizing: border-box; padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; box-shadow: ${tokens_1.elevation.floating}; opacity: 1; transform: none; transition: opacity .08s ease, transform .08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
|
|
11
14
|
& .crisp-contextmenu-item { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; width: 100%; height: ${tokens_1.contract.size.menuRow}; box-sizing: border-box; padding: 0 ${tokens_1.contract.space["2"]}; margin: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: none; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; text-align: start; white-space: nowrap; cursor: pointer; }
|
|
12
15
|
& .crisp-contextmenu-item svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
|
|
13
16
|
& .crisp-contextmenu-item:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); }
|
|
@@ -79,7 +79,7 @@ function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ar
|
|
|
79
79
|
return ((0, jsx_runtime_1.jsx)(react_1.Portal, { children: (0, jsx_runtime_1.jsxs)("div", { className: (0, dialog_recipe_1.dialogRecipe)(), children: [(0, jsx_runtime_1.jsx)("div", { ...api.getBackdropProps(), className: "crisp-dialog-backdrop" }), (0, jsx_runtime_1.jsx)("div", { ...api.getPositionerProps(), className: "crisp-dialog-positioner", children: (0, jsx_runtime_1.jsx)("div", { ...rest, ...(0, core_1.withDescribedBy)(api.getContentProps(), describedBy), className: (0, core_1.cx)("crisp-dialog-content", className), "data-crisp-dialog": id,
|
|
80
80
|
// `width` is the component's own knob, so it wins over a passed
|
|
81
81
|
// maxWidth; everything else a consumer sets survives
|
|
82
|
-
style: { ...style, maxWidth: `min(${width}px, 90vw)` }, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-dialog-card", children: [title != null && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-dialog-header", children: [icon, (0, jsx_runtime_1.jsx)("
|
|
82
|
+
style: { ...style, maxWidth: `min(${width}px, 90vw)` }, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-dialog-card", children: [title != null && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-dialog-header", children: [icon, (0, jsx_runtime_1.jsx)("h2", { ...api.getTitleProps(), className: "crisp-dialog-title", children: title }), (0, jsx_runtime_1.jsx)("button", { type: "button", ...api.getCloseTriggerProps(), className: "crisp-dialog-close", "aria-label": closeLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 16, strokeWidth: 2, "aria-hidden": "true" }) })] })), children] }) }) })] }) }));
|
|
83
83
|
}
|
|
84
84
|
Dialog.displayName = "Dialog";
|
|
85
85
|
function DialogBody({ children, className, }) {
|
|
@@ -17,7 +17,7 @@ const config = (0, core_1.defineRecipe)({
|
|
|
17
17
|
& .crisp-dialog-card { display: flex; flex-direction: column; flex: 1; min-height: 0; box-sizing: border-box; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgSurface}; box-shadow: 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); overflow: hidden; }
|
|
18
18
|
& .crisp-dialog-header { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; flex-shrink: 0; height: 52px; padding: 0 ${tokens_1.contract.space["2"]} 0 ${tokens_1.contract.space["4"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; }
|
|
19
19
|
& .crisp-dialog-header svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
|
|
20
|
-
& .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; }
|
|
20
|
+
& .crisp-dialog-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; margin: 0; }
|
|
21
21
|
& .crisp-dialog-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; background: none; color: ${tokens_1.contract.fgMuted}; font-family: inherit; cursor: pointer; }
|
|
22
22
|
& .crisp-dialog-close:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); color: ${tokens_1.contract.fgPrimary}; }
|
|
23
23
|
& .crisp-dialog-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: ${tokens_1.contract.space["4"]}; padding: ${tokens_1.contract.space["5"]} ${tokens_1.contract.space["4"]}; }
|
|
@@ -98,13 +98,30 @@ function Menu({ trigger, children, align = "start", open: openProp, defaultOpen,
|
|
|
98
98
|
setOpen(false);
|
|
99
99
|
triggerRef.current?.focus?.();
|
|
100
100
|
}, [setOpen]);
|
|
101
|
+
// Registers the (now-portaled) content on the shared floating-layer stack
|
|
102
|
+
// (#339) so this doesn't get closed out from under itself when it holds
|
|
103
|
+
// another floating layer (a Select in a sort popover, e.g.) -- see
|
|
104
|
+
// core/floating-stack.ts.
|
|
105
|
+
React.useEffect(() => {
|
|
106
|
+
if (!open || !contentRef.current)
|
|
107
|
+
return;
|
|
108
|
+
return (0, core_1.registerFloatingLayer)(contentRef.current);
|
|
109
|
+
}, [open]);
|
|
101
110
|
// close on outside pointer-down
|
|
102
111
|
React.useEffect(() => {
|
|
103
112
|
if (!open)
|
|
104
113
|
return;
|
|
105
114
|
const onDown = (e) => {
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
// contentRef too (#339): the menu content portals to document.body, so
|
|
116
|
+
// it's no longer a descendant of rootRef.
|
|
117
|
+
const target = e.target;
|
|
118
|
+
if (rootRef.current?.contains(target))
|
|
119
|
+
return;
|
|
120
|
+
if (contentRef.current?.contains(target))
|
|
121
|
+
return;
|
|
122
|
+
if ((0, core_1.isInsideLaterFloatingLayer)(target, contentRef.current))
|
|
123
|
+
return;
|
|
124
|
+
setOpen(false);
|
|
108
125
|
};
|
|
109
126
|
document.addEventListener("pointerdown", onDown);
|
|
110
127
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
@@ -128,7 +145,7 @@ function Menu({ trigger, children, align = "start", open: openProp, defaultOpen,
|
|
|
128
145
|
setOpen(!open);
|
|
129
146
|
},
|
|
130
147
|
});
|
|
131
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, menu_recipe_1.menuRecipe)({ className }), children: [triggerEl, open && ((0, jsx_runtime_1.jsx)(MenuContext.Provider, { value: { close }, children: (0, jsx_runtime_1.jsx)("div", { ref: contentRef, className: "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: children }) }))] }));
|
|
148
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, menu_recipe_1.menuRecipe)({ className }), children: [triggerEl, open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { children: (0, jsx_runtime_1.jsx)(MenuContext.Provider, { value: { close }, children: (0, jsx_runtime_1.jsx)("div", { ref: contentRef, className: "crisp-menu-content", role: "menu", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: onContentKeyDown, children: children }) }) }))] }));
|
|
132
149
|
}
|
|
133
150
|
Menu.displayName = "Menu";
|
|
134
151
|
function MenuItem({ children, icon, shortcut, danger, disabled, onSelect, closeOnSelect = true, }) {
|
|
@@ -6,8 +6,9 @@ const tokens_1 = require("../../tokens");
|
|
|
6
6
|
// A dropdown menu — same popover surface used for ⋯ / sort / filter
|
|
7
7
|
// menus (measured from listboxes): radius 12, 4px padding, a layered
|
|
8
8
|
// soft shadow, over 32px items (radius 8, icon + label + optional shortcut /
|
|
9
|
-
// check). The content
|
|
10
|
-
//
|
|
9
|
+
// check). The content portals to document.body (#339) so no ancestor's
|
|
10
|
+
// transform/backdrop-filter can capture its position: fixed box; z-index 70
|
|
11
|
+
// keeps it above Dialog/Drawer's 60/61.
|
|
11
12
|
const config = (0, core_1.defineRecipe)({
|
|
12
13
|
name: "menu",
|
|
13
14
|
base: `
|
|
@@ -15,7 +16,7 @@ const config = (0, core_1.defineRecipe)({
|
|
|
15
16
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
16
17
|
|
|
17
18
|
& .crisp-menu-content {
|
|
18
|
-
position: fixed; z-index:
|
|
19
|
+
position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
|
|
19
20
|
min-width: 200px; max-height: 360px; overflow-y: auto; padding: ${tokens_1.contract.space["1"]};
|
|
20
21
|
border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover};
|
|
21
22
|
box-shadow: ${tokens_1.elevation.floating};
|
|
@@ -82,6 +82,9 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
82
82
|
const [query, setQuery] = React.useState("");
|
|
83
83
|
const [highlight, setHighlight] = React.useState(0);
|
|
84
84
|
const rootRef = React.useRef(null);
|
|
85
|
+
// The listbox portals to document.body (#339), so it's no longer a
|
|
86
|
+
// descendant of rootRef -- outside-click detection needs this ref too.
|
|
87
|
+
const listboxRef = React.useRef(null);
|
|
85
88
|
// `form.reset()` restores native controls; a button + listbox holds its value
|
|
86
89
|
// in React, so it has to listen (#245). Controlled multiselects are left
|
|
87
90
|
// alone.
|
|
@@ -155,12 +158,26 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
155
158
|
// Tab is already moving focus out; `close` would drag it back to the trigger
|
|
156
159
|
dismiss: () => setOpen(false),
|
|
157
160
|
});
|
|
161
|
+
// Registers the (now-portaled) listbox on the shared floating-layer stack
|
|
162
|
+
// (#339) so this doesn't get closed out from under itself when nested
|
|
163
|
+
// inside another floating layer -- see Select's matching comment.
|
|
164
|
+
React.useEffect(() => {
|
|
165
|
+
if (!open || !listboxRef.current)
|
|
166
|
+
return;
|
|
167
|
+
return (0, core_1.registerFloatingLayer)(listboxRef.current);
|
|
168
|
+
}, [open]);
|
|
158
169
|
React.useEffect(() => {
|
|
159
170
|
if (!open)
|
|
160
171
|
return;
|
|
161
172
|
const onDown = (e) => {
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
const target = e.target;
|
|
174
|
+
if (rootRef.current?.contains(target))
|
|
175
|
+
return;
|
|
176
|
+
if (listboxRef.current?.contains(target))
|
|
177
|
+
return;
|
|
178
|
+
if ((0, core_1.isInsideLaterFloatingLayer)(target, listboxRef.current))
|
|
179
|
+
return;
|
|
180
|
+
setOpen(false);
|
|
164
181
|
};
|
|
165
182
|
document.addEventListener("pointerdown", onDown);
|
|
166
183
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
@@ -173,12 +190,12 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
|
|
|
173
190
|
e.preventDefault();
|
|
174
191
|
openMenu();
|
|
175
192
|
}
|
|
176
|
-
}, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => (0, jsx_runtime_1.jsx)(Chip, { opt: o }, o.value))) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { id: listboxId, className: "crisp-multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-multiselect-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
193
|
+
}, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => (0, jsx_runtime_1.jsx)(Chip, { opt: o }, o.value))) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-multiselect-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
194
|
+
setQuery(e.target.value);
|
|
195
|
+
setHighlight(0);
|
|
196
|
+
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
197
|
+
const sel = selectedSet.has(opt.value);
|
|
198
|
+
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": sel, tabIndex: -1, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-multiselect-option", onClick: () => toggle(opt.value), onMouseEnter: () => setHighlight(i), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-option-chip", children: (0, jsx_runtime_1.jsx)(Chip, { opt: opt }) }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-check", children: sel && (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 12, strokeWidth: 3 }) })] }, opt.value));
|
|
199
|
+
}))] }) }))] }));
|
|
183
200
|
}
|
|
184
201
|
MultiSelect.displayName = "MultiSelect";
|
|
@@ -6,8 +6,9 @@ const tokens_1 = require("../../tokens");
|
|
|
6
6
|
// A multi-select / tag editor — measured from a cell tag picker: an
|
|
7
7
|
// outlined trigger showing the selected values as coloured chips, opening a
|
|
8
8
|
// popover with a search field over rows of "[coloured chip] … [checkbox]" that
|
|
9
|
-
// toggle on click. The popover
|
|
10
|
-
//
|
|
9
|
+
// toggle on click. The popover portals to document.body (#339) so no
|
|
10
|
+
// ancestor's transform/backdrop-filter can capture its position: fixed box;
|
|
11
|
+
// z-index 70 keeps it above Dialog/Drawer's 60/61.
|
|
11
12
|
const config = (0, core_1.defineRecipe)({
|
|
12
13
|
name: "multiselect",
|
|
13
14
|
base: `
|
|
@@ -30,7 +31,7 @@ const config = (0, core_1.defineRecipe)({
|
|
|
30
31
|
& .crisp-multiselect-trigger[data-open] .crisp-multiselect-chevron { transform: rotate(180deg); }
|
|
31
32
|
|
|
32
33
|
& .crisp-multiselect-listbox {
|
|
33
|
-
position: fixed; z-index:
|
|
34
|
+
position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
|
|
34
35
|
max-height: 300px; overflow-y: auto; padding: ${tokens_1.contract.space["1"]};
|
|
35
36
|
border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover};
|
|
36
37
|
box-shadow: ${tokens_1.elevation.floating};
|
|
@@ -55,6 +55,9 @@ function Popover({ trigger, children, align = "start", open: openProp, defaultOp
|
|
|
55
55
|
const open = openProp ?? internalOpen;
|
|
56
56
|
const rootRef = React.useRef(null);
|
|
57
57
|
const triggerRef = React.useRef(null);
|
|
58
|
+
// The content portals to document.body (#339), so it's no longer a
|
|
59
|
+
// descendant of rootRef -- outside-click detection needs this ref too.
|
|
60
|
+
const contentRef = React.useRef(null);
|
|
58
61
|
const [pos, setPos] = React.useState({});
|
|
59
62
|
const [up, setUp] = React.useState(false);
|
|
60
63
|
const setOpen = React.useCallback((next) => {
|
|
@@ -82,12 +85,26 @@ function Popover({ trigger, children, align = "start", open: openProp, defaultOp
|
|
|
82
85
|
if (open)
|
|
83
86
|
place();
|
|
84
87
|
}, [open]);
|
|
88
|
+
// Registers the (now-portaled) content on the shared floating-layer stack
|
|
89
|
+
// (#339) so this doesn't get closed out from under itself when it holds
|
|
90
|
+
// another floating layer -- see core/floating-stack.ts.
|
|
91
|
+
React.useEffect(() => {
|
|
92
|
+
if (!open || !contentRef.current)
|
|
93
|
+
return;
|
|
94
|
+
return (0, core_1.registerFloatingLayer)(contentRef.current);
|
|
95
|
+
}, [open]);
|
|
85
96
|
React.useEffect(() => {
|
|
86
97
|
if (!open)
|
|
87
98
|
return;
|
|
88
99
|
const onDown = (e) => {
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
const target = e.target;
|
|
101
|
+
if (rootRef.current?.contains(target))
|
|
102
|
+
return;
|
|
103
|
+
if (contentRef.current?.contains(target))
|
|
104
|
+
return;
|
|
105
|
+
if ((0, core_1.isInsideLaterFloatingLayer)(target, contentRef.current))
|
|
106
|
+
return;
|
|
107
|
+
setOpen(false);
|
|
91
108
|
};
|
|
92
109
|
document.addEventListener("pointerdown", onDown);
|
|
93
110
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
@@ -105,11 +122,11 @@ function Popover({ trigger, children, align = "start", open: openProp, defaultOp
|
|
|
105
122
|
setOpen(!open);
|
|
106
123
|
},
|
|
107
124
|
});
|
|
108
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, popover_recipe_1.popoverRecipe)({ className }), children: [triggerEl, open && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-popover-content", role: "dialog", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: (e) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, popover_recipe_1.popoverRecipe)({ className }), children: [triggerEl, open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { children: (0, jsx_runtime_1.jsx)("div", { ref: contentRef, className: "crisp-popover-content", role: "dialog", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-align": align, "data-up": up ? "" : undefined, style: pos, onKeyDown: (e) => {
|
|
126
|
+
if (e.key === "Escape") {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
setOpen(false);
|
|
129
|
+
}
|
|
130
|
+
}, children: children }) }))] }));
|
|
114
131
|
}
|
|
115
132
|
Popover.displayName = "Popover";
|
|
@@ -5,12 +5,14 @@ const core_1 = require("../../core");
|
|
|
5
5
|
const tokens_1 = require("../../tokens");
|
|
6
6
|
// A generic anchored popover — the same measured surface as Menu / Select
|
|
7
7
|
// (radius 12, 4px padding, layered soft shadow, inset border), but holding
|
|
8
|
-
// arbitrary content.
|
|
9
|
-
//
|
|
8
|
+
// arbitrary content. The content portals to document.body (#339) so no
|
|
9
|
+
// ancestor's transform/backdrop-filter can capture its position: fixed box;
|
|
10
|
+
// z-index 70 keeps it above Dialog/Drawer's 60/61. The display:contents root
|
|
11
|
+
// is what's left in place, so a consumer's own styling of it survives.
|
|
10
12
|
const config = (0, core_1.defineRecipe)({
|
|
11
13
|
name: "popover",
|
|
12
14
|
base: `display: contents; font-family: ${tokens_1.contract.font.sans};
|
|
13
|
-
& .crisp-popover-content { position: fixed; z-index:
|
|
15
|
+
& .crisp-popover-content { position: fixed; z-index: 70; box-sizing: border-box; min-width: 200px; max-width: min(360px, 92vw); padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; color: ${tokens_1.contract.fgPrimary}; box-shadow: ${tokens_1.elevation.floating}; opacity: 1; transform: none; transition: opacity 0.08s ease, transform 0.08s ease; transform-origin: top left; @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
|
|
14
16
|
& .crisp-popover-content[data-align="end"] { transform-origin: top right; }
|
|
15
17
|
& .crisp-popover-content[data-up] { transform-origin: bottom left; }`,
|
|
16
18
|
variants: {},
|
|
@@ -69,6 +69,10 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
69
69
|
const [highlight, setHighlight] = React.useState(0);
|
|
70
70
|
const [query, setQuery] = React.useState("");
|
|
71
71
|
const rootRef = React.useRef(null);
|
|
72
|
+
// The listbox portals to document.body (#339), so it's no longer a
|
|
73
|
+
// descendant of rootRef -- outside-click detection needs this ref too, or
|
|
74
|
+
// every click inside the listbox reads as "outside" and closes it.
|
|
75
|
+
const listboxRef = React.useRef(null);
|
|
72
76
|
// `form.reset()` restores native controls; a button + listbox holds its value
|
|
73
77
|
// in React, so it has to listen (#245). Controlled selects are left alone.
|
|
74
78
|
(0, core_1.useFormReset)(rootRef, () => {
|
|
@@ -146,12 +150,29 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
146
150
|
if (!searchable)
|
|
147
151
|
onNavKeys(e);
|
|
148
152
|
};
|
|
153
|
+
// Registers the (now-portaled) listbox on the shared floating-layer stack
|
|
154
|
+
// (#339) so a Select nested inside another floating layer (a Menu's sort
|
|
155
|
+
// popover, e.g.) doesn't get closed out from under itself: once both
|
|
156
|
+
// portal independently to document.body they're DOM siblings, not
|
|
157
|
+
// ancestor/descendant, so the outer layer's own outside-click check can no
|
|
158
|
+
// longer see "inside me" by DOM containment alone.
|
|
159
|
+
React.useEffect(() => {
|
|
160
|
+
if (!open || !listboxRef.current)
|
|
161
|
+
return;
|
|
162
|
+
return (0, core_1.registerFloatingLayer)(listboxRef.current);
|
|
163
|
+
}, [open]);
|
|
149
164
|
React.useEffect(() => {
|
|
150
165
|
if (!open)
|
|
151
166
|
return;
|
|
152
167
|
const onDown = (e) => {
|
|
153
|
-
|
|
154
|
-
|
|
168
|
+
const target = e.target;
|
|
169
|
+
if (rootRef.current?.contains(target))
|
|
170
|
+
return;
|
|
171
|
+
if (listboxRef.current?.contains(target))
|
|
172
|
+
return;
|
|
173
|
+
if ((0, core_1.isInsideLaterFloatingLayer)(target, listboxRef.current))
|
|
174
|
+
return;
|
|
175
|
+
setOpen(false);
|
|
155
176
|
};
|
|
156
177
|
document.addEventListener("pointerdown", onDown);
|
|
157
178
|
return () => document.removeEventListener("pointerdown", onDown);
|
|
@@ -159,12 +180,12 @@ function Select({ options, value, defaultValue, onValueChange, name, required, p
|
|
|
159
180
|
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef, className: (0, select_recipe_1.selectRecipe)({ variant, width, className }), ...rest, children: [name && (selected || required) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-formvalue", style: core_1.formValueStyle, name: name, value: selected ?? "", required: required, tabIndex: -1, "aria-hidden": "true",
|
|
160
181
|
// controlled by the listbox above; `readOnly` would bar it from
|
|
161
182
|
// validation, which is the only reason this input is not hidden
|
|
162
|
-
onChange: () => { } })), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsxs)("div", { id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
183
|
+
onChange: () => { } })), (0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", id: id, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listboxId : undefined, "aria-activedescendant": searchable ? undefined : activeId, "aria-label": ariaLabel, disabled: disabled, "data-open": open ? "" : undefined, className: "crisp-select-trigger", onClick: () => (open ? setOpen(false) : openMenu()), onKeyDown: onTriggerKeyDown, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-select-value", children: selectedOption ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [selectedOption.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: selectedOption.color } })), selectedOption.label] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-placeholder", children: placeholder })) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-select-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-select-listbox", role: "listbox", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-select-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
|
|
184
|
+
setQuery(e.target.value);
|
|
185
|
+
setHighlight(0);
|
|
186
|
+
}, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-select-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
|
|
187
|
+
const isSel = opt.value === selected;
|
|
188
|
+
return ((0, jsx_runtime_1.jsxs)("button", { type: "button", id: optionId(i), role: "option", "aria-selected": isSel, tabIndex: -1, disabled: opt.disabled, "data-disabled": opt.disabled ? "" : undefined, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-select-option", onClick: () => pick(opt), onMouseEnter: () => setHighlight(i), children: [opt.color && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-dot", style: { background: opt.color } })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-select-option-label", children: opt.label }), isSel && ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-select-option-check", size: 15, strokeWidth: 2 }))] }, opt.value));
|
|
189
|
+
}))] }) }))] }));
|
|
169
190
|
}
|
|
170
191
|
Select.displayName = "Select";
|
|
@@ -7,8 +7,10 @@ const tokens_1 = require("../../tokens");
|
|
|
7
7
|
// (radius 9, value + a 16px chevron) over a listbox — radius 12, 4px padding, a
|
|
8
8
|
// layered soft shadow — of 32px options (radius 8, 6/8 padding) that show a
|
|
9
9
|
// check on the selected one and highlight on hover / keyboard. The listbox is
|
|
10
|
-
// portaled
|
|
11
|
-
//
|
|
10
|
+
// portaled to document.body (#339) so no ancestor's transform/backdrop-filter
|
|
11
|
+
// (Dialog's own measured surface sets both) can capture its position: fixed
|
|
12
|
+
// box; z-index 70 keeps it above Dialog/Drawer's z-index 60/61 layers, since
|
|
13
|
+
// it's no longer nested inside whichever one opened it.
|
|
12
14
|
const config = (0, core_1.defineRecipe)({
|
|
13
15
|
name: "select",
|
|
14
16
|
base: `
|
|
@@ -31,7 +33,7 @@ const config = (0, core_1.defineRecipe)({
|
|
|
31
33
|
& .crisp-select-trigger[data-open] .crisp-select-chevron { transform: rotate(180deg); }
|
|
32
34
|
|
|
33
35
|
& .crisp-select-listbox {
|
|
34
|
-
position: fixed; z-index:
|
|
36
|
+
position: fixed; z-index: 70; box-sizing: border-box; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px};
|
|
35
37
|
max-height: 280px; overflow-y: auto; padding: ${tokens_1.contract.space["1"]};
|
|
36
38
|
border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover};
|
|
37
39
|
box-shadow: ${tokens_1.elevation.floating};
|
|
@@ -40,12 +40,14 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
40
40
|
const lucide_react_1 = require("lucide-react");
|
|
41
41
|
const React = __importStar(require("react"));
|
|
42
42
|
const core_1 = require("../../core");
|
|
43
|
+
const menu_1 = require("../menu");
|
|
43
44
|
const select_1 = require("../select");
|
|
44
45
|
const tabletoolbar_recipe_1 = require("./tabletoolbar.recipe");
|
|
45
46
|
const OP_LABELS = {
|
|
46
47
|
contains: "contains",
|
|
47
48
|
is: "is",
|
|
48
49
|
isnot: "is not",
|
|
50
|
+
isanyof: "is any of",
|
|
49
51
|
empty: "is empty",
|
|
50
52
|
notempty: "is not empty",
|
|
51
53
|
};
|
|
@@ -56,7 +58,16 @@ const OP_ORDER = [
|
|
|
56
58
|
"empty",
|
|
57
59
|
"notempty",
|
|
58
60
|
];
|
|
61
|
+
// "isanyof" is appended only for a column that supplies `values` — there is
|
|
62
|
+
// nothing to check off otherwise.
|
|
63
|
+
const opsFor = (col) => col?.values?.length ? [...OP_ORDER, "isanyof"] : OP_ORDER;
|
|
59
64
|
const opNeedsValue = (op) => op === "contains" || op === "is" || op === "isnot";
|
|
65
|
+
const opNeedsValues = (op) => op === "isanyof";
|
|
66
|
+
// "Department is any of IT, Finance" past two values becomes "… +N": the
|
|
67
|
+
// chip is one line, and a field can have far more than two facet values.
|
|
68
|
+
const isAnyOfSummary = (values) => values.length <= 2
|
|
69
|
+
? values.join(", ")
|
|
70
|
+
: `${values.slice(0, 2).join(", ")} +${values.length - 2}`;
|
|
60
71
|
const isEmptyValue = (v) => v == null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
61
72
|
const asText = (v) => {
|
|
62
73
|
if (isEmptyValue(v))
|
|
@@ -82,7 +93,8 @@ const asText = (v) => {
|
|
|
82
93
|
* display string, so a grouped-thousands "$1,200" still matches "1200".
|
|
83
94
|
*/
|
|
84
95
|
function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
85
|
-
const active = filters.filter((f) => !opNeedsValue(f.op) || f.value.trim() !== "")
|
|
96
|
+
const active = filters.filter((f) => (!opNeedsValue(f.op) || f.value.trim() !== "") &&
|
|
97
|
+
(!opNeedsValues(f.op) || (f.values?.length ?? 0) > 0));
|
|
86
98
|
if (!active.length)
|
|
87
99
|
return rows;
|
|
88
100
|
return rows.filter((row) => active.every((f) => {
|
|
@@ -92,6 +104,8 @@ function applyFilters(rows, filters, get = (row, key) => row[key]) {
|
|
|
92
104
|
if (f.op === "notempty")
|
|
93
105
|
return !isEmptyValue(raw);
|
|
94
106
|
const t = asText(raw).toLowerCase();
|
|
107
|
+
if (f.op === "isanyof")
|
|
108
|
+
return (f.values ?? []).some((v) => v.toLowerCase() === t);
|
|
95
109
|
const v = f.value.trim().toLowerCase();
|
|
96
110
|
if (f.op === "contains")
|
|
97
111
|
return t.includes(v);
|
|
@@ -153,6 +167,8 @@ function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange,
|
|
|
153
167
|
const [openMenu, setOpenMenu] = React.useState(null);
|
|
154
168
|
const [dragSort, setDragSort] = React.useState(null);
|
|
155
169
|
const [filterQuery, setFilterQuery] = React.useState("");
|
|
170
|
+
// filters the "isanyof" checkbox list, same lifetime as filterQuery above
|
|
171
|
+
const [anyOfQuery, setAnyOfQuery] = React.useState("");
|
|
156
172
|
// close any open menu on outside click (anything not inside a menu or its
|
|
157
173
|
// trigger) or Escape
|
|
158
174
|
React.useEffect(() => {
|
|
@@ -160,8 +176,15 @@ function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange,
|
|
|
160
176
|
return;
|
|
161
177
|
const onDown = (e) => {
|
|
162
178
|
const t = e.target;
|
|
163
|
-
if (
|
|
164
|
-
|
|
179
|
+
if (t.closest(".crisp-tabletoolbar-menu, [data-menu-trigger]"))
|
|
180
|
+
return;
|
|
181
|
+
// #339: a Select opened from inside this popover (the sort row's
|
|
182
|
+
// direction control) now portals to document.body, so it's no longer
|
|
183
|
+
// inside .crisp-tabletoolbar-menu by DOM containment -- check the
|
|
184
|
+
// shared floating-layer stack too.
|
|
185
|
+
if ((0, core_1.isInsideLaterFloatingLayer)(t, null))
|
|
186
|
+
return;
|
|
187
|
+
setOpenMenu(null);
|
|
165
188
|
};
|
|
166
189
|
const onKey = (e) => e.key === "Escape" && setOpenMenu(null);
|
|
167
190
|
document.addEventListener("mousedown", onDown);
|
|
@@ -251,9 +274,30 @@ function TableToolbar({ columns, sorts: sortsProp, defaultSorts, onSortsChange,
|
|
|
251
274
|
.map((col) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => addFilter(col.key), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label] }, col.key)))] }))] }), filters.map((cond) => {
|
|
252
275
|
const col = columns.find((c) => c.key === cond.key);
|
|
253
276
|
const open = openMenu === `fcond:${cond.id}`;
|
|
254
|
-
const hasValue = !opNeedsValue(cond.op) || cond.value.trim()
|
|
255
|
-
|
|
277
|
+
const hasValue = (!opNeedsValue(cond.op) || cond.value.trim()) &&
|
|
278
|
+
(!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
|
|
279
|
+
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fchip", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-field", children: col?.label }), hasValue ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: OP_LABELS[cond.op] }), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "aria-label": `Remove ${col?.label} filter`, onClick: () => removeFilter(cond.id), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2 }) })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} condition`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " condition"] }), opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
|
|
256
280
|
// biome-ignore lint/a11y/noAutofocus: focus the value field
|
|
257
|
-
autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) }))
|
|
281
|
+
autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })), cond.op === "isanyof" && col?.values && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
|
|
282
|
+
// biome-ignore lint/a11y/noAutofocus: focus the search field
|
|
283
|
+
autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
|
|
284
|
+
.filter((v) => v
|
|
285
|
+
.toLowerCase()
|
|
286
|
+
.includes(anyOfQuery.trim().toLowerCase()))
|
|
287
|
+
.map((v) => {
|
|
288
|
+
const checked = (cond.values ?? []).includes(v);
|
|
289
|
+
return ((0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: checked, onCheckedChange: (next) => {
|
|
290
|
+
const set = new Set(cond.values ?? []);
|
|
291
|
+
if (next)
|
|
292
|
+
set.add(v);
|
|
293
|
+
else
|
|
294
|
+
set.delete(v);
|
|
295
|
+
// the column's own order, not click order —
|
|
296
|
+
// what the chip summary reads out
|
|
297
|
+
patchFilter(cond.id, {
|
|
298
|
+
values: col.values?.filter((x) => set.has(x)),
|
|
299
|
+
});
|
|
300
|
+
}, children: v }, v));
|
|
301
|
+
})] }))] }))] }, cond.id));
|
|
258
302
|
}), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
|
|
259
303
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FloatingPortal = FloatingPortal;
|
|
4
|
+
const react_dom_1 = require("react-dom");
|
|
5
|
+
/**
|
|
6
|
+
* Portals a floating layer (a `position: fixed` listbox/menu/popover
|
|
7
|
+
* content) to `document.body` — exactly what Tooltip already did with a
|
|
8
|
+
* bare `createPortal` call, pulled out so `Select`, `MultiSelect`, `Menu`,
|
|
9
|
+
* `Popover` and `ContextMenu` share the one implementation (#339).
|
|
10
|
+
*
|
|
11
|
+
* Needed because a `position: fixed` box resolves against the nearest
|
|
12
|
+
* ancestor that establishes a containing block for it — not necessarily the
|
|
13
|
+
* viewport. `transform` (even the identity `scale(1)`), `filter` and
|
|
14
|
+
* `backdrop-filter` all qualify, and `Dialog`'s own measured surface sets
|
|
15
|
+
* both a resting `transform: scale(1)` (so its `@starting-style` entrance
|
|
16
|
+
* has something to animate from) and `backdrop-filter: blur(15px)` (Attio's
|
|
17
|
+
* frosted-glass modal). A floating layer opened from a trigger inside a
|
|
18
|
+
* `Dialog` was rendering offset by the dialog's own box instead of anchored
|
|
19
|
+
* to its trigger.
|
|
20
|
+
*
|
|
21
|
+
* Every caller renders this conditionally on its own `open` state, which
|
|
22
|
+
* starts `false` and is driven only by client interaction — never a prop a
|
|
23
|
+
* fixture sets true at mount (unlike Dialog/Drawer/Command) — so this never
|
|
24
|
+
* touches `document` during SSR, the same safety Tooltip already had.
|
|
25
|
+
*/
|
|
26
|
+
function FloatingPortal({ children, }) {
|
|
27
|
+
return (0, react_dom_1.createPortal)(children, document.body);
|
|
28
|
+
}
|