UNPKG

@kwiz/fluentui

Version:

KWIZ common controls for FluentUI

127 lines 7.68 kB
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