UNPKG

@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
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 };