From 30702963d5a48da5a09e87c6c82116d4042988d1 Mon Sep 17 00:00:00 2001 From: Felix Habib Date: Tue, 26 Nov 2024 15:18:13 +1100 Subject: [PATCH] Only render Menu on `open` --- .../MenuRenderer/MenuRenderer.actions.ts | 40 ++-- .../MenuRenderer/MenuRenderer.css.ts | 2 +- .../components/MenuRenderer/MenuRenderer.tsx | 216 +++++++++--------- 3 files changed, 127 insertions(+), 131 deletions(-) diff --git a/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.actions.ts b/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.actions.ts index c232bd084ef..a86acc5a7d3 100644 --- a/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.actions.ts +++ b/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.actions.ts @@ -1,27 +1,26 @@ // Action type IDs (allows action type names to be minified) export const actionTypes = { - CLIENT_ENVIRONMENT: 0, - MENU_TRIGGER_UP: 1, - MENU_ITEM_UP: 2, - MENU_TRIGGER_DOWN: 3, - MENU_ITEM_DOWN: 4, - MENU_ITEM_ESCAPE: 5, - MENU_ITEM_TAB: 6, - MENU_ITEM_ENTER: 7, - MENU_ITEM_SPACE: 8, - MENU_ITEM_CLICK: 9, - MENU_ITEM_HOVER: 10, - MENU_TRIGGER_ENTER: 11, - MENU_TRIGGER_SPACE: 12, - MENU_TRIGGER_CLICK: 13, - MENU_TRIGGER_TAB: 14, - MENU_TRIGGER_ESCAPE: 15, - BACKDROP_CLICK: 16, - WINDOW_RESIZE: 17, + MENU_TRIGGER_UP: 0, + MENU_ITEM_UP: 1, + MENU_TRIGGER_DOWN: 2, + MENU_ITEM_DOWN: 3, + MENU_ITEM_ESCAPE: 4, + MENU_ITEM_TAB: 5, + MENU_ITEM_ENTER: 6, + MENU_ITEM_SPACE: 7, + MENU_ITEM_CLICK: 8, + MENU_ITEM_HOVER: 9, + MENU_TRIGGER_ENTER: 10, + MENU_TRIGGER_SPACE: 11, + MENU_TRIGGER_CLICK: 12, + MENU_TRIGGER_TAB: 13, + MENU_TRIGGER_ESCAPE: 14, + BACKDROP_CLICK: 15, + WINDOW_RESIZE: 16, + MENU_CLOSE: 18, } as const; export type Action = - | { type: typeof actionTypes.CLIENT_ENVIRONMENT } | { type: typeof actionTypes.MENU_TRIGGER_UP } | { type: typeof actionTypes.MENU_ITEM_UP } | { type: typeof actionTypes.MENU_TRIGGER_DOWN } @@ -53,4 +52,5 @@ export type Action = | { type: typeof actionTypes.MENU_TRIGGER_TAB } | { type: typeof actionTypes.MENU_TRIGGER_ESCAPE } | { type: typeof actionTypes.BACKDROP_CLICK } - | { type: typeof actionTypes.WINDOW_RESIZE }; + | { type: typeof actionTypes.WINDOW_RESIZE } + | { type: typeof actionTypes.MENU_CLOSE }; diff --git a/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.css.ts b/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.css.ts index d42fd6b57c0..d1a99c01653 100644 --- a/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.css.ts +++ b/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.css.ts @@ -22,7 +22,7 @@ export const menuPosition = style({ right: triggerVars.right, }); -export const menuIsClosed = style({ +export const entrance = style({ transform: `translateY(${calc(vars.grid).negate().multiply(2)})`, visibility: 'hidden', }); diff --git a/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.tsx b/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.tsx index 24ddfa8420b..869e840c40a 100644 --- a/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.tsx +++ b/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.tsx @@ -8,6 +8,7 @@ import React, { useRef, useReducer, useEffect, + useState, } from 'react'; import flattenChildren from '../../utils/flattenChildren'; import { Box } from '../Box/Box'; @@ -61,7 +62,6 @@ export interface MenuRendererProps { } const { - CLIENT_ENVIRONMENT, MENU_TRIGGER_UP, MENU_ITEM_UP, MENU_TRIGGER_DOWN, @@ -100,7 +100,6 @@ const getPosition = (element: HTMLElement | null): position | undefined => { }; interface State { - onClient: boolean; open: boolean; highlightIndex: number; closeReason: CloseReason; @@ -110,7 +109,6 @@ interface State { const CLOSED_INDEX = -1; const CLOSE_REASON_EXIT: CloseReasonExit = { reason: 'exit' }; const initialState: State = { - onClient: false, open: false, highlightIndex: CLOSED_INDEX, closeReason: CLOSE_REASON_EXIT, @@ -146,109 +144,94 @@ export const MenuRenderer = ({ 'All child nodes within a menu component must be a MenuItem, MenuItemLink, MenuItemCheckbox or MenuItemDivider: https://seek-oss.github.io/braid-design-system/components/MenuRenderer', ); - const [ - { - onClient: hasRenderedOnClient, - open, - highlightIndex, - closeReason, - triggerPosition, - }, - dispatch, - ] = useReducer((state: State, action: Action): State => { - switch (action.type) { - case CLIENT_ENVIRONMENT: { - return { ...state, onClient: true }; - } - case MENU_TRIGGER_UP: - case MENU_ITEM_UP: { - return { - ...state, - open: true, - closeReason: CLOSE_REASON_EXIT, - highlightIndex: getNextIndex(-1, state.highlightIndex, itemCount), - triggerPosition: buttonRef && getPosition(buttonRef.current), - }; - } - case MENU_TRIGGER_DOWN: - case MENU_ITEM_DOWN: { - return { - ...state, - open: true, - closeReason: CLOSE_REASON_EXIT, - highlightIndex: getNextIndex(1, state.highlightIndex, itemCount), - triggerPosition: buttonRef && getPosition(buttonRef.current), - }; - } - case BACKDROP_CLICK: - case MENU_TRIGGER_ESCAPE: - case MENU_TRIGGER_TAB: - case MENU_ITEM_ESCAPE: - case MENU_ITEM_TAB: { - return { - ...state, - open: false, - closeReason: CLOSE_REASON_EXIT, - highlightIndex: CLOSED_INDEX, - }; - } - case MENU_ITEM_ENTER: - case MENU_ITEM_SPACE: - case MENU_ITEM_CLICK: { - // Don't close the menu if the user clicked a "form element" item, e.g. checkbox - if ('formElement' in action && action.formElement) { - return state; + const [{ open, highlightIndex, closeReason, triggerPosition }, dispatch] = + useReducer((state: State, action: Action): State => { + switch (action.type) { + case MENU_TRIGGER_UP: + case MENU_ITEM_UP: { + return { + ...state, + open: true, + closeReason: CLOSE_REASON_EXIT, + highlightIndex: getNextIndex(-1, state.highlightIndex, itemCount), + triggerPosition: buttonRef && getPosition(buttonRef.current), + }; } - - return { - ...state, - open: false, - closeReason: { - reason: 'selection', - index: action.index, - id: action.id, - }, - highlightIndex: CLOSED_INDEX, - }; - } - case MENU_ITEM_HOVER: { - return { ...state, highlightIndex: action.value }; - } - case MENU_TRIGGER_ENTER: - case MENU_TRIGGER_SPACE: { - const nextOpen = !state.open; - return { - ...state, - open: nextOpen, - closeReason: CLOSE_REASON_EXIT, - highlightIndex: nextOpen ? 0 : CLOSED_INDEX, - triggerPosition: buttonRef && getPosition(buttonRef.current), - }; - } - case MENU_TRIGGER_CLICK: { - const nextOpen = !state.open; - - return { - ...state, - open: nextOpen, - closeReason: CLOSE_REASON_EXIT, - triggerPosition: buttonRef && getPosition(buttonRef.current), - }; - } - case WINDOW_RESIZE: { - return { - ...state, - triggerPosition: buttonRef && getPosition(buttonRef.current), - }; + case MENU_TRIGGER_DOWN: + case MENU_ITEM_DOWN: { + return { + ...state, + open: true, + closeReason: CLOSE_REASON_EXIT, + highlightIndex: getNextIndex(1, state.highlightIndex, itemCount), + triggerPosition: buttonRef && getPosition(buttonRef.current), + }; + } + case BACKDROP_CLICK: + case MENU_TRIGGER_ESCAPE: + case MENU_TRIGGER_TAB: + case MENU_ITEM_ESCAPE: + case MENU_ITEM_TAB: { + return { + ...state, + open: false, + closeReason: CLOSE_REASON_EXIT, + highlightIndex: CLOSED_INDEX, + }; + } + case MENU_ITEM_ENTER: + case MENU_ITEM_SPACE: + case MENU_ITEM_CLICK: { + // Don't close the menu if the user clicked a "form element" item, e.g. checkbox + if ('formElement' in action && action.formElement) { + return state; + } + + return { + ...state, + open: false, + closeReason: { + reason: 'selection', + index: action.index, + id: action.id, + }, + highlightIndex: CLOSED_INDEX, + }; + } + case MENU_ITEM_HOVER: { + return { ...state, highlightIndex: action.value }; + } + case MENU_TRIGGER_ENTER: + case MENU_TRIGGER_SPACE: { + const nextOpen = !state.open; + return { + ...state, + open: nextOpen, + closeReason: CLOSE_REASON_EXIT, + highlightIndex: nextOpen ? 0 : CLOSED_INDEX, + triggerPosition: buttonRef && getPosition(buttonRef.current), + }; + } + case MENU_TRIGGER_CLICK: { + const nextOpen = !state.open; + + return { + ...state, + open: nextOpen, + closeReason: CLOSE_REASON_EXIT, + triggerPosition: buttonRef && getPosition(buttonRef.current), + }; + } + case WINDOW_RESIZE: { + return { + ...state, + triggerPosition: buttonRef && getPosition(buttonRef.current), + }; + } + default: + return state; } - default: - return state; - } - }, initialState); - - useEffect(() => { - dispatch({ type: CLIENT_ENVIRONMENT }); - }, []); + }, initialState); useEffect(() => { if (lastOpen.current === open) { @@ -354,10 +337,9 @@ export const MenuRenderer = ({ {trigger(triggerProps, { open })} - {hasRenderedOnClient && ( + {open && ( void; focusTrigger: () => void; highlightIndex: number; - open: boolean; children: ReactNode[]; triggerPosition?: position; } +const RENDER_DURATION = 1; + export function Menu({ offsetSpace, align, width, placement, children, - open, dispatch, focusTrigger, highlightIndex, @@ -429,6 +411,20 @@ export function Menu({ }: MenuProps) { let dividerCount = 0; + const [opening, setOpening] = useState(true); + + useEffect(() => { + if (opening) { + const timer = setTimeout(() => { + setOpening(false); + }, RENDER_DURATION); + + return () => { + clearTimeout(timer); + }; + } + }, [opening]); + const inlineVars = triggerPosition && assignInlineVars({ @@ -449,11 +445,11 @@ export function Menu({ marginTop={placement === 'bottom' ? offsetSpace : undefined} marginBottom={placement === 'top' ? offsetSpace : undefined} transition="fast" - opacity={!open ? 0 : undefined} + opacity={opening ? 0 : undefined} overflow="hidden" className={[ styles.menuPosition, - !open && styles.menuIsClosed, + opening && styles.entrance, width !== 'content' && styles.width[width], ]} >