@syncfusion/react-navigations
Version:
A package of Pure React navigation components such as Toolbar and Context-menu which is used to navigate from one page to another
411 lines (410 loc) • 21.2 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { useRef, useMemo, useCallback, useEffect, useLayoutEffect, useState, Children, isValidElement, cloneElement, memo, forwardRef, useImperativeHandle } from 'react';
import { ChevronUp, ChevronDown } from '@syncfusion/react-icons';
import { isVisible, closest, useProviderContext } from '@syncfusion/react-base';
import { Popup, CollisionType } from '@syncfusion/react-popups';
import { OverflowMode, Orientation } from './toolbar';
import { ToolbarItem } from './toolbar-item';
import { ToolbarSeparator } from './toolbar-separator';
import { ToolbarSpacer } from './toolbar-spacer';
const CLS_ITEMS = 'sf-toolbar-items';
const CLS_POPUPICON = 'sf-popup-up-icon';
const CLS_POPUPDOWN = 'sf-popup-down-icon';
const CLS_POPUPOPEN = 'sf-popup-open';
const CLS_POPUPNAV = 'sf-hor-nav';
const CLS_TBARNAVACT = 'sf-nav-active';
const CLS_POPUPCLASS = 'sf-toolbar-pop';
const CLS_HIDDEN_POPUP = 'sf-hidden-popup';
const CLS_EXTENDABLE_CLASS = 'sf-toolbar-extended';
const CLS_EXTENDPOPUP = 'sf-extended-nav';
const CLS_OVERFLOW = 'sf-popup-overflow';
const CLS_SEPARATOR = 'sf-separator';
const CLS_SPACER = 'sf-spacer';
const CLS_ITEM = 'sf-toolbar-item';
/**
* ToolbarPopup component that renders the overflowing toolbar items in the popup.
*
* This component manages the display of toolbar items that don't fit in the available space
* by moving them to a popup. It supports both Popup and Extended overflow modes and
* automatically calculates which items should be visible in the toolbar and which should
* be moved to the popup based on available space.
*/
const ToolbarPopup = memo(forwardRef((props, ref) => {
const { toolbarRef, orientation, overflowMode, isToolbarRefReady, children, className, collision, isPopupVisible, onPopupOpenChange, onOverflowChange } = props;
const getItems = useCallback((items) => {
return Children.toArray(items)
.filter((child) => isValidElement(child) && (child.type === ToolbarItem || child.type === ToolbarSeparator || child.type === ToolbarSpacer))
.map((child, index) => cloneElement(child, { key: child.key || index }));
}, []);
const resizeObserverRef = useRef(null);
const previousChildrenCountRef = useRef(Children.count(children));
const itemsRef = useRef(getItems(children));
const toolbarItemsRef = useRef([...itemsRef.current]);
const popupItemsRef = useRef([]);
const popupNavRef = useRef(null);
const popupRef = useRef(null);
const { dir } = useProviderContext();
const [toolbarItems, setToolbarItems] = useState([...toolbarItemsRef.current]);
const [popupItems, setPopupItems] = useState([]);
const [hasInitialRenderCompleted, setHasInitialRenderCompleted] = useState(false);
const [isPopupOpen, setIsPopupOpen] = useState(isPopupVisible);
const [isPopupRefresh, setIsPopupRefresh] = useState(false);
const [popupStyles, setPopupStyles] = useState({ maxHeight: '' });
const getEleWidth = useCallback((item) => {
let width = 0;
if (item) {
width = orientation === Orientation.Vertical ? item.offsetHeight : item.offsetWidth;
}
return width;
}, [orientation]);
const getEleLeft = useCallback((item) => {
let width = 0;
if (item) {
width = orientation === Orientation.Vertical ? item.offsetTop : item.offsetLeft;
}
return width;
}, [orientation]);
const getPopupNavOffset = useCallback(() => {
if (popupNavRef.current) {
return getEleWidth(popupNavRef.current);
}
return orientation === Orientation.Horizontal ? 40 : 48;
}, [getEleWidth, orientation]);
const getElementOffsetY = useCallback(() => {
return toolbarRef.current ? toolbarRef.current.offsetHeight : 0;
}, [overflowMode]);
const getToolbarPopupWidth = useCallback(() => {
let toolbarWidth = 0;
if (toolbarRef.current) {
const computedStyle = window.getComputedStyle(toolbarRef.current);
const width = parseFloat(computedStyle.width);
const borderRightWidth = parseFloat(computedStyle.borderRightWidth);
toolbarWidth = width + (borderRightWidth * 2);
}
return toolbarWidth;
}, []);
const onPopupOpen = useCallback(() => {
if (toolbarRef.current && popupRef.current?.element) {
const popupElement = popupRef.current.element;
const toolbarElement = toolbarRef.current;
const popupElementPos = popupElement.offsetTop + popupElement.offsetHeight +
(toolbarElement.getBoundingClientRect().top || 0);
const scrollVal = window.scrollY || 0;
if (orientation === Orientation.Horizontal && (window.innerHeight + scrollVal) < popupElementPos &&
toolbarElement.offsetTop < popupElement.offsetHeight && overflowMode === OverflowMode.Popup) {
let overflowHeight = (popupElement.offsetHeight - ((popupElementPos - window.innerHeight - scrollVal) + 5));
for (let i = 0; i < popupElement.childElementCount; i++) {
const ele = popupElement.children[parseInt(i.toString(), 10)];
if (ele.offsetTop + ele.offsetHeight > overflowHeight) {
overflowHeight = ele.offsetTop;
break;
}
}
setPopupStyles((prevStyles) => ({ ...prevStyles, maxHeight: `${overflowHeight}px` }));
}
else if (orientation === Orientation.Vertical) {
const tbEleData = toolbarElement.getBoundingClientRect();
setPopupStyles((prevStyles) => ({
...prevStyles,
maxHeight: `${tbEleData.top + toolbarElement.offsetHeight}px`,
bottom: '0'
}));
}
}
}, [overflowMode, orientation]);
const onPopupClose = useCallback(() => {
setIsPopupOpen(false);
setPopupStyles({ maxHeight: '' });
}, []);
const updateOverflowItems = useCallback((itemsElement) => {
if (toolbarRef.current && itemsElement && isVisible(toolbarRef.current)) {
const toolbarItemsData = [...toolbarItems];
const popupItemsData = [...popupItems];
const items = Array.from(itemsElement.children);
const toolbarWidth = getEleWidth(toolbarRef.current);
const computedStyle = window.getComputedStyle(itemsElement);
const padding = parseInt(orientation === Orientation.Vertical ?
computedStyle.paddingTop : computedStyle.paddingLeft, 10);
if (dir === 'rtl' && orientation === Orientation.Horizontal) {
const itemsGap = parseInt(computedStyle.gap, 10) || 0;
const totalItems = items.length;
let totalRequiredWidth = 0;
items.forEach((item) => {
totalRequiredWidth += getEleWidth(item);
});
totalRequiredWidth += (padding * 2) + (itemsGap * (totalItems - 1));
const isOverflow = totalRequiredWidth > toolbarWidth;
const popupNavWidth = popupItemsData.length > 0 || isOverflow ? getPopupNavOffset() : 0;
const availableToolbarWidth = toolbarWidth - popupNavWidth;
if (totalRequiredWidth > availableToolbarWidth) {
let requiredWidth = totalRequiredWidth;
for (let i = totalItems - 1; i >= 0; i--) {
const item = items[parseInt(i.toString(), 10)];
if (requiredWidth > availableToolbarWidth || item.classList.contains(CLS_SEPARATOR)) {
const itemWidth = getEleWidth(item);
requiredWidth -= (itemWidth + ((i === 0) ? 0 : itemsGap));
popupItemsData.unshift(toolbarItemsData[parseInt(i.toString(), 10)]);
toolbarItemsData.pop();
}
else {
break;
}
}
}
}
else {
let itemWidth = getEleWidth(items[items.length - 1]);
let itemLeft = getEleLeft(items[items.length - 1]);
const isOverflow = itemLeft + itemWidth + padding > toolbarWidth;
const popupNavWidth = popupItemsData.length > 0 || isOverflow ? getPopupNavOffset() : 0;
const availableWidth = toolbarWidth - popupNavWidth;
for (let i = items.length - 1; i >= 0; i--) {
const item = items[parseInt(i.toString(), 10)];
itemWidth = getEleWidth(item);
itemLeft = getEleLeft(item);
const isItemOverflow = (itemLeft + itemWidth + padding > availableWidth);
if (isItemOverflow || item.classList.contains(CLS_SEPARATOR)) {
popupItemsData.unshift(toolbarItemsData[parseInt(i.toString(), 10)]);
toolbarItemsData.pop();
}
else {
break;
}
}
}
toolbarItemsRef.current = toolbarItemsData;
popupItemsRef.current = popupItemsData;
}
}, [orientation, getEleWidth, getEleLeft, getPopupNavOffset, toolbarItems, popupItems, dir]);
const setItems = useCallback(() => {
if (toolbarItems !== toolbarItemsRef.current) {
setToolbarItems((prev) => prev.length !== toolbarItemsRef.current.length ?
[...toolbarItemsRef.current] : prev);
setPopupItems((prev) => prev.length !== popupItemsRef.current.length ? [...popupItemsRef.current] : prev);
}
}, [toolbarItems]);
const resize = useCallback(() => {
if (toolbarRef.current && !isPopupRefresh) {
const toolbarItems = toolbarRef.current.querySelector(`.${CLS_ITEMS}`);
if (overflowMode === OverflowMode.Popup) {
setIsPopupOpen((prevState) => prevState ? false : prevState);
}
updateOverflowItems(toolbarItems);
setItems();
setIsPopupRefresh(popupItemsRef.current.length > 0 ? true : false);
}
}, [updateOverflowItems, isPopupRefresh, setItems, overflowMode]);
const resizeRef = useRef(resize);
useLayoutEffect(() => {
resizeRef.current = resize;
}, [resize]);
const getItemsWidth = useCallback((itemsElement) => {
let totalWidth = 0;
if (itemsElement) {
const items = Array.from(itemsElement.children);
const computedStyle = window.getComputedStyle(itemsElement);
const itemsGap = parseInt(computedStyle.gap, 10) || 0;
const itemPadding = parseInt((orientation === Orientation.Vertical ? computedStyle.paddingTop : computedStyle.paddingLeft), 10);
items.forEach((item) => {
totalWidth += (item.classList.contains(CLS_SPACER) ? 0 : getEleWidth(item)) + itemsGap;
});
if (items.length > 0) {
totalWidth -= itemsGap;
}
totalWidth += (itemPadding * 2);
}
return totalWidth;
}, [getEleWidth, orientation]);
const popupEleRefresh = useCallback((availableSpace, popup) => {
const popupItemElements = [].slice.call(popup.querySelectorAll(`.${CLS_ITEM}`));
const toolbarItemsData = [...toolbarItems];
const popupItemsData = [...popupItems];
const itemsElement = toolbarRef.current?.querySelector(`.${CLS_ITEMS}`);
const itemsGap = parseInt(window.getComputedStyle(itemsElement).gap, 10) || 0;
for (let i = 0; i < popupItemElements.length; i++) {
const item = popupItemElements[parseInt(i.toString(), 10)];
const isSpacer = item.classList.contains(CLS_SPACER);
const hasToolbarItems = toolbarItemsData.length > 0;
const itemWidth = (isSpacer ? 0 : getEleWidth(item)) + (hasToolbarItems ? itemsGap : 0);
let isSpaceAvailable = availableSpace > itemWidth;
if (isSpacer || item.classList.contains(CLS_SEPARATOR)) {
let nextItemWidth = itemWidth;
let j = i + 1;
while (j < popupItemElements.length) {
const nextItem = popupItemElements[parseInt(j.toString(), 10)];
const isNextSpacer = nextItem.classList.contains(CLS_SPACER);
nextItemWidth += (isNextSpacer ? 0 : getEleWidth(nextItem)) + (hasToolbarItems ? itemsGap : 0);
if (!nextItem.classList.contains(CLS_SPACER) && !nextItem.classList.contains(CLS_SEPARATOR)) {
break;
}
j++;
}
isSpaceAvailable = availableSpace > nextItemWidth;
}
if (isSpaceAvailable) {
toolbarItemsData.push(popupItemsData[0]);
popupItemsData.shift();
availableSpace -= itemWidth;
}
else {
break;
}
}
toolbarItemsRef.current = toolbarItemsData;
popupItemsRef.current = popupItemsData;
}, [getEleWidth, toolbarItems, popupItems]);
const popupRefresh = useCallback(() => {
if (toolbarRef.current && popupRef.current?.element) {
const itemsElement = toolbarRef.current.querySelector(`.${CLS_ITEMS}`);
const itemsElementWidth = getEleWidth(itemsElement);
const itemsWidth = getItemsWidth(itemsElement);
const availableSpace = itemsElementWidth - itemsWidth;
const itemsGap = parseInt(window.getComputedStyle(itemsElement).gap, 10) || 0;
const popupItemWidth = getEleWidth(popupRef.current.element.querySelector(`.${CLS_ITEM}`)) + itemsGap;
const isSpaceAvailable = availableSpace > popupItemWidth;
const popupElements = [].slice.call(popupRef.current.element.querySelectorAll(`.${CLS_ITEM}`));
let popupItemsWidth = 0;
popupElements.forEach((item) => {
popupItemsWidth += item.classList.contains(CLS_SPACER) ? 0 : getEleWidth(item);
});
popupItemsWidth += (popupElements.length * itemsGap);
const isResetToDefault = (availableSpace + getPopupNavOffset()) > popupItemsWidth;
if (isResetToDefault) {
toolbarItemsRef.current = [...itemsRef.current];
popupItemsRef.current = [];
}
else if (isSpaceAvailable) {
popupEleRefresh(availableSpace, popupRef.current.element);
}
}
}, [getEleWidth, getItemsWidth, popupEleRefresh]);
const onPopupClick = useCallback(() => {
if (overflowMode === OverflowMode.Popup && isPopupOpen) {
setIsPopupOpen(false);
}
}, [overflowMode, isPopupOpen]);
const onPopupNavClick = () => {
setIsPopupOpen((prev) => !prev);
};
const refreshOverflow = useCallback(() => {
resize();
}, [resize]);
useEffect(() => {
if (overflowMode === OverflowMode.Popup || overflowMode === OverflowMode.Extended) {
const closePopup = (event) => {
if (overflowMode === OverflowMode.Popup && popupRef.current && popupRef.current.element) {
const isNotPopup = !closest(event.target, '.sf-popup');
const isOpen = popupRef.current.element.classList.contains(CLS_POPUPOPEN);
if (isNotPopup && isOpen) {
setIsPopupOpen(false);
}
}
};
document.addEventListener('click', closePopup);
document.addEventListener('scroll', closePopup, true);
return () => {
document.removeEventListener('click', closePopup);
document.removeEventListener('scroll', closePopup, true);
};
}
return undefined;
}, [overflowMode]);
useEffect(() => {
onPopupOpenChange(isPopupOpen);
}, [isPopupOpen, onPopupOpenChange]);
useEffect(() => {
setIsPopupOpen((prev) => prev !== isPopupVisible ? isPopupVisible : prev);
}, [isPopupVisible]);
useEffect(() => {
onOverflowChange();
}, [toolbarItems, onOverflowChange]);
useLayoutEffect(() => {
if (toolbarRef.current && isToolbarRefReady && !hasInitialRenderCompleted) {
resizeRef.current();
setHasInitialRenderCompleted(true);
}
}, [isToolbarRefReady, hasInitialRenderCompleted]);
useEffect(() => {
if (toolbarRef.current) {
let isFirstObservation = true;
if (!resizeObserverRef.current) {
resizeObserverRef.current = new ResizeObserver(() => {
if (isFirstObservation) {
isFirstObservation = false;
return;
}
resizeRef.current();
});
resizeObserverRef.current.observe(toolbarRef.current);
}
return () => {
resizeObserverRef.current?.disconnect();
resizeObserverRef.current = null;
};
}
return undefined;
}, []);
useLayoutEffect(() => {
if (isPopupRefresh) {
popupRefresh();
setItems();
setIsPopupOpen((prev) => prev && popupItemsRef.current.length <= 0 ? false : prev);
setIsPopupRefresh(false);
}
}, [isPopupRefresh, popupRefresh, setItems]);
useLayoutEffect(() => {
const currentCount = Children.count(children);
if (hasInitialRenderCompleted && previousChildrenCountRef.current !== currentCount) {
previousChildrenCountRef.current = Children.count(children);
itemsRef.current = getItems(children);
toolbarItemsRef.current = [...itemsRef.current];
popupItemsRef.current = [];
setToolbarItems([...itemsRef.current]);
setPopupItems([]);
setIsPopupOpen(false);
setHasInitialRenderCompleted(false);
}
}, [children, hasInitialRenderCompleted, getItems]);
useImperativeHandle(ref, () => ({
refreshOverflow
}), [refreshOverflow]);
const classes = useMemo(() => {
const classArray = [CLS_ITEMS];
if (popupItems.length > 0) {
classArray.push(CLS_OVERFLOW);
}
return classArray.join(' ');
}, [popupItems]);
const popupNavClasses = useMemo(() => {
const classArray = [CLS_POPUPNAV];
if (overflowMode === OverflowMode.Extended) {
classArray.push(CLS_EXTENDPOPUP);
}
if (isPopupOpen) {
classArray.push(CLS_TBARNAVACT);
}
return classArray.join(' ');
}, [overflowMode, isPopupOpen]);
const popupClasses = useMemo(() => {
const classArray = [CLS_POPUPCLASS];
if (overflowMode === OverflowMode.Extended) {
classArray.push(CLS_EXTENDABLE_CLASS);
}
if (className) {
classArray.push(className);
}
if (isPopupRefresh) {
classArray.push(CLS_HIDDEN_POPUP);
}
return classArray.join(' ');
}, [overflowMode, className, isPopupRefresh]);
return (_jsxs(_Fragment, { children: [_jsx("div", { className: classes, children: toolbarItems }), (popupItems.length > 0) &&
_jsx("div", { ref: popupNavRef, className: popupNavClasses, tabIndex: 0, role: 'button', "aria-haspopup": 'true', "aria-label": 'overflow', "aria-expanded": isPopupOpen ? 'true' : 'false', onClick: onPopupNavClick, children: _jsx("div", { className: `${isPopupOpen ? CLS_POPUPICON : CLS_POPUPDOWN} sf-icons`, children: isPopupOpen ? _jsx(ChevronUp, {}) : _jsx(ChevronDown, {}) }) }), popupItems.length > 0 &&
_jsx(Popup, { ref: popupRef, className: popupClasses, relateTo: toolbarRef.current, offsetY: orientation === Orientation.Vertical ? 0 : getElementOffsetY(), isOpen: isPopupOpen, onOpen: onPopupOpen, onClose: onPopupClose, collision: { Y: collision ? CollisionType.None : CollisionType.None, X: CollisionType.None }, position: dir === 'rtl' ? { X: 'left', Y: 'top' } : { X: 'right', Y: 'top' }, width: overflowMode === OverflowMode.Extended && orientation === Orientation.Horizontal ?
getToolbarPopupWidth() : undefined, offsetX: overflowMode === OverflowMode.Extended && orientation === Orientation.Horizontal ? 0 : undefined, animation: {
show: { name: 'FadeIn', duration: 100 },
hide: { name: 'FadeOut', duration: 100 }
}, style: popupStyles, onClick: onPopupClick, children: popupItems })] }));
}));
ToolbarPopup.displayName = 'ToolbarPopup';
export { ToolbarPopup };