diff --git a/.changeset/perf-select-panel-memo-context.md b/.changeset/perf-select-panel-memo-context.md new file mode 100644 index 00000000000..01a78e13875 --- /dev/null +++ b/.changeset/perf-select-panel-memo-context.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Memoize SelectPanel overlayProps, focusTrapSettings, and preventBubbling to reduce allocations on re-renders diff --git a/packages/react/src/SelectPanel/SelectPanel.tsx b/packages/react/src/SelectPanel/SelectPanel.tsx index 8fc6ede738a..d83644f9b67 100644 --- a/packages/react/src/SelectPanel/SelectPanel.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.tsx @@ -1,5 +1,5 @@ import {SearchIcon, TriangleDownIcon, XIcon, type IconProps} from '@primer/octicons-react' -import React, {useCallback, useEffect, useMemo, useRef, useState, type KeyboardEventHandler, type JSX} from 'react' +import React, {useCallback, useEffect, useMemo, useRef, useState, type JSX} from 'react' import type {AnchoredOverlayProps} from '../AnchoredOverlay' import {AnchoredOverlay} from '../AnchoredOverlay' import type {AnchoredOverlayWrapperAnchorProps} from '../AnchoredOverlay/AnchoredOverlay' @@ -146,6 +146,8 @@ const focusZoneSettings: Partial = { disabled: true, } +const closeButtonProps = {'aria-label': 'Cancel and close'} + const areItemsEqual = (itemA: ItemInput, itemB: ItemInput) => { // prefer checking equivality by item.id if (typeof itemA.id !== 'undefined') return itemA.id === itemB.id @@ -698,9 +700,7 @@ function Panel({ } }, [open, resetSort]) - const focusTrapSettings = { - initialFocusRef: inputRef || undefined, - } + const focusTrapSettings = useMemo(() => ({initialFocusRef: inputRef || undefined}), [inputRef]) const extendedTextInputProps: Partial = useMemo(() => { return { @@ -797,12 +797,11 @@ function Panel({ 'anchored', ) - const preventBubbling = - (customOnKeyDown: KeyboardEventHandler | undefined) => + const preventBubbling = useCallback( (event: React.KeyboardEvent) => { - // skip if a TextInput has focus - customOnKeyDown?.(event) + overlayProps?.onKeyDown?.(event as unknown as React.KeyboardEvent) + // skip if a TextInput has focus const activeElement = document.activeElement as HTMLElement if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') return @@ -817,7 +816,35 @@ function Panel({ // if this is a typeahead event, don't propagate outside of menu event.stopPropagation() - } + }, + [overlayProps], + ) + + const mergedOverlayProps = useMemo( + () => ({ + role: 'dialog' as const, + 'aria-labelledby': titleId, + 'aria-describedby': subtitle ? subtitleId : undefined, + ...overlayProps, + ...(variant === 'modal' + ? { + top: '50vh' as const, + left: '50vw' as const, + anchorSide: undefined, + } + : {}), + style: { + transform: variant === 'modal' ? 'translate(-50%, -50%)' : undefined, + ...(isKeyboardVisible + ? { + maxHeight: availablePanelHeight !== undefined ? `${availablePanelHeight}px` : 'auto', + } + : {}), + } as React.CSSProperties, + onKeyDown: preventBubbling, + }), + [titleId, subtitle, subtitleId, overlayProps, variant, isKeyboardVisible, availablePanelHeight, preventBubbling], + ) return ( <> @@ -828,31 +855,7 @@ function Panel({ open={open} onOpen={onOpen} onClose={onClose} - overlayProps={{ - role: 'dialog', - 'aria-labelledby': titleId, - 'aria-describedby': subtitle ? subtitleId : undefined, - ...overlayProps, - ...(variant === 'modal' - ? { - /* override AnchoredOverlay position */ - top: '50vh', - left: '50vw', - anchorSide: undefined, - } - : {}), - style: { - /* override AnchoredOverlay position */ - transform: variant === 'modal' ? 'translate(-50%, -50%)' : undefined, - // set maxHeight based on calculated availablePanelHeight when keyboard is visible - ...(isKeyboardVisible - ? { - maxHeight: availablePanelHeight !== undefined ? `${availablePanelHeight}px` : 'auto', - } - : {}), - } as React.CSSProperties, - onKeyDown: preventBubbling(overlayProps?.onKeyDown), - }} + overlayProps={mergedOverlayProps} focusTrapSettings={focusTrapSettings} focusZoneSettings={focusZoneSettings} height={height} @@ -862,7 +865,7 @@ function Panel({ pinPosition={!height} className={classes.Overlay} displayCloseButton={showXCloseIcon} - closeButtonProps={{'aria-label': 'Cancel and close'}} + closeButtonProps={closeButtonProps} >