Shared:ContextMenu:Added scroll to all levels of the context menu. The structure of all context menu views has been changed and nesting has been added.

This commit is contained in:
Vlada Gazizova 2024-05-23 12:19:05 +03:00
parent cb8a1233f0
commit 64233c9342
3 changed files with 194 additions and 155 deletions

View File

@ -60,9 +60,11 @@ const ContextMenu = React.forwardRef((props: ContextMenuProps, ref) => {
const [model, setModel] = React.useState<ContextMenuModel[] | null>(null);
const [changeView, setChangeView] = React.useState(false);
const [showMobileMenu, setShowMobileMenu] = React.useState(false);
const [onLoad, setOnLoad] = React.useState<
undefined | (() => Promise<ContextMenuModel[]>)
const [mobileSubMenuItems, setMobileSubMenuItems] = React.useState<
ContextMenuModel[] | undefined
>(undefined);
const [mobileHeader, setMobileHeader] = React.useState<string>("");
const [articleWidth, setArticleWidth] = React.useState(0);
const prevReshow = React.useRef(false);
@ -204,7 +206,7 @@ const ContextMenu = React.forwardRef((props: ContextMenuProps, ref) => {
? rects.left - currentLeftOffset - currentRightOffset
: event.pageX + 1;
let top = rects ? rects.top : event.pageY + 1;
const width =
let width =
menuRef.current && menuRef.current.offsetParent
? menuRef.current.offsetWidth
: DomHelpers.getHiddenElementOuterWidth(menuRef.current);
@ -214,29 +216,46 @@ const ContextMenu = React.forwardRef((props: ContextMenuProps, ref) => {
: DomHelpers.getHiddenElementOuterHeight(menuRef.current);
const viewport = DomHelpers.getViewport();
const mobileView = isMobileUtils() && (height > 210 || ignoreChangeView);
if (!mobileView) {
const options = menuRef?.current?.getElementsByClassName("p-menuitem");
const optionsWidth: number[] = [];
if (options) {
Array.from(options).forEach((option) =>
optionsWidth.push(option.clientWidth),
);
const widthMaxContent = Math.max(...optionsWidth);
width = width || widthMaxContent;
}
}
if (theme.interfaceDirection === "rtl" && !rects && left > width) {
left = event.pageX - width + 1;
}
if (
isTabletUtils() &&
(height > 483 ||
(isMobileOnly && window.innerHeight < window.innerWidth))
) {
const article = document.getElementById("article-container");
// if (
// isTabletUtils() &&
// (height > 483 ||
// (isMobileOnly && window.innerHeight < window.innerWidth))
// ) {
// const article = document.getElementById("article-container");
let currentArticleWidth = 0;
if (article) {
currentArticleWidth = article.offsetWidth;
}
// let currentArticleWidth = 0;
// if (article) {
// currentArticleWidth = article.offsetWidth;
// }
setChangeView(true);
setArticleWidth(currentArticleWidth);
// setChangeView(true);
// setArticleWidth(currentArticleWidth);
return;
}
// return;
// }
if (isMobileUtils() && (height > 210 || ignoreChangeView)) {
if (mobileView) {
setChangeView(true);
setArticleWidth(0);
@ -260,7 +279,10 @@ const ContextMenu = React.forwardRef((props: ContextMenuProps, ref) => {
// fit
if (top < document.body.scrollTop) {
top = document.body.scrollTop;
const marginTop = 16;
if (document.body.scrollTop === 0) top = marginTop;
else top = document.body.scrollTop;
}
if (containerRef) {
@ -276,6 +298,8 @@ const ContextMenu = React.forwardRef((props: ContextMenuProps, ref) => {
if (menuRef.current) {
menuRef.current.style.left = `${left}px`;
menuRef.current.style.top = `${top}px`;
if (!mobileView) menuRef.current.style.width = `${width}px`;
}
}
};
@ -399,14 +423,20 @@ const ContextMenu = React.forwardRef((props: ContextMenuProps, ref) => {
};
}, [documentResizeListener, onHide, visible]);
const onMobileItemClick = (
const onMobileItemClick = async (
e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>,
label: string,
items?: ContextMenuModel[],
loadFunc?: () => Promise<ContextMenuModel[]>,
) => {
e.stopPropagation();
setShowMobileMenu(true);
if (loadFunc) setOnLoad(loadFunc);
const res = loadFunc ? await loadFunc() : items;
setMobileSubMenuItems(res);
setMobileHeader(label);
};
const onBackClick = (e: React.MouseEvent<HTMLDivElement>) => {
@ -506,7 +536,7 @@ const ContextMenu = React.forwardRef((props: ContextMenuProps, ref) => {
)}
<Text className="text" truncate dir="auto">
{header.title}
{showMobileMenu ? mobileHeader : header.title}
</Text>
</div>
)}
@ -516,7 +546,7 @@ const ContextMenu = React.forwardRef((props: ContextMenuProps, ref) => {
root
resetMenu={resetMenu}
onLeafClick={onLeafClick}
onLoad={onLoad}
mobileSubMenuItems={mobileSubMenuItems}
/>
) : (
<SubMenu
@ -524,7 +554,6 @@ const ContextMenu = React.forwardRef((props: ContextMenuProps, ref) => {
root
resetMenu={resetMenu}
onLeafClick={onLeafClick}
changeView={changeView}
onMobileItemClick={onMobileItemClick}
/>
)}

View File

@ -44,9 +44,9 @@ const MobileSubMenu = (props: {
onLeafClick: (e: React.MouseEvent) => void;
root?: boolean;
resetMenu: boolean;
onLoad?: () => Promise<ContextMenuModel[]>;
mobileSubMenuItems?: ContextMenuModel[];
}) => {
const { onLeafClick, root, resetMenu, onLoad } = props;
const { onLeafClick, root, resetMenu, mobileSubMenuItems } = props;
const [submenu, setSubmenu] = useState<null | ContextMenuModel[]>(null);
@ -91,16 +91,12 @@ const MobileSubMenu = (props: {
}
});
const fetchSubMenu = React.useCallback(async () => {
const res = await onLoad?.();
if (res) setSubmenu(res);
position();
}, [position, setSubmenu, onLoad]);
useEffect(() => {
if (onLoad) fetchSubMenu();
}, [onLoad, fetchSubMenu]);
if (!mobileSubMenuItems?.length) return;
setSubmenu(mobileSubMenuItems);
position();
}, [mobileSubMenuItems, mobileSubMenuItems?.length, position]);
const onItemClick = (e: React.MouseEvent, item: ContextMenuType) => {
const { disabled, url, onClick, items, action } = item;

View File

@ -32,21 +32,14 @@ import { useTheme } from "styled-components";
import ArrowIcon from "PUBLIC_DIR/images/arrow.right.react.svg";
import OutsdideIcon from "PUBLIC_DIR/images/arrow.outside.react.svg";
import { isMobile as isMobileDevice } from "react-device-detect";
import {
classNames,
ObjectUtils,
DomHelpers,
isMobile,
isTablet,
} from "../../../utils";
import { classNames, ObjectUtils, DomHelpers, isMobile } from "../../../utils";
import { ContextMenuSkeleton } from "../../../skeletons/context-menu";
import { Scrollbar } from "../../scrollbar";
import { ToggleButton } from "../../toggle-button";
import { Scrollbar } from "../../scrollbar";
import { SubMenuItem } from "../ContextMenu.styled";
import { SubMenuItem, StyledList } from "../ContextMenu.styled";
import {
ContextMenuModel,
ContextMenuType,
@ -63,19 +56,13 @@ const SubMenu = (props: {
) => void;
onMobileItemClick?: (
e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>,
loadFunc: () => Promise<ContextMenuModel[]>,
label: string,
items?: ContextMenuModel[],
loadFunc?: () => Promise<ContextMenuModel[]>,
) => void;
changeView?: boolean;
onLoad?: () => Promise<ContextMenuModel[]>;
}) => {
const {
onLeafClick,
root,
resetMenu,
changeView,
onMobileItemClick,
onLoad,
} = props;
const { onLeafClick, root, resetMenu, onMobileItemClick, onLoad } = props;
const [model, setModel] = useState(props?.model);
const [isLoading, setIsLoading] = useState(false);
@ -86,7 +73,7 @@ const SubMenu = (props: {
const theme = useTheme();
const onItemMouseEnter = (e: React.MouseEvent, item: ContextMenuType) => {
if (item.disabled || isMobileDevice) {
if (item.disabled) {
e.preventDefault();
return;
}
@ -98,16 +85,18 @@ const SubMenu = (props: {
e: React.MouseEvent | React.ChangeEvent<HTMLInputElement>,
item: ContextMenuType,
) => {
if (item.onLoad) {
e.preventDefault();
if (!isMobile() && !isTablet()) return;
const { disabled, url, onClick, items, action, label } = item;
if (isMobile() || isTablet()) onMobileItemClick?.(e, item.onLoad);
else onLeafClick?.(e);
if (label && (items || item.onLoad)) {
e.preventDefault();
if (!isMobile()) return;
if (items) onMobileItemClick?.(e, label as string, items, undefined);
else if (item.onLoad)
onMobileItemClick?.(e, label as string, undefined, item.onLoad);
return;
}
const { disabled, url, onClick, items, action } = item;
if (disabled) {
e.preventDefault();
return;
@ -134,7 +123,9 @@ const SubMenu = (props: {
const containerOffset = DomHelpers.getOffset(parentItem);
const viewport = DomHelpers.getViewport();
const subListWidth = subMenuRef.current?.offsetParent
const options = subMenuRef.current?.getElementsByClassName("p-menuitem");
let subListWidth = subMenuRef.current?.offsetParent
? subMenuRef.current.offsetWidth
: DomHelpers.getHiddenElementOuterWidth(subMenuRef.current);
@ -144,16 +135,49 @@ const SubMenu = (props: {
const isRtl = theme.interfaceDirection === "rtl";
if (!isMobile() && options) {
const optionsWidth: number[] = [];
Array.from(options).forEach((option) =>
optionsWidth.push(Math.ceil(option.getBoundingClientRect().width)),
);
const widthMaxContent = Math.max(...optionsWidth);
subListWidth = subListWidth || widthMaxContent;
}
if (subMenuRef.current) {
subMenuRef.current.style.top = "0px";
let subMenuRefTop = null;
if (!isMobile()) subMenuRef.current.style.width = `${subListWidth}px`;
if (!isMobile() && !root) {
const firstList = parentItem?.firstChild as HTMLElement;
const menuItemActive = firstList.querySelector(
".p-menuitem-active",
) as HTMLElement;
const top = menuItemActive.offsetTop;
const scroller = firstList.querySelector(".scroller") as HTMLElement;
const scrollTop = scroller.scrollTop;
const positionActiveItem = top - scrollTop;
subMenuRefTop = positionActiveItem - 2;
subMenuRef.current.style.top = `${subMenuRefTop}px`;
}
const submenuRects = subMenuRef.current.getBoundingClientRect();
if (submenuRects.bottom > viewport.height) {
if (submenuRects.bottom > viewport.height && subMenuRefTop) {
const submenuMargin = 16;
const topOffset = submenuRects.bottom - viewport.height + submenuMargin;
subMenuRef.current.style.top = `${-1 * topOffset}px`;
const topOffset =
subMenuRefTop -
(submenuRects.bottom - viewport.height) -
submenuMargin;
subMenuRef.current.style.top = `${topOffset}px`;
}
if (isRtl) {
@ -205,29 +229,6 @@ const SubMenu = (props: {
/>
);
const renderSubMenu = (item: ContextMenuType) => {
const loaderItem = {
id: "link-loader-option",
key: "link-loader",
isLoader: true,
label: <ContextMenuSkeleton />,
};
if (item.items || item.onLoad) {
return (
<SubMenu
model={item.onLoad ? [loaderItem] : item.items || []}
resetMenu={item !== activeItem}
onLeafClick={onLeafClick}
// onEnter={onEnter}
onLoad={item.onLoad}
/>
);
}
return null;
};
const renderMenuitem = (
item: ContextMenuType,
index: number,
@ -273,7 +274,7 @@ const SubMenu = (props: {
const subMenuIcon = (item.items || item.onLoad) && (
<ArrowIcon className={subMenuIconClassName} />
);
const subMenu = renderSubMenu(item);
const dataKeys = Object.fromEntries(
Object.entries(item).filter((el) => el[0].indexOf("data-") === 0),
);
@ -330,7 +331,6 @@ const SubMenu = (props: {
onMouseEnter={(e) => onItemMouseEnter(e, item)}
>
{content}
{subMenu}
<ToggleButton
isChecked={item.checked || false}
onChange={onClick}
@ -350,7 +350,6 @@ const SubMenu = (props: {
onMouseEnter={(e) => onItemMouseEnter(e, item)}
>
{content}
{subMenu}
</li>
);
};
@ -389,78 +388,93 @@ const SubMenu = (props: {
};
const renderMenu = () => {
if (model) {
if (changeView) {
const newModel = model.filter(
(item: ContextMenuModel) => item && !item.disabled,
if (!model) return null;
return model.map((item: ContextMenuModel, index: number) => {
if (item?.disabled) return null;
return renderItem(item, index);
});
};
const renderSubMenuLower = () => {
if (!model) return null;
const submenu: JSX.Element[] = [];
const loaderItem = {
id: "link-loader-option",
key: "link-loader",
isLoader: true,
label: <ContextMenuSkeleton />,
};
model.forEach((item) => {
const contextMenuTypeItem = item as ContextMenuType;
if (contextMenuTypeItem?.items || contextMenuTypeItem?.onLoad) {
submenu.push(
<SubMenu
key={`sub-menu_${item.id}`}
model={
contextMenuTypeItem?.onLoad
? [loaderItem]
: contextMenuTypeItem?.items || []
}
resetMenu={item !== activeItem}
onLeafClick={onLeafClick}
onLoad={contextMenuTypeItem?.onLoad}
/>,
);
const rowHeights: number[] = newModel.map((item: ContextMenuModel) => {
if (!item) return 0;
if (item.isSeparator) return 13;
return 36;
});
// const getItemSize = (index) => rowHeights[index];
const height = rowHeights.reduce((a, b) => a + b);
const viewport = DomHelpers.getViewport();
const listHeight =
height + 61 > viewport.height - 64
? viewport.height - 125
: height + 5;
return (
<Scrollbar style={{ height: listHeight }}>
{model.map((item: ContextMenuModel, index: number) => {
if (!item || item?.disabled) return null;
return renderItem(item, index);
})}
</Scrollbar>
);
// return (
// <VariableSizeList
// height={listHeight}
// width={"auto"}
// itemCount={newModel.length}
// itemSize={getItemSize}
// itemData={newModel}
// outerElementType={CustomScrollbarsVirtualList}
// >
// {renderItem}
// </VariableSizeList>
// );
}
});
return model.map((item: ContextMenuModel, index: number) => {
if (item?.disabled) return null;
return renderItem(item, index);
});
}
return null;
return submenu;
};
const className = classNames({ "p-submenu-list": !root });
const submenu = renderMenu();
const active = isActive();
const submenuLower = renderSubMenuLower();
return (
<CSSTransition
nodeRef={subMenuRef}
classNames="p-contextmenusub"
in={active}
timeout={{ enter: 0, exit: 0 }}
unmountOnExit
onEnter={onEnter}
>
<ul ref={subMenuRef} className={`${className} not-selectable`}>
{submenu}
</ul>
</CSSTransition>
const newModel = model.filter(
(item: ContextMenuModel) => item && !item.disabled,
);
const rowHeights: number[] = newModel.map((item: ContextMenuModel) => {
if (!item) return 0;
if (item.isSeparator) return 13;
return 36;
});
const height = rowHeights.reduce((a, b) => a + b);
const viewport = DomHelpers.getViewport();
const paddingList = 12;
const marginsList = 32;
const listHeight =
height + paddingList + marginsList > viewport.height
? viewport.height - marginsList
: height + paddingList;
if (model) {
return (
<CSSTransition
nodeRef={subMenuRef}
classNames="p-contextmenusub"
in={active}
timeout={{ enter: 0, exit: 0 }}
unmountOnExit
onEnter={onEnter}
>
<StyledList
ref={subMenuRef}
className={`${className} not-selectable`}
listHeight={height + paddingList}
>
<Scrollbar style={{ height: listHeight }}>{submenu}</Scrollbar>
{submenuLower}
</StyledList>
</CSSTransition>
);
}
};
export { SubMenu };