UNPKG

@syncfusion/react-navigations

Version:

Syncfusion React Navigations with Toolbar and Context Menu for seamless page navigation

340 lines (339 loc) 15.7 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { forwardRef, useImperativeHandle, useRef, useLayoutEffect, useState, useMemo, memo, useCallback } from 'react'; import { preRender, useProviderContext, closest, isNullOrUndefined, Orientation } from '@syncfusion/react-base'; import { ToolbarMultiRow } from './toolbar-multi-row'; import { ToolbarScrollable } from './toolbar-scrollable'; import { ToolbarPopup } from './toolbar-popup'; export { Orientation }; /** * Specifies the options of Toolbar display mode. Display option is considered when Toolbar content exceeds the available space. */ export var OverflowMode; (function (OverflowMode) { /** * All the elements are displayed in a single line with horizontal or vertical scrolling enabled. */ OverflowMode["Scrollable"] = "Scrollable"; /** * Overflowing elements are moved to the popup. Shows the overflowing Toolbar items when you click the expand button. If the popup content overflows the height of the page, the rest of the elements will be hidden. */ OverflowMode["Popup"] = "Popup"; /** * Displays the overflowing Toolbar items in multiple rows within the Toolbar, allowing all items to remain visible by wrapping to new lines as needed. */ OverflowMode["MultiRow"] = "MultiRow"; /** * Hides the overflowing Toolbar items in the next row. Shows the overflowing Toolbar items when you click the expand button. */ OverflowMode["Extended"] = "Extended"; })(OverflowMode || (OverflowMode = {})); const CLS_TOOLBAR = 'sf-toolbar'; const CLS_VERTICAL = 'sf-toolbar-vertical'; const CLS_ITEMS = 'sf-toolbar-items'; const CLS_RTL = 'sf-rtl'; const CLS_TBARSCRLNAV = 'sf-hscroll-nav'; const CLS_POPUPNAV = 'sf-toolbar-hor-nav'; const CLS_POPUPCLASS = 'sf-toolbar-popup-items'; const CLS_EXTENDABLE_TOOLBAR = 'sf-toolbar-extended'; const CLS_MULTIROW_TOOLBAR = 'sf-toolbar-multirow'; const CLS_EXTENDEDPOPOPEN = 'sf-tbar-extended'; const CLS_POPUP_TOOLBAR = 'sf-toolbar-popup'; /** * The Toolbar component helps users to effectively organize and quickly access frequently used actions. * It provides multiple overflow handling modes to accommodate different UI requirements and screen sizes. * * ```typescript * import { Toolbar, ToolbarItem, ToolbarSeparator, ToolbarSpacer, OverflowMode } from "@syncfusion/react-navigations"; * * <Toolbar overflowMode={OverflowMode.Popup} style={{ width: '300px' }}> * <ToolbarItem><Button>Cut</Button></ToolbarItem> * <ToolbarItem><Button>Copy</Button></ToolbarItem> * <ToolbarSeparator /> * <ToolbarItem><Button>Paste</Button></ToolbarItem> * <ToolbarSpacer /> * <ToolbarItem><Button>Help</Button></ToolbarItem> * </Toolbar> * ``` */ export const Toolbar = memo(forwardRef((props, ref) => { const { keyboardNavigation = true, children, className = '', collision = true, orientation = Orientation.Horizontal, overflowMode = OverflowMode.Scrollable, scrollStep = undefined, ...eleAttr } = props; const toolbarRef = useRef(null); const focusItemsRef = useRef({ toolbar: [], popup: [] }); const focusItemRef = useRef(null); const scrollableRef = useRef(null); const popupRef = useRef(null); const { dir } = useProviderContext(); const [isToolbarRefReady, setIsToolbarRefReady] = useState(false); const [isPopupVisible, setIsPopupVisible] = useState(false); const isItemDisabled = useCallback((item) => { return item.hasAttribute('disabled') || item.getAttribute('aria-disabled') === 'true' || item.classList.contains('sf-disabled'); }, []); const findNextEnabledItem = useCallback((items, startIndex, direction) => { if (!items.length) { return -1; } const itemCount = items.length; let index = startIndex; let loopCount = 0; while (loopCount < itemCount) { index = (index + direction + itemCount) % itemCount; if (!isItemDisabled(items[index])) { return index; } loopCount++; } return -1; }, [isItemDisabled]); const onOverflowChange = useCallback(() => { if (keyboardNavigation && toolbarRef.current) { const queryElements = (containerClass) => { return toolbarRef.current ? Array.from(toolbarRef.current.querySelectorAll(`.${containerClass} .sf-btn`)) : []; }; focusItemsRef.current.toolbar = queryElements(CLS_ITEMS); if (overflowMode === OverflowMode.Extended || overflowMode === OverflowMode.Popup) { focusItemsRef.current.popup = queryElements(CLS_POPUPCLASS); } let focusItemIndex = 0; if (focusItemRef.current) { focusItemIndex = focusItemsRef.current.toolbar.indexOf(focusItemRef.current); focusItemIndex = focusItemIndex === -1 ? 0 : focusItemIndex; } const allItems = [...focusItemsRef.current.toolbar, ...focusItemsRef.current.popup]; if (focusItemIndex >= 0 && focusItemIndex < allItems.length && isItemDisabled(allItems[focusItemIndex])) { focusItemIndex = findNextEnabledItem(allItems, -1, 1); } allItems.forEach((item, i) => { item.tabIndex = i === focusItemIndex ? 0 : -1; if (i === focusItemIndex) { focusItemRef.current = item; } }); } }, [keyboardNavigation, overflowMode, isItemDisabled, findNextEnabledItem]); const getElementContext = useCallback((target) => { const isInPopup = !!closest(target, '.' + CLS_POPUPCLASS); return { isToolbar: !isInPopup, items: isInPopup ? focusItemsRef.current.popup : focusItemsRef.current.toolbar, currentIndex: isInPopup ? focusItemsRef.current.popup.indexOf(target) : focusItemsRef.current.toolbar.indexOf(target) }; }, []); const isInPopup = useCallback((target) => { return !isNullOrUndefined(closest(target, '.' + CLS_POPUPCLASS)); }, []); const isPopupNavElement = useCallback((target) => { return !isNullOrUndefined(closest(target, '.' + CLS_POPUPNAV)); }, []); const manageFocus = useCallback((newIndex, isToolbar) => { const items = isToolbar ? focusItemsRef.current.toolbar : focusItemsRef.current.popup; if (newIndex > -1 && newIndex < items.length) { items.forEach((item) => { item.tabIndex = -1; }); const item = items[parseInt(newIndex.toString(), 10)]; item.tabIndex = 0; item.focus(); if (isToolbar) { focusItemRef.current = item; } } }, []); const navigateItems = useCallback((target, direction) => { const { isToolbar, items, currentIndex } = getElementContext(target); if (!items.length) { return; } let newIndex; switch (direction) { case 'next': newIndex = findNextEnabledItem(items, currentIndex, 1); break; case 'previous': newIndex = findNextEnabledItem(items, currentIndex, -1); break; case 'first': newIndex = findNextEnabledItem(items, -1, 1); break; case 'last': newIndex = findNextEnabledItem(items, items.length, -1); break; } if (newIndex !== -1) { manageFocus(newIndex, isToolbar); } }, [getElementContext, manageFocus, findNextEnabledItem]); const handleHorizontalNavigation = useCallback((target, key) => { if (orientation === Orientation.Horizontal) { const isNavButton = target.classList.contains(CLS_POPUPNAV); const isScrollButton = target.classList.contains(CLS_TBARSCRLNAV); if (key === 'ArrowRight' && toolbarRef.current === target) { manageFocus(0, true); return; } if ((key === 'ArrowRight' || key === 'ArrowLeft') && !isNavButton && !isScrollButton) { const direction = key === 'ArrowRight' ? 'next' : 'previous'; navigateItems(target, direction); } } }, [manageFocus, navigateItems, orientation]); const handleVerticalNavigation = useCallback((target, key) => { const isVerticalMode = orientation === Orientation.Vertical; const inPopup = isInPopup(target); const isPopupNav = isPopupNavElement(target); if (key === 'ArrowUp' && !isPopupNav && (isVerticalMode || inPopup)) { navigateItems(target, 'previous'); } else if (key === 'ArrowDown') { if (isPopupNav && isPopupVisible && focusItemsRef.current.popup.length > 0) { manageFocus(0, false); } else if (isVerticalMode || inPopup) { navigateItems(target, 'next'); } } }, [isInPopup, isPopupNavElement, isPopupVisible, manageFocus, navigateItems, orientation]); const handleEdgeNavigation = useCallback((target, key) => { const direction = key === 'Home' ? 'first' : 'last'; navigateItems(target, direction); }, [navigateItems]); const handleTabNavigation = useCallback((target) => { const isNavButton = target.classList.contains(CLS_POPUPNAV); const isScrollButton = target.classList.contains(CLS_TBARSCRLNAV); const toolbarElement = toolbarRef.current; if (!isScrollButton && !isNavButton && toolbarElement === target && focusItemsRef.current.toolbar) { if (focusItemRef.current && !isItemDisabled(focusItemRef.current)) { focusItemRef.current.focus(); } else { const firstEnabledIndex = findNextEnabledItem(focusItemsRef.current.toolbar, -1, 1); if (firstEnabledIndex !== -1) { manageFocus(firstEnabledIndex, true); } } } }, [manageFocus, isItemDisabled, findNextEnabledItem]); const handleEnterKey = useCallback((target) => { const isNavButton = target.classList.contains(CLS_POPUPNAV); if (isNavButton) { setIsPopupVisible((prevState) => !prevState); } }, []); const handleEscapeKey = useCallback(() => { if (overflowMode === OverflowMode.Popup && isPopupVisible) { setIsPopupVisible(false); } }, [isPopupVisible, overflowMode]); const keyActionHandler = useCallback((e) => { if (!toolbarRef.current || !keyboardNavigation || ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) { return; } const target = e.target; e.preventDefault(); const keyHandlers = { ArrowRight: () => handleHorizontalNavigation(target, e.key), ArrowLeft: () => handleHorizontalNavigation(target, e.key), ArrowUp: () => handleVerticalNavigation(target, e.key), ArrowDown: () => handleVerticalNavigation(target, e.key), Home: () => handleEdgeNavigation(target, e.key), End: () => handleEdgeNavigation(target, e.key), Tab: () => handleTabNavigation(target), Enter: () => handleEnterKey(target), Escape: () => handleEscapeKey() }; const handler = keyHandlers[e.key]; if (handler) { handler(); } }, [ keyboardNavigation, handleHorizontalNavigation, handleVerticalNavigation, handleEdgeNavigation, handleTabNavigation, handleEnterKey, handleEscapeKey ]); const handleDocKeyDown = useCallback((e) => { if (!keyboardNavigation || ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) { return; } const popupCheck = overflowMode === OverflowMode.Popup && isPopupVisible; if (e.key === 'Tab' && e.target.classList.contains(CLS_POPUPNAV) && popupCheck) { setIsPopupVisible(false); } if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'End' || e.key === 'Home') { e.preventDefault(); } }, [keyboardNavigation, overflowMode, isPopupVisible]); const handlePopupStateChange = useCallback((isOpen) => { setIsPopupVisible(isOpen); }, []); const refreshOverflow = useCallback(() => { if (overflowMode === OverflowMode.Scrollable && scrollableRef.current) { scrollableRef.current.refreshOverflow(); } else if ((overflowMode === OverflowMode.Popup || overflowMode === OverflowMode.Extended) && popupRef.current) { popupRef.current.refreshOverflow(); } }, [overflowMode]); useLayoutEffect(() => { preRender('toolbar'); setIsToolbarRefReady(true); }, []); const classes = useMemo(() => { const classArray = ['sf-control', CLS_TOOLBAR, 'sf-lib']; if (dir === 'rtl') { classArray.push(CLS_RTL); } switch (overflowMode) { case OverflowMode.MultiRow: classArray.push(CLS_MULTIROW_TOOLBAR); break; case OverflowMode.Popup: classArray.push(CLS_POPUP_TOOLBAR); break; case OverflowMode.Extended: classArray.push(CLS_EXTENDABLE_TOOLBAR); if (isPopupVisible) { classArray.push(CLS_EXTENDEDPOPOPEN); } break; } if (orientation === Orientation.Vertical) { classArray.push(CLS_VERTICAL); } if (className) { classArray.push(className); } return classArray.join(' '); }, [className, dir, orientation, isPopupVisible, overflowMode]); const publicAPI = { keyboardNavigation, collision, orientation, overflowMode, scrollStep }; useImperativeHandle(ref, () => { return { ...publicAPI, refreshOverflow: refreshOverflow, element: toolbarRef.current }; }); return (_jsx("div", { ref: toolbarRef, className: classes, role: 'toolbar', "aria-orientation": orientation.toLowerCase(), onKeyDown: handleDocKeyDown, onKeyUp: keyActionHandler, ...eleAttr, children: (() => { switch (overflowMode) { case OverflowMode.Scrollable: return (_jsx(ToolbarScrollable, { ref: scrollableRef, toolbarRef: toolbarRef, orientation: orientation, scrollStep: scrollStep, onOverflowChange: onOverflowChange, className: className, children: children }, `scrollable-${orientation}-toolbar`)); case OverflowMode.MultiRow: return (_jsx(ToolbarMultiRow, { onOverflowChange: onOverflowChange, children: children })); case OverflowMode.Popup: case OverflowMode.Extended: return (_jsx(ToolbarPopup, { ref: popupRef, toolbarRef: toolbarRef, isToolbarRefReady: isToolbarRefReady, orientation: orientation, overflowMode: overflowMode, collision: collision, isPopupVisible: isPopupVisible, onPopupOpenChange: handlePopupStateChange, onOverflowChange: onOverflowChange, className: className, children: children }, `${overflowMode === OverflowMode.Popup ? 'popup' : 'extended'}-${orientation}-toolbar`)); } })() })); })); Toolbar.displayName = 'Toolbar'; export default Toolbar;