@mezzanine-ui/react 1.0.0-beta.0 → 1.0.0-beta.2
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/Badge/Badge.js +2 -2
- package/Breadcrumb/BreadcrumbItem.d.ts +1 -1
- package/Description/Description.d.ts +40 -0
- package/Description/Description.js +33 -0
- package/Description/DescriptionContent.d.ts +41 -0
- package/Description/DescriptionContent.js +14 -0
- package/Description/DescriptionGroup.d.ts +13 -0
- package/Description/DescriptionGroup.js +12 -0
- package/Description/DescriptionTitle.d.ts +45 -0
- package/Description/DescriptionTitle.js +17 -0
- package/Description/index.d.ts +8 -0
- package/Description/index.js +4 -0
- package/Dropdown/Dropdown.d.ts +128 -15
- package/Dropdown/Dropdown.js +273 -32
- package/Dropdown/DropdownAction.d.ts +50 -0
- package/Dropdown/DropdownAction.js +26 -0
- package/Dropdown/DropdownItem.d.ts +60 -0
- package/Dropdown/DropdownItem.js +318 -0
- package/Dropdown/DropdownItemCard.d.ts +96 -0
- package/Dropdown/DropdownItemCard.js +115 -0
- package/Dropdown/DropdownStatus.d.ts +22 -0
- package/Dropdown/dropdownKeydownHandler.d.ts +15 -0
- package/Dropdown/highlightText.d.ts +9 -0
- package/Dropdown/highlightText.js +32 -0
- package/Dropdown/index.d.ts +1 -1
- package/Form/FormControlContext.d.ts +2 -2
- package/Form/FormField.d.ts +56 -4
- package/Form/FormField.js +8 -6
- package/Form/FormHintText.d.ts +24 -1
- package/Form/FormHintText.js +4 -4
- package/Form/FormLabel.d.ts +6 -3
- package/Form/FormLabel.js +5 -3
- package/Input/PasswordStrengthIndicator/PasswordStrengthIndicator.js +1 -1
- package/Navigation/Navigation.d.ts +17 -14
- package/Navigation/Navigation.js +73 -41
- package/Navigation/NavigationFooter.d.ts +10 -0
- package/Navigation/NavigationFooter.js +28 -0
- package/Navigation/NavigationHeader.d.ts +14 -0
- package/Navigation/NavigationHeader.js +15 -0
- package/Navigation/NavigationIconButton.d.ts +15 -0
- package/Navigation/NavigationIconButton.js +12 -0
- package/Navigation/NavigationOption.d.ts +35 -0
- package/Navigation/NavigationOption.js +54 -0
- package/Navigation/NavigationOptionCategory.d.ts +6 -0
- package/Navigation/NavigationOptionCategory.js +29 -0
- package/Navigation/NavigationUserMenu.d.ts +8 -0
- package/Navigation/NavigationUserMenu.js +18 -0
- package/Navigation/context.d.ts +15 -0
- package/Navigation/context.js +7 -0
- package/Navigation/index.d.ts +12 -6
- package/Navigation/index.js +6 -3
- package/Navigation/useCurrentPathname.d.ts +1 -0
- package/Navigation/useCurrentPathname.js +14 -0
- package/Slider/useSlider.js +1 -1
- package/Table/Table.d.ts +53 -15
- package/Table/Table.js +178 -82
- package/Table/TableContext.d.ts +18 -42
- package/Table/components/TableActionsCell.d.ts +26 -0
- package/Table/components/TableActionsCell.js +78 -0
- package/Table/components/TableBody.d.ts +2 -5
- package/Table/components/TableBody.js +16 -19
- package/Table/components/TableBulkActions.d.ts +15 -0
- package/Table/components/TableBulkActions.js +44 -0
- package/Table/components/TableCell.d.ts +2 -0
- package/Table/components/TableCell.js +42 -10
- package/Table/components/TableColGroup.js +10 -112
- package/Table/components/TableColumnTitleMenu.d.ts +6 -0
- package/Table/components/TableColumnTitleMenu.js +20 -0
- package/Table/components/TableDragHandleCell.d.ts +2 -0
- package/Table/components/TableDragHandleCell.js +8 -1
- package/Table/components/TableExpandCell.d.ts +2 -0
- package/Table/components/TableExpandCell.js +8 -1
- package/Table/components/TableExpandedRow.js +3 -2
- package/Table/components/TableHeader.d.ts +2 -4
- package/Table/components/TableHeader.js +11 -14
- package/Table/components/TableResizeHandle.js +3 -7
- package/Table/components/TableRow.js +54 -20
- package/Table/components/TableSelectionCell.d.ts +5 -0
- package/Table/components/TableSelectionCell.js +12 -1
- package/Table/components/index.d.ts +1 -0
- package/Table/components/index.js +1 -0
- package/Table/hooks/index.d.ts +1 -1
- package/Table/hooks/index.js +1 -1
- package/Table/hooks/useTableDataSource.d.ts +2 -2
- package/Table/hooks/useTableExpansion.js +0 -6
- package/Table/hooks/useTableFixedOffsets.d.ts +1 -1
- package/Table/hooks/useTableFixedOffsets.js +19 -21
- package/Table/hooks/useTableResizedColumns.d.ts +2 -0
- package/Table/hooks/useTableResizedColumns.js +22 -0
- package/Table/hooks/useTableScroll.d.ts +3 -1
- package/Table/hooks/useTableScroll.js +25 -19
- package/Table/hooks/useTableSelection.js +32 -8
- package/Table/hooks/useTableVirtualization.d.ts +1 -1
- package/Table/index.d.ts +4 -4
- package/Table/index.js +5 -3
- package/Table/utils/calculateColumnWidths.d.ts +28 -0
- package/Table/utils/calculateColumnWidths.js +80 -0
- package/Table/utils/index.d.ts +2 -0
- package/Table/utils/index.js +1 -0
- package/Table/utils/useTableRowSelection.d.ts +5 -5
- package/Table/utils/useTableRowSelection.js +1 -1
- package/hooks/useElementHeight.d.ts +8 -0
- package/hooks/useElementHeight.js +41 -0
- package/index.d.ts +8 -6
- package/index.js +9 -3
- package/package.json +5 -4
- package/utils/format-number-with-commas.d.ts +4 -0
- package/utils/format-number-with-commas.js +27 -0
- package/utils/parse-number-with-commas.d.ts +4 -0
- package/utils/parse-number-with-commas.js +22 -0
- package/Navigation/NavigationContext.d.ts +0 -5
- package/Navigation/NavigationContext.js +0 -8
- package/Navigation/NavigationItem.d.ts +0 -31
- package/Navigation/NavigationItem.js +0 -23
- package/Navigation/NavigationSubMenu.d.ts +0 -22
- package/Navigation/NavigationSubMenu.js +0 -50
- package/Table/hooks/useTableColumns.d.ts +0 -8
- package/Table/hooks/useTableColumns.js +0 -91
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import keycode from 'keycode';
|
|
4
|
+
import { useRef, useState, useCallback, useMemo, useEffect } from 'react';
|
|
5
|
+
import { dropdownClasses } from '@mezzanine-ui/core/dropdown/dropdown';
|
|
6
|
+
import { CaretDownIcon, CaretRightIcon } from '@mezzanine-ui/icons';
|
|
7
|
+
import { useElementHeight } from '../hooks/useElementHeight.js';
|
|
8
|
+
import Typography from '../Typography/Typography.js';
|
|
9
|
+
import DropdownAction from './DropdownAction.js';
|
|
10
|
+
import DropdownItemCard from './DropdownItemCard.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Limits DropdownOption array to a maximum depth, truncating extra children levels and showing error message if exceeded.
|
|
14
|
+
* @param input - The original DropdownOption array
|
|
15
|
+
* @param maxDepth - Maximum depth (default: 3)
|
|
16
|
+
* @param warn - Whether to show warning (default: true)
|
|
17
|
+
* @returns DropdownOption array with at most the specified depth
|
|
18
|
+
*/
|
|
19
|
+
function truncateArrayDepth(input, maxDepth = 3, warn = true) {
|
|
20
|
+
// Internal recursive function: truncates children to specified depth
|
|
21
|
+
const truncate = (options, currentDepth = 1) => {
|
|
22
|
+
if (currentDepth >= maxDepth) {
|
|
23
|
+
// Stop going deeper once maximum depth is reached, remove children
|
|
24
|
+
return options.map(({ children: _children, ...option }) => option);
|
|
25
|
+
}
|
|
26
|
+
return options.map(option => {
|
|
27
|
+
if (!option.children)
|
|
28
|
+
return option;
|
|
29
|
+
return {
|
|
30
|
+
...option,
|
|
31
|
+
children: truncate(option.children, currentDepth + 1),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
// Calculate maximum depth by checking all elements (not just the first one)
|
|
36
|
+
const getDepth = (options, depth = 1) => {
|
|
37
|
+
if (!options || options.length === 0)
|
|
38
|
+
return depth - 1;
|
|
39
|
+
// Find the maximum depth among all options
|
|
40
|
+
return Math.max(...options.map((option) => {
|
|
41
|
+
if (!option.children || option.children.length === 0) {
|
|
42
|
+
return depth - 1;
|
|
43
|
+
}
|
|
44
|
+
return getDepth(option.children, depth + 1);
|
|
45
|
+
}));
|
|
46
|
+
};
|
|
47
|
+
const depth = getDepth(input);
|
|
48
|
+
if (depth <= maxDepth)
|
|
49
|
+
return input;
|
|
50
|
+
// Exceeds maximum depth → warn
|
|
51
|
+
if (warn) {
|
|
52
|
+
console.error(`[truncateArrayDepth] Input DropdownOption array exceeds ${maxDepth} levels. Extra levels were truncated.`);
|
|
53
|
+
}
|
|
54
|
+
// Truncate to specified depth
|
|
55
|
+
return truncate(input);
|
|
56
|
+
}
|
|
57
|
+
function DropdownItem(props) {
|
|
58
|
+
const { activeIndex, disabled = false, listboxId, listboxLabel, mode = 'single', options, value, type, maxHeight, actionConfig, onHover, onSelect, followText, headerContent, } = props;
|
|
59
|
+
const optionsContent = truncateArrayDepth(options, 3);
|
|
60
|
+
const listRef = useRef(null);
|
|
61
|
+
const [expandedNodes, setExpandedNodes] = useState(new Set());
|
|
62
|
+
const hasActions = Boolean(actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.showActions);
|
|
63
|
+
const hasHeader = Boolean(headerContent);
|
|
64
|
+
// Use custom hook to measure element heights
|
|
65
|
+
const [actionRef, actionHeight] = useElementHeight(hasActions && !!maxHeight);
|
|
66
|
+
const [headerRef, headerHeight] = useElementHeight(hasHeader && !!maxHeight);
|
|
67
|
+
const toggleExpand = useCallback((optionId) => {
|
|
68
|
+
setExpandedNodes((prev) => {
|
|
69
|
+
const next = new Set(prev);
|
|
70
|
+
if (next.has(optionId)) {
|
|
71
|
+
next.delete(optionId);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
next.add(optionId);
|
|
75
|
+
}
|
|
76
|
+
return next;
|
|
77
|
+
});
|
|
78
|
+
}, []);
|
|
79
|
+
const visibleShortcutOptions = useMemo(() => {
|
|
80
|
+
const result = [];
|
|
81
|
+
const collectDefault = (optionList) => {
|
|
82
|
+
optionList === null || optionList === void 0 ? void 0 : optionList.forEach((option) => {
|
|
83
|
+
result.push(option);
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
const collectGrouped = (optionList) => {
|
|
87
|
+
optionList === null || optionList === void 0 ? void 0 : optionList.forEach((groupOption) => {
|
|
88
|
+
var _a;
|
|
89
|
+
(_a = groupOption.children) === null || _a === void 0 ? void 0 : _a.forEach((option) => {
|
|
90
|
+
result.push(option);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const collectTree = (optionList) => {
|
|
95
|
+
optionList === null || optionList === void 0 ? void 0 : optionList.forEach((option) => {
|
|
96
|
+
result.push(option);
|
|
97
|
+
if (option.children && expandedNodes.has(option.id)) {
|
|
98
|
+
collectTree(option.children);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
if (type === 'grouped') {
|
|
103
|
+
collectGrouped(optionsContent);
|
|
104
|
+
}
|
|
105
|
+
else if (type === 'tree') {
|
|
106
|
+
collectTree(optionsContent);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
collectDefault(optionsContent);
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}, [expandedNodes, optionsContent, type]);
|
|
113
|
+
const matchShortcut = useCallback((event, shortcut) => {
|
|
114
|
+
var _a, _b;
|
|
115
|
+
const eventCode = (_a = event.which) !== null && _a !== void 0 ? _a : event.keyCode;
|
|
116
|
+
if (typeof shortcut === 'number') {
|
|
117
|
+
return eventCode === shortcut;
|
|
118
|
+
}
|
|
119
|
+
const tokens = shortcut
|
|
120
|
+
.split('+')
|
|
121
|
+
.map((token) => token.trim().toLowerCase())
|
|
122
|
+
.filter(Boolean);
|
|
123
|
+
let requireMeta = false;
|
|
124
|
+
let requireCtrl = false;
|
|
125
|
+
let requireAlt = false;
|
|
126
|
+
let requireShift = false;
|
|
127
|
+
let mainToken = null;
|
|
128
|
+
tokens.forEach((token) => {
|
|
129
|
+
switch (token) {
|
|
130
|
+
case 'cmd':
|
|
131
|
+
case 'meta':
|
|
132
|
+
case 'command':
|
|
133
|
+
requireMeta = true;
|
|
134
|
+
break;
|
|
135
|
+
case 'ctrl':
|
|
136
|
+
case 'control':
|
|
137
|
+
requireCtrl = true;
|
|
138
|
+
break;
|
|
139
|
+
case 'alt':
|
|
140
|
+
case 'option':
|
|
141
|
+
requireAlt = true;
|
|
142
|
+
break;
|
|
143
|
+
case 'shift':
|
|
144
|
+
requireShift = true;
|
|
145
|
+
break;
|
|
146
|
+
default:
|
|
147
|
+
mainToken = token;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
if (!mainToken)
|
|
152
|
+
return false;
|
|
153
|
+
if (requireMeta !== event.metaKey
|
|
154
|
+
|| requireCtrl !== event.ctrlKey
|
|
155
|
+
|| requireAlt !== event.altKey
|
|
156
|
+
|| requireShift !== event.shiftKey) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const mainCode = keycode(mainToken);
|
|
160
|
+
if (typeof mainCode === 'number' && eventCode === mainCode) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
const eventKey = (_b = event.key) === null || _b === void 0 ? void 0 : _b.toLowerCase();
|
|
164
|
+
if (eventKey && eventKey === mainToken) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
const eventKeyName = keycode(eventCode);
|
|
168
|
+
return typeof eventKeyName === 'string' && eventKeyName.toLowerCase() === mainToken;
|
|
169
|
+
}, []);
|
|
170
|
+
const renderGroupedOptions = (optionList, startIndex) => {
|
|
171
|
+
let currentIndex = startIndex;
|
|
172
|
+
const elements = (optionList !== null && optionList !== void 0 ? optionList : []).flatMap((groupOption) => {
|
|
173
|
+
var _a;
|
|
174
|
+
const hasChildren = Boolean(groupOption.children && groupOption.children.length > 0);
|
|
175
|
+
const groupElements = [];
|
|
176
|
+
if (hasChildren) {
|
|
177
|
+
groupElements.push(jsx(Typography, { variant: "body", className: dropdownClasses.groupLabel, children: groupOption.name }, groupOption.id));
|
|
178
|
+
(_a = groupOption.children) === null || _a === void 0 ? void 0 : _a.forEach((option) => {
|
|
179
|
+
var _a, _b;
|
|
180
|
+
currentIndex += 1;
|
|
181
|
+
const optionIndex = currentIndex;
|
|
182
|
+
const isActive = optionIndex === activeIndex;
|
|
183
|
+
const isSelected = Array.isArray(value)
|
|
184
|
+
? value.includes(option.id)
|
|
185
|
+
: value === option.id;
|
|
186
|
+
groupElements.push(jsx(DropdownItemCard, { followText: followText, active: isActive, checked: isSelected, disabled: disabled, id: `${listboxId}-option-${optionIndex}`, label: option.name, mode: mode, name: option.name, onClick: () => {
|
|
187
|
+
if (disabled)
|
|
188
|
+
return;
|
|
189
|
+
onSelect === null || onSelect === void 0 ? void 0 : onSelect(option);
|
|
190
|
+
}, checkSite: "none", validate: (_a = option.validate) !== null && _a !== void 0 ? _a : 'default', onMouseEnter: () => onHover === null || onHover === void 0 ? void 0 : onHover(optionIndex), showUnderline: (_b = option.showUnderline) !== null && _b !== void 0 ? _b : false, appendContent: option.shortcutText }, option.id));
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return groupElements;
|
|
194
|
+
});
|
|
195
|
+
return { elements, nextIndex: currentIndex };
|
|
196
|
+
};
|
|
197
|
+
const renderTreeOptions = (optionList, depth, startIndex) => {
|
|
198
|
+
let currentIndex = startIndex;
|
|
199
|
+
const elements = (optionList !== null && optionList !== void 0 ? optionList : []).flatMap((option) => {
|
|
200
|
+
var _a, _b;
|
|
201
|
+
currentIndex += 1;
|
|
202
|
+
const optionIndex = currentIndex;
|
|
203
|
+
const level = Math.min(depth, 2);
|
|
204
|
+
const isActive = optionIndex === activeIndex;
|
|
205
|
+
const isSelected = Array.isArray(value)
|
|
206
|
+
? value.includes(option.id)
|
|
207
|
+
: value === option.id;
|
|
208
|
+
const hasChildren = Boolean(option.children && option.children.length > 0);
|
|
209
|
+
const isExpanded = hasChildren && expandedNodes.has(option.id);
|
|
210
|
+
let prependIcon = undefined;
|
|
211
|
+
if (hasChildren && level !== 2) {
|
|
212
|
+
prependIcon = isExpanded ? CaretDownIcon : CaretRightIcon;
|
|
213
|
+
}
|
|
214
|
+
const checkSite = option.showCheckbox ? 'prepend' : 'none';
|
|
215
|
+
const card = (jsx(DropdownItemCard, { active: isActive, checked: isSelected, disabled: disabled, id: `${listboxId}-option-${optionIndex}`, label: option.name, level: level, mode: mode, name: option.name, onClick: () => {
|
|
216
|
+
if (disabled)
|
|
217
|
+
return;
|
|
218
|
+
if (hasChildren && type === 'tree') {
|
|
219
|
+
toggleExpand(option.id);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
onSelect === null || onSelect === void 0 ? void 0 : onSelect(option);
|
|
223
|
+
}
|
|
224
|
+
}, followText: followText, checkSite: checkSite, onMouseEnter: () => onHover === null || onHover === void 0 ? void 0 : onHover(optionIndex), prependIcon: prependIcon, showUnderline: (_a = option.showUnderline) !== null && _a !== void 0 ? _a : false, validate: (_b = option.validate) !== null && _b !== void 0 ? _b : 'default', appendContent: option.shortcutText }, option.id));
|
|
225
|
+
if (hasChildren && isExpanded && type === 'tree') {
|
|
226
|
+
const childResult = renderTreeOptions(option.children, depth + 1, currentIndex);
|
|
227
|
+
currentIndex = childResult.nextIndex;
|
|
228
|
+
return [card, ...childResult.elements];
|
|
229
|
+
}
|
|
230
|
+
return [card];
|
|
231
|
+
});
|
|
232
|
+
return { elements, nextIndex: currentIndex };
|
|
233
|
+
};
|
|
234
|
+
const renderDefaultOptions = (optionList, startIndex) => {
|
|
235
|
+
let currentIndex = startIndex;
|
|
236
|
+
const elements = (optionList !== null && optionList !== void 0 ? optionList : []).flatMap((option) => {
|
|
237
|
+
var _a, _b;
|
|
238
|
+
currentIndex += 1;
|
|
239
|
+
const optionIndex = currentIndex;
|
|
240
|
+
const isSelected = Array.isArray(value)
|
|
241
|
+
? value.includes(option.id)
|
|
242
|
+
: value === option.id;
|
|
243
|
+
const isActive = optionIndex === activeIndex;
|
|
244
|
+
let checkSite = 'none';
|
|
245
|
+
if (option === null || option === void 0 ? void 0 : option.checkSite) {
|
|
246
|
+
checkSite = option.checkSite;
|
|
247
|
+
}
|
|
248
|
+
return (jsx(DropdownItemCard, { followText: followText, active: isActive, checked: isSelected, disabled: disabled, id: `${listboxId}-option-${optionIndex}`, label: option.name, mode: mode, name: option.name, onClick: () => {
|
|
249
|
+
if (disabled)
|
|
250
|
+
return;
|
|
251
|
+
onSelect === null || onSelect === void 0 ? void 0 : onSelect(option);
|
|
252
|
+
}, onMouseEnter: () => onHover === null || onHover === void 0 ? void 0 : onHover(optionIndex), prependIcon: option.icon, validate: (_a = option.validate) !== null && _a !== void 0 ? _a : 'default', showUnderline: (_b = option.showUnderline) !== null && _b !== void 0 ? _b : false, checkSite: checkSite, appendContent: option.shortcutText }, option.id));
|
|
253
|
+
});
|
|
254
|
+
return { elements, nextIndex: currentIndex };
|
|
255
|
+
};
|
|
256
|
+
const renderOptions = (optionList, depth, startIndex) => {
|
|
257
|
+
if (type === 'grouped') {
|
|
258
|
+
return renderGroupedOptions(optionList, startIndex);
|
|
259
|
+
}
|
|
260
|
+
if (type === 'tree') {
|
|
261
|
+
return renderTreeOptions(optionList, depth, startIndex);
|
|
262
|
+
}
|
|
263
|
+
return renderDefaultOptions(optionList, startIndex);
|
|
264
|
+
};
|
|
265
|
+
const { elements: renderedOptions } = renderOptions(optionsContent, 0, -1);
|
|
266
|
+
const listStyle = useMemo(() => {
|
|
267
|
+
if (!maxHeight) {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight,
|
|
272
|
+
};
|
|
273
|
+
}, [maxHeight]);
|
|
274
|
+
const listWrapperStyle = useMemo(() => {
|
|
275
|
+
if (!maxHeight) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
const maxHeightValue = typeof maxHeight === 'number' ? maxHeight : parseFloat(maxHeight);
|
|
279
|
+
const availableHeight = Math.max(0, maxHeightValue - actionHeight - headerHeight);
|
|
280
|
+
return {
|
|
281
|
+
maxHeight: `${availableHeight}px`,
|
|
282
|
+
};
|
|
283
|
+
}, [maxHeight, actionHeight, headerHeight]);
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
const listElement = listRef.current;
|
|
286
|
+
if (!listElement || disabled) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const handleKeyDown = (event) => {
|
|
290
|
+
if (event.repeat)
|
|
291
|
+
return;
|
|
292
|
+
const targetOption = visibleShortcutOptions.find((option) => {
|
|
293
|
+
if (!Array.isArray(option.shortcutKeys) || option.shortcutKeys.length === 0) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
return option.shortcutKeys.some((shortcut) => matchShortcut(event, shortcut));
|
|
297
|
+
});
|
|
298
|
+
if (!targetOption)
|
|
299
|
+
return;
|
|
300
|
+
event.preventDefault();
|
|
301
|
+
event.stopPropagation();
|
|
302
|
+
if (type === 'tree' && targetOption.children && targetOption.children.length > 0) {
|
|
303
|
+
toggleExpand(targetOption.id);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
onSelect === null || onSelect === void 0 ? void 0 : onSelect(targetOption);
|
|
307
|
+
};
|
|
308
|
+
listElement.addEventListener('keydown', handleKeyDown);
|
|
309
|
+
return () => {
|
|
310
|
+
listElement.removeEventListener('keydown', handleKeyDown);
|
|
311
|
+
};
|
|
312
|
+
}, [disabled, matchShortcut, onSelect, type, toggleExpand, visibleShortcutOptions]);
|
|
313
|
+
return (jsxs("ul", { "aria-label": listboxLabel || (optionsContent.length === 0 ? 'Dropdown options' : undefined), className: dropdownClasses.list, id: listboxId, ref: listRef, role: "listbox", style: listStyle, tabIndex: -1, children: [hasHeader && (jsx("li", { className: dropdownClasses.listHeader, role: "presentation", ref: headerRef, children: jsx("div", { className: dropdownClasses.listHeaderInner, children: headerContent }) })), maxHeight
|
|
314
|
+
? (jsx("div", { className: dropdownClasses.listWrapper, style: listWrapperStyle, children: renderedOptions }))
|
|
315
|
+
: renderedOptions, hasActions && (jsx("div", { ref: actionRef, children: jsx(DropdownAction, { ...actionConfig }) }))] }));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export { DropdownItem as default };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { DropdownCheckPosition, DropdownItemLevel, DropdownItemValidate, DropdownMode } from "@mezzanine-ui/core/dropdown/dropdown";
|
|
2
|
+
import { type IconDefinition } from "@mezzanine-ui/icons";
|
|
3
|
+
export interface DropdownItemCardProps {
|
|
4
|
+
/**
|
|
5
|
+
* Whether the option is currently active (highlighted by keyboard navigation).
|
|
6
|
+
* This controls the aria-selected attribute according to W3C ARIA spec.
|
|
7
|
+
* When an option is referenced by aria-activedescendant, it should have aria-selected="true".
|
|
8
|
+
*/
|
|
9
|
+
active?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The icon to append.
|
|
12
|
+
*/
|
|
13
|
+
appendIcon?: IconDefinition;
|
|
14
|
+
/**
|
|
15
|
+
* The content to append.
|
|
16
|
+
*/
|
|
17
|
+
appendContent?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The position of the checkbox.
|
|
20
|
+
*/
|
|
21
|
+
checkSite?: DropdownCheckPosition;
|
|
22
|
+
/**
|
|
23
|
+
* Controlled: Whether the option is selected/checked.
|
|
24
|
+
* Controls checkbox state in multiple mode.
|
|
25
|
+
* When provided, the state is controlled externally.
|
|
26
|
+
*/
|
|
27
|
+
checked?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Additional className for the list item.
|
|
30
|
+
*/
|
|
31
|
+
className?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Uncontrolled: Default checked/selected state.
|
|
34
|
+
* Only used when `checked` is not provided.
|
|
35
|
+
*/
|
|
36
|
+
defaultChecked?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the dropdown item card is disabled.
|
|
39
|
+
*/
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The text to follow.
|
|
43
|
+
*/
|
|
44
|
+
followText?: string;
|
|
45
|
+
/**
|
|
46
|
+
* DOM id for the option, useful for aria-activedescendant.
|
|
47
|
+
*/
|
|
48
|
+
id?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The label of the dropdown item card.
|
|
51
|
+
*/
|
|
52
|
+
label?: string;
|
|
53
|
+
/**
|
|
54
|
+
* The level of the dropdown item card.
|
|
55
|
+
*/
|
|
56
|
+
level?: DropdownItemLevel;
|
|
57
|
+
/**
|
|
58
|
+
* The mode of the dropdown item card.
|
|
59
|
+
*/
|
|
60
|
+
mode: DropdownMode;
|
|
61
|
+
/**
|
|
62
|
+
* The accessible name / label for the option.
|
|
63
|
+
* Falls back to label if not provided.
|
|
64
|
+
*/
|
|
65
|
+
name?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Callback fired when the checked/selected state changes.
|
|
68
|
+
*/
|
|
69
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Click handler when the list item is activated.
|
|
72
|
+
*/
|
|
73
|
+
onClick?: () => void;
|
|
74
|
+
/**
|
|
75
|
+
* Mouse enter handler.
|
|
76
|
+
*/
|
|
77
|
+
onMouseEnter?: () => void;
|
|
78
|
+
/**
|
|
79
|
+
* The icon to prepend.
|
|
80
|
+
*/
|
|
81
|
+
prependIcon?: IconDefinition;
|
|
82
|
+
/**
|
|
83
|
+
* Whether to show the underline.
|
|
84
|
+
* @default false
|
|
85
|
+
*/
|
|
86
|
+
showUnderline?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* The subtitle of the dropdown item card.
|
|
89
|
+
*/
|
|
90
|
+
subTitle?: string;
|
|
91
|
+
/**
|
|
92
|
+
* The validation of the dropdown item card.
|
|
93
|
+
*/
|
|
94
|
+
validate?: DropdownItemValidate;
|
|
95
|
+
}
|
|
96
|
+
export default function DropdownItemCard(props: DropdownItemCardProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
3
|
+
import cx from 'clsx';
|
|
4
|
+
import { useMemo, useState } from 'react';
|
|
5
|
+
import { dropdownClasses } from '@mezzanine-ui/core/dropdown/dropdown';
|
|
6
|
+
import { CheckedIcon } from '@mezzanine-ui/icons';
|
|
7
|
+
import Checkbox from '../Checkbox/Checkbox.js';
|
|
8
|
+
import Typography from '../Typography/Typography.js';
|
|
9
|
+
import { highlightText } from './highlightText.js';
|
|
10
|
+
import Icon from '../Icon/Icon.js';
|
|
11
|
+
|
|
12
|
+
function DropdownItemCard(props) {
|
|
13
|
+
const { active = false, appendIcon, appendContent, followText, id, label, level: levelProp, mode, name: _name, prependIcon, subTitle, validate, disabled, checked, defaultChecked, checkSite, onCheckedChange, onClick, className, onMouseEnter, showUnderline, } = props;
|
|
14
|
+
const cardLabel = label || '';
|
|
15
|
+
const cardName = _name || cardLabel;
|
|
16
|
+
const level = levelProp || 0;
|
|
17
|
+
// Generate ID for the label element to use with aria-labelledby
|
|
18
|
+
// If no id is provided, we'll rely on the visible text content for accessibility
|
|
19
|
+
const labelId = useMemo(() => {
|
|
20
|
+
if (!id)
|
|
21
|
+
return undefined;
|
|
22
|
+
return `${id}-label`;
|
|
23
|
+
}, [id]);
|
|
24
|
+
// If name is different from label, we need to use aria-label as fallback
|
|
25
|
+
// Note: aria-label on role="option" has limited support, but it's better than nothing
|
|
26
|
+
const ariaLabel = useMemo(() => {
|
|
27
|
+
if (cardName !== cardLabel) {
|
|
28
|
+
return cardName;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}, [cardName, cardLabel]);
|
|
32
|
+
// Controlled/uncontrolled mode for checked/selected state
|
|
33
|
+
const isControlled = checked !== undefined;
|
|
34
|
+
const [internalChecked, setInternalChecked] = useState(defaultChecked !== null && defaultChecked !== void 0 ? defaultChecked : false);
|
|
35
|
+
const isChecked = isControlled ? checked : internalChecked;
|
|
36
|
+
const labelColor = useMemo(() => {
|
|
37
|
+
return validate === 'danger' ? 'text-error' : 'text-neutral-solid';
|
|
38
|
+
}, [validate]);
|
|
39
|
+
const appendIconColor = useMemo(() => {
|
|
40
|
+
return disabled || validate === 'danger' ? 'neutral-light' : 'neutral';
|
|
41
|
+
}, [disabled, validate]);
|
|
42
|
+
const iconColor = useMemo(() => {
|
|
43
|
+
if (disabled)
|
|
44
|
+
return 'neutral-light';
|
|
45
|
+
return validate === 'danger' ? 'error' : 'neutral';
|
|
46
|
+
}, [disabled, validate]);
|
|
47
|
+
const labelParts = useMemo(() => {
|
|
48
|
+
return followText
|
|
49
|
+
? highlightText(cardLabel, followText)
|
|
50
|
+
: [
|
|
51
|
+
{
|
|
52
|
+
text: cardLabel,
|
|
53
|
+
highlight: false
|
|
54
|
+
}
|
|
55
|
+
];
|
|
56
|
+
}, [cardLabel, followText]);
|
|
57
|
+
const showPrependContent = useMemo(() => {
|
|
58
|
+
return prependIcon || (checkSite === 'prepend' && mode === 'multiple');
|
|
59
|
+
}, [prependIcon, checkSite, mode]);
|
|
60
|
+
const showAppendContent = useMemo(() => {
|
|
61
|
+
return appendContent || appendIcon || (checkSite === 'append' && isChecked);
|
|
62
|
+
}, [appendContent, appendIcon, checkSite, isChecked]);
|
|
63
|
+
const subTitleParts = useMemo(() => {
|
|
64
|
+
return followText && subTitle
|
|
65
|
+
? highlightText(subTitle, followText)
|
|
66
|
+
: subTitle
|
|
67
|
+
? [
|
|
68
|
+
{
|
|
69
|
+
text: subTitle,
|
|
70
|
+
highlight: false
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
: [];
|
|
74
|
+
}, [subTitle, followText]);
|
|
75
|
+
const renderHighlightedText = (parts, defaultColor, className, id) => {
|
|
76
|
+
return (jsx(Typography, { color: defaultColor, className: className, id: id, children: parts.map((part, index) => (jsx("span", { className: part.highlight && validate !== 'danger' ? dropdownClasses.cardHighlightedText : '', children: part.text }, index))) }));
|
|
77
|
+
};
|
|
78
|
+
const toggleChecked = () => {
|
|
79
|
+
if (disabled)
|
|
80
|
+
return;
|
|
81
|
+
const newChecked = !isChecked;
|
|
82
|
+
if (!isControlled) {
|
|
83
|
+
setInternalChecked(newChecked);
|
|
84
|
+
}
|
|
85
|
+
onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange(newChecked);
|
|
86
|
+
};
|
|
87
|
+
const handleClick = () => {
|
|
88
|
+
if (disabled)
|
|
89
|
+
return;
|
|
90
|
+
if (mode === 'multiple') {
|
|
91
|
+
toggleChecked();
|
|
92
|
+
}
|
|
93
|
+
onClick === null || onClick === void 0 ? void 0 : onClick();
|
|
94
|
+
};
|
|
95
|
+
const handleCheckboxChange = (event) => {
|
|
96
|
+
event.stopPropagation();
|
|
97
|
+
toggleChecked();
|
|
98
|
+
};
|
|
99
|
+
const handleKeyDown = (event) => {
|
|
100
|
+
if (disabled)
|
|
101
|
+
return;
|
|
102
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
onClick === null || onClick === void 0 ? void 0 : onClick();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
return (jsxs(Fragment, { children: [jsx("li", { ...(labelId ? { 'aria-labelledby': labelId } : {}), ...(ariaLabel ? { 'aria-label': ariaLabel } : {}), "aria-selected": active, className: cx(dropdownClasses.card, dropdownClasses.cardLevel(level), {
|
|
108
|
+
// Highlight: keyboard/mouse focused (active) or selected (isChecked)
|
|
109
|
+
[dropdownClasses.cardActive]: active || isChecked,
|
|
110
|
+
[dropdownClasses.cardDisabled]: disabled,
|
|
111
|
+
}, className), id: id, role: "option", tabIndex: -1, onMouseEnter: onMouseEnter, onClick: handleClick, onKeyDown: handleKeyDown, children: jsxs("div", { className: dropdownClasses.cardContainer, children: [showPrependContent && (jsxs("div", { className: dropdownClasses.cardPrependContent, children: [prependIcon
|
|
112
|
+
&& jsx(Icon, { icon: prependIcon, color: iconColor }), checkSite === 'prepend' && mode === 'multiple' && (jsx(Checkbox, { checked: isChecked, disabled: disabled, onChange: handleCheckboxChange }))] })), jsxs("div", { className: dropdownClasses.cardBody, children: [cardLabel && renderHighlightedText(labelParts, labelColor, dropdownClasses.cardTitle, labelId), subTitleParts.length > 0 && renderHighlightedText(subTitleParts, 'text-neutral', dropdownClasses.cardDescription)] }), showAppendContent && (jsxs("div", { className: dropdownClasses.cardAppendContent, children: [appendContent && jsx(Typography, { color: "text-neutral-light", children: appendContent }), appendIcon && jsx(Icon, { icon: appendIcon, color: iconColor }), checkSite === 'append' && isChecked && jsx(Icon, { icon: CheckedIcon, color: appendIconColor, size: 16 })] }))] }) }), showUnderline && jsx("div", { className: dropdownClasses.cardUnderline })] }));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { DropdownItemCard as default };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DropdownStatus as DropdownStatusType } from "@mezzanine-ui/core/dropdown/dropdown";
|
|
2
|
+
import { IconDefinition } from "@mezzanine-ui/icons";
|
|
3
|
+
export interface DropdownStatusProps {
|
|
4
|
+
/**
|
|
5
|
+
* The status of the dropdown.
|
|
6
|
+
* @default 'loading'
|
|
7
|
+
*/
|
|
8
|
+
status: DropdownStatusType;
|
|
9
|
+
/**
|
|
10
|
+
* The text of the dropdown loading status.
|
|
11
|
+
*/
|
|
12
|
+
loadingText?: string;
|
|
13
|
+
/**
|
|
14
|
+
* The text of the dropdown empty status.
|
|
15
|
+
*/
|
|
16
|
+
emptyText?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The icon of the dropdown empty status.
|
|
19
|
+
*/
|
|
20
|
+
emptyIcon?: IconDefinition;
|
|
21
|
+
}
|
|
22
|
+
export default function DropdownStatus(props: DropdownStatusProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DropdownOption } from '@mezzanine-ui/core/dropdown/dropdown';
|
|
2
|
+
/**
|
|
3
|
+
* Provides a keyboard navigation handler for dropdown lists, encapsulating Arrow keys, Enter, and Escape behaviors.
|
|
4
|
+
* Keeps logic centralized in DropdownItem for easy reuse.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createDropdownKeydownHandler(params: {
|
|
7
|
+
activeIndex: number | null;
|
|
8
|
+
onEnterSelect?: (option: DropdownOption) => void;
|
|
9
|
+
onEscape?: () => void;
|
|
10
|
+
open: boolean;
|
|
11
|
+
options: DropdownOption[];
|
|
12
|
+
setActiveIndex: (updater: (prev: number | null) => number | null) => void;
|
|
13
|
+
setListboxHasVisualFocus: (focus: boolean) => void;
|
|
14
|
+
setOpen: (open: boolean) => void;
|
|
15
|
+
}): (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type HighlightSegment = {
|
|
2
|
+
text: string;
|
|
3
|
+
highlight: boolean;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Splits the text into segments based on the keyword and marks which segments should be highlighted.
|
|
7
|
+
* - If the keyword is empty or not found, returns a single non-highlighted segment.
|
|
8
|
+
*/
|
|
9
|
+
export declare function highlightText(text: string, keyword?: string): HighlightSegment[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits the text into segments based on the keyword and marks which segments should be highlighted.
|
|
3
|
+
* - If the keyword is empty or not found, returns a single non-highlighted segment.
|
|
4
|
+
*/
|
|
5
|
+
function highlightText(text, keyword) {
|
|
6
|
+
var _a;
|
|
7
|
+
if (!text)
|
|
8
|
+
return [];
|
|
9
|
+
if (!keyword)
|
|
10
|
+
return [{ text, highlight: false }];
|
|
11
|
+
const safeKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
12
|
+
const regex = new RegExp(`(${safeKeyword})`, 'gi');
|
|
13
|
+
const parts = [];
|
|
14
|
+
let lastIndex = 0;
|
|
15
|
+
for (const match of text.matchAll(regex)) {
|
|
16
|
+
const matchIndex = (_a = match.index) !== null && _a !== void 0 ? _a : 0;
|
|
17
|
+
if (matchIndex > lastIndex) {
|
|
18
|
+
parts.push({
|
|
19
|
+
text: text.slice(lastIndex, matchIndex),
|
|
20
|
+
highlight: false,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
parts.push({ text: match[0], highlight: true });
|
|
24
|
+
lastIndex = matchIndex + match[0].length;
|
|
25
|
+
}
|
|
26
|
+
if (lastIndex < text.length) {
|
|
27
|
+
parts.push({ text: text.slice(lastIndex), highlight: false });
|
|
28
|
+
}
|
|
29
|
+
return parts.length ? parts : [{ text, highlight: false }];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { highlightText };
|
package/Dropdown/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SeverityWithInfo } from '@mezzanine-ui/system/severity';
|
|
2
2
|
export interface FormControl {
|
|
3
3
|
disabled: boolean;
|
|
4
4
|
fullWidth: boolean;
|
|
5
5
|
required: boolean;
|
|
6
|
-
severity?:
|
|
6
|
+
severity?: SeverityWithInfo;
|
|
7
7
|
}
|
|
8
8
|
export declare const FormControlContext: import("react").Context<FormControl | undefined>;
|