diff --git a/src/navigation-bar.tsx b/src/navigation-bar.tsx index ca41ecd59..e8701f0db 100644 --- a/src/navigation-bar.tsx +++ b/src/navigation-bar.tsx @@ -226,6 +226,25 @@ type SectionMenu = ExclusifyUnion< | {content?: React.ReactElement | ((props: {closeMenu: () => void}) => React.ReactElement)} >; +const getInteractivePropsWithCloseMenu = (interactiveProps: InteractiveProps, closeMenu: () => void) => { + if ( + interactiveProps.href === undefined && + interactiveProps.onPress === undefined && + interactiveProps.to === undefined + ) { + return {onPress: closeMenu}; + } + + return interactiveProps.onPress + ? { + onPress: () => { + interactiveProps.onPress(); + closeMenu(); + }, + } + : {...interactiveProps, onNavigate: () => closeMenu()}; +}; + type MainNavigationBarSection = { title: string; menu?: SectionMenu; @@ -248,6 +267,225 @@ type MainNavigationBarProps = { type MainNavigationBarMenuStatus = 'opening' | 'opened' | 'closing' | 'closed'; type MainNavigationBarMenuAction = 'open' | 'finishOpen' | 'close' | 'finishClose'; +const mainNavigationBurgerMenuTranstions: Record< + MainNavigationBarMenuStatus, + Partial> +> = { + opening: { + close: 'closing', + finishOpen: 'opened', + }, + opened: { + close: 'closing', + }, + closing: { + open: 'opening', + finishClose: 'closed', + }, + closed: { + open: 'opening', + }, +}; + +const burgerMenuReducer = (state: MainNavigationBarMenuStatus, action: MainNavigationBarMenuAction) => { + return mainNavigationBurgerMenuTranstions[state][action] || state; +}; + +const MainNavigationBarBurgerMenu = ({ + sections, + extra, + closeMenu, + open, + id, + disableFocusTrap, + setDisableFocusTrap, +}: { + sections: ReadonlyArray; + extra: React.ReactNode; + closeMenu: () => void; + open: boolean; + id: string; + disableFocusTrap: boolean; + setDisableFocusTrap: (value: boolean) => void; +}) => { + const {texts, t, isDarkMode} = useTheme(); + const [openedSection, setOpenedSection] = React.useState(-1); + const [isSubMenuOpen, setIsSubMenuOpen] = React.useState(false); + const menuRef = React.useRef(null); + + const shadowAlpha = isDarkMode ? 1 : 0.2; + const menuAnimationDuration = isRunningAcceptanceTest() ? 0 : styles.BURGER_MENU_ANIMATION_DURATION_MS; + + const [subMenuStatus, dispatch] = React.useReducer(burgerMenuReducer, 'closed'); + + React.useEffect(() => { + let id: NodeJS.Timeout; + + // menu starts opening or closing + if (isSubMenuOpen) { + dispatch('open'); + id = setTimeout(() => dispatch('finishOpen'), menuAnimationDuration); + } else { + dispatch('close'); + id = setTimeout(() => dispatch('finishClose'), menuAnimationDuration); + } + + return () => clearTimeout(id); + }, [isSubMenuOpen, menuAnimationDuration]); + + const renderSection = (index: number) => { + const {title, menu, ...interactiveProps} = sections[index]; + const columns = menu?.columns || []; + const customContent = menu?.content; + const hasCustomInteraction = + interactiveProps.href !== undefined || + interactiveProps.onPress !== undefined || + interactiveProps.to !== undefined; + + return ( + + + + setIsSubMenuOpen(false)} + topFixed={false} + withBorder={false} + /> + + {texts.mainNavigationBarSectionSeeAll || + t(tokens.mainNavigationBarSectionSeeAll)} + + ) : undefined + } + > + {sections[index].title} + + + + {customContent ? ( + + {typeof customContent === 'function' ? customContent({closeMenu}) : customContent} + + ) : ( + columns.map((column, columnIndex) => ( + + {column.title} + + + {column.items.map( + ({title: itemTitle, ...itemInteractiveProps}, itemIndex) => ( + + ) + )} + + + + )) + )} + + + ); + }; + + return ( + + + setDisableFocusTrap(false)} + onExiting={() => setDisableFocusTrap(true)} + onExited={() => { + setIsSubMenuOpen(false); + setOpenedSection(-1); + }} + classNames={styles.burgerMenuTransition} + in={open} + nodeRef={menuRef} + timeout={menuAnimationDuration} + mountOnEnter + unmountOnExit + > + + + + + ); +}; + const mainNavigationDesktopMenuTranstions: Record< MainNavigationBarMenuStatus, Partial> @@ -270,7 +508,7 @@ const mainNavigationDesktopMenuTranstions: Record< }, }; -const menuReducer = (state: MainNavigationBarMenuStatus, action: MainNavigationBarMenuAction) => { +const desktopMenuReducer = (state: MainNavigationBarMenuStatus, action: MainNavigationBarMenuAction) => { return mainNavigationDesktopMenuTranstions[state][action] || state; }; @@ -314,7 +552,7 @@ const MainNavigationBarDesktopMenuContextProvider = ({ const {isTabletOrSmaller} = useScreenSize(); const [isMenuOpen, setIsMenuOpen] = React.useState(false); const [menuHeight, setMenuHeight] = React.useState('0px'); - const [menuStatus, dispatch] = React.useReducer(menuReducer, 'closed'); + const [menuStatus, dispatch] = React.useReducer(desktopMenuReducer, 'closed'); const [debouncedOpenSectionIndex, setDebouncedOpenSectionIndex] = React.useState(-1); // Item that is currently focused inside a section. This state is used to handle pressing @@ -491,217 +729,9 @@ const MainNavigationBarDesktopMenuContextProvider = ({ ); }; -const getInteractivePropsWithCloseMenu = (interactiveProps: InteractiveProps, closeMenu: () => void) => { - if ( - interactiveProps.href === undefined && - interactiveProps.onPress === undefined && - interactiveProps.to === undefined - ) { - return {onPress: closeMenu}; - } - - return interactiveProps.onPress - ? { - onPress: () => { - interactiveProps.onPress(); - closeMenu(); - }, - } - : {...interactiveProps, onNavigate: () => closeMenu()}; -}; - export const useMainNavigationBarDesktopMenuState = (): MainNavigationBarDesktopMenuState => React.useContext(MainNavigationBarDesktopMenuContext); -const MainNavigationBarBurgerMenu = ({ - sections, - extra, - closeMenu, - open, - id, - disableFocusTrap, - setDisableFocusTrap, -}: { - sections: ReadonlyArray; - extra: React.ReactNode; - closeMenu: () => void; - open: boolean; - id: string; - disableFocusTrap: boolean; - setDisableFocusTrap: (value: boolean) => void; -}) => { - const {texts, t, isDarkMode} = useTheme(); - const [openedSection, setOpenedSection] = React.useState(-1); - const [isSubMenuOpen, setIsSubMenuOpen] = React.useState(false); - const menuRef = React.useRef(null); - const menuContentRef = React.useRef(null); - - const shadowAlpha = isDarkMode ? 1 : 0.2; - const menuAnimationDuration = isRunningAcceptanceTest() ? 0 : styles.BURGER_MENU_ANIMATION_DURATION_MS; - - const renderSection = (index: number) => { - const {title, menu, ...interactiveProps} = sections[index]; - const columns = menu?.columns || []; - const customContent = menu?.content; - const hasCustomInteraction = - interactiveProps.href !== undefined || - interactiveProps.onPress !== undefined || - interactiveProps.to !== undefined; - - return ( - - - - setIsSubMenuOpen(false)} - topFixed={false} - withBorder={false} - /> - - {texts.mainNavigationBarSectionSeeAll || - t(tokens.mainNavigationBarSectionSeeAll)} - - ) : undefined - } - > - {sections[index].title} - - - - {customContent ? ( - - {typeof customContent === 'function' ? customContent({closeMenu}) : customContent} - - ) : ( - columns.map((column, columnIndex) => ( - - {column.title} - - - {column.items.map( - ({title: itemTitle, ...itemInteractiveProps}, itemIndex) => ( - - ) - )} - - - - )) - )} - - - ); - }; - - return ( - - - setDisableFocusTrap(false)} - onExiting={() => setDisableFocusTrap(true)} - onExited={() => { - setIsSubMenuOpen(false); - setOpenedSection(-1); - }} - classNames={styles.burgerMenuTransition} - in={open} - nodeRef={menuRef} - timeout={menuAnimationDuration} - mountOnEnter - unmountOnExit - > - - - - - ); -}; - const MainNavigationBarDesktopMenuSectionColumn = ({ column, columnIndex, @@ -1160,39 +1190,6 @@ const MainNavigationBarDesktopSection = ({ ); }; -// It's not easy to coordinate the animation of the menu content height when switching between opened -// sections. This is because each section has it's own element where it displays the content. Instead, -// the contents of the sections are rendered without any animation, and we keep this wrapper around -// all of them, which "hides" the rendered smoothly by using animated clip-path -const MainNavigationBarContentWrapper = ({ - children, - isLargeNavigationBar, - desktopSmallMenu, -}: { - children: React.ReactNode; - isLargeNavigationBar: boolean; - desktopSmallMenu: boolean; -}): JSX.Element => { - const {menuHeight} = useMainNavigationBarDesktopMenuState(); - const topSpace = isLargeNavigationBar ? NAVBAR_HEIGHT_DESKTOP_LARGE : NAVBAR_HEIGHT_DESKTOP; - - return ( -
- {children} -
- ); -}; - const MainNavigationBarDesktopSections = ({ sections, selectedIndex, @@ -1249,6 +1246,39 @@ const MainNavigationBarDesktopSections = ({ ); }; +// It's not easy to coordinate the animation of the menu content height when switching between opened +// sections. This is because each section has it's own element where it displays the content. Instead, +// the contents of the sections are rendered without any animation, and we keep this wrapper around +// all of them, which "hides" the rendered smoothly by using animated clip-path +const MainNavigationBarContentWrapper = ({ + children, + isLargeNavigationBar, + desktopSmallMenu, +}: { + children: React.ReactNode; + isLargeNavigationBar: boolean; + desktopSmallMenu: boolean; +}): JSX.Element => { + const {menuHeight} = useMainNavigationBarDesktopMenuState(); + const topSpace = isLargeNavigationBar ? NAVBAR_HEIGHT_DESKTOP_LARGE : NAVBAR_HEIGHT_DESKTOP; + + return ( +
+ {children} +
+ ); +}; + export const MainNavigationBar = ({ sections = [], selectedIndex,