@kwiz/fluentui
Version:
KWIZ common controls for FluentUI
127 lines • 7.68 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Menu, MenuDivider, MenuGroup, MenuGroupHeader, MenuItem, MenuList, MenuPopover, menuPopoverClassNames, MenuTrigger } from '@fluentui/react-components';
import { isNotEmptyArray, isNotEmptyString, isNullOrEmptyString, isNullOrUndefined, isNumber, isString, isUndefined, jsonClone, stopEvent } from '@kwiz/common';
import React from 'react';
import { useClickableDiv, useStateEX } from '../helpers';
import { useKWIZFluentContext } from '../helpers/context-internal';
import { ButtonEX } from './button';
import { DividerEX } from './divider';
import { Horizontal } from './horizontal';
import { Search } from './search';
export const MenuEx = (props) => {
const ctx = useKWIZFluentContext();
const [startIndexPerLevel, setStartIndexPerLevel] = useStateEX({});
const [filterPerLevel, setFilterPerLevel] = useStateEX({});
let pageSize = isUndefined(props.pageSize) ? 8 : isNumber(props.pageSize) ? props.pageSize : 99999999999;
let filterThreshold = isUndefined(props.filterThreshold) ? 8 : isNumber(props.filterThreshold) ? props.filterThreshold : 99999999999;
//when hovering over sub menu the parent would close - have menu trigger keep open on the parent level
const [keepOpen, setKeepOpen] = useStateEX({});
const [opened, setOpened] = useStateEX({});
const clickableDiv = useClickableDiv();
React.useEffect(() => {
window.setTimeout(() => {
var menus = document.querySelectorAll(`.${menuPopoverClassNames.root}`);
menus.forEach((menu) => {
var rect = menu.getBoundingClientRect();
if (rect.bottom > document.documentElement.clientHeight) {
menu.style.overflow = "auto";
menu.style.height = `${rect.height - (rect.bottom - document.documentElement.clientHeight)}px`;
}
});
}, 100);
}, [opened]);
function renderItems(items, level) {
const myLevelFilter = filterPerLevel[level];
const showItem = (i) => {
//get rid of empty/null items
let show = !isNullOrUndefined(i) && (isNotEmptyString(i.type) || isNotEmptyString(i.title));
if (show && isNotEmptyString(myLevelFilter)) {
if (i.type === "separator")
show = false;
else if (i.type === "group") {
//only show group if 1 or more results are in it
return i.items.filter(sub => showItem(sub)).length > 0;
}
else
show = i.title.toLowerCase().indexOf(myLevelFilter) >= 0;
}
return show;
};
//inject group items into this level - so we share the filter/next functionality. it looks wierd if filter/paging is done per group if they are displayed inline.
items = items.map(i => i.type === "group" && isNotEmptyArray(i.items) ? [i, ...i.items] : i)
.flat()
//filter empty item or based on text filter
.filter(i => showItem(i));
let menuItems = items.map((item, index) => {
switch (item.type) {
case "group":
//todo: technically group items should be nested inside the group for better screen reder support
return _jsx(MenuGroup, { children: _jsx(MenuGroupHeader, { children: item.title }) }, index);
case "separator":
return _jsx(MenuDivider, {}, index);
case "item":
default:
const openKey = `${level}|${index}`;
const menuItem = _jsx(MenuItem, { icon: item.icon, disabled: item.disabled, onClick: item.onClick, children: item.title }, index);
return isNotEmptyArray(item.items)
? _jsxs(Menu, { mountNode: ctx.mountNode, open: opened[openKey] || false, onOpenChange: (e, data) => {
if (data.open) {
setOpened(Object.assign(Object.assign({}, opened), { [openKey]: true }));
setKeepOpen(Object.assign(Object.assign({}, keepOpen), { [level]: true }));
}
else if (!keepOpen[openKey]) {
setOpened(Object.assign(Object.assign({}, opened), { [openKey]: false }));
setKeepOpen(Object.assign(Object.assign({}, keepOpen), { [level]: false }));
}
}, children: [_jsx(MenuTrigger, { disableButtonEnhancement: true, children: menuItem }), _jsx(MenuPopover, { children: _jsx(MenuList, { children: renderItems(item.items, level + 1) }) })] }, index)
: menuItem;
}
});
const paged = menuItems.length > pageSize;
const filtered = menuItems.length > filterThreshold || !isNullOrEmptyString(myLevelFilter);
if (paged) {
let start = startIndexPerLevel[level];
if (isNullOrUndefined(start))
start = 0;
let hasMore = menuItems.length > start + pageSize;
menuItems = menuItems.slice(start, start + pageSize);
if (start > 0) {
menuItems.splice(0, 0, _jsx(DividerEX, Object.assign({ title: 'Previous' }, clickableDiv, { onClick: () => {
const s = jsonClone(startIndexPerLevel);
s[level] = start - pageSize;
setStartIndexPerLevel(s);
}, children: "previous" }), "$prev"));
}
if (hasMore)
menuItems.push(_jsx(DividerEX, Object.assign({ title: 'Next' }, clickableDiv, { onClick: () => {
const s = jsonClone(startIndexPerLevel);
s[level] = start + pageSize;
setStartIndexPerLevel(s);
}, children: "next" }), "$next"));
}
if (filtered) {
//just filter - no paging
menuItems.splice(0, 0, _jsx(Horizontal, { children: _jsx(Search, { defaultValue: myLevelFilter || "", onChangeDeferred: (newValue) => {
const s = jsonClone(filterPerLevel);
s[level] = newValue ? newValue.toLowerCase() : "";
setFilterPerLevel(s);
} }) }, '$search'));
}
return menuItems;
}
return (_jsxs(Menu, Object.assign({ mountNode: ctx.mountNode }, props.menuProps, { open: opened[0] || false, onOpenChange: (e, data) => {
if (data.open)
setOpened(Object.assign(Object.assign({}, opened), { 0: true }));
else if (!keepOpen[0])
setOpened(Object.assign(Object.assign({}, opened), { 0: false }));
}, children: [_jsx(MenuTrigger, { disableButtonEnhancement: true, children: isString(props.trigger)
? _jsx(ButtonEX, { title: props.trigger, onClick: (e) => {
stopEvent(e);
} })
: isString(props.trigger.title)
? _jsx(ButtonEX, Object.assign({}, props.trigger, { onClick: (e) => {
stopEvent(e);
} }))
: props.trigger }), _jsx(MenuPopover, Object.assign({}, props.menuPopOverProps, { children: _jsx(MenuList, Object.assign({}, props.menuListProps, { children: renderItems(props.items, 0) })) }))] })));
};
//# sourceMappingURL=menu.js.map