diff --git a/src/components/flyout/flyout.tsx b/src/components/flyout/flyout.tsx index 31f2b5ad76e..a6ac5deb1fb 100644 --- a/src/components/flyout/flyout.tsx +++ b/src/components/flyout/flyout.tsx @@ -9,6 +9,8 @@ import React, { useEffect, useRef, + useMemo, + useCallback, useState, forwardRef, ComponentPropsWithRef, @@ -187,7 +189,7 @@ export const EuiFlyout = forwardRef( outsideClickCloses, pushMinBreakpoint = 'l', pushAnimation = false, - focusTrapProps: _focusTrapProps = {}, + focusTrapProps: _focusTrapProps, includeFixedHeadersInFocusTrap = true, 'aria-describedby': _ariaDescribedBy, ...rest @@ -246,23 +248,31 @@ export const EuiFlyout = forwardRef( /** * ESC key closes flyout (always?) */ - const onKeyDown = (event: KeyboardEvent) => { - if (!isPushed && event.key === keys.ESCAPE) { - event.preventDefault(); - onClose(event); - } - }; + const onKeyDown = useCallback( + (event: KeyboardEvent) => { + if (!isPushed && event.key === keys.ESCAPE) { + event.preventDefault(); + onClose(event); + } + }, + [onClose, isPushed] + ); /** * Set inline styles */ - let newStyle = style; - if (typeof maxWidth !== 'boolean') { - newStyle = { ...newStyle, ...logicalStyle('max-width', maxWidth) }; - } - if (!isEuiFlyoutSizeNamed(size)) { - newStyle = { ...newStyle, ...logicalStyle('width', size) }; - } + const inlineStyles = useMemo(() => { + const widthStyle = + !isEuiFlyoutSizeNamed(size) && logicalStyle('width', size); + const maxWidthStyle = + typeof maxWidth !== 'boolean' && logicalStyle('max-width', maxWidth); + + return { + ...style, + ...widthStyle, + ...maxWidthStyle, + }; + }, [style, maxWidth, size]); const euiTheme = useEuiTheme(); const styles = euiFlyoutStyles(euiTheme); @@ -280,8 +290,9 @@ export const EuiFlyout = forwardRef( const classes = classnames('euiFlyout', className); - let closeButton; - if (onClose && !hideCloseButton) { + const closeButton = useMemo(() => { + if (hideCloseButton || !onClose) return null; + const closeButtonClasses = classnames( 'euiFlyout__closeButton', closeButtonProps?.className @@ -296,7 +307,7 @@ export const EuiFlyout = forwardRef( closeButtonProps?.css, ]; - closeButton = ( + return ( {(closeAriaLabel: string) => ( ); - } + }, [ + onClose, + hideCloseButton, + closeButtonPosition, + closeButtonProps, + side, + euiTheme, + ]); /* * If not disabled, automatically add fixed EuiHeaders as shards @@ -345,10 +363,13 @@ export const EuiFlyout = forwardRef( } }, [includeFixedHeadersInFocusTrap, resizeRef]); - const focusTrapProps: EuiFlyoutProps['focusTrapProps'] = { - ..._focusTrapProps, - shards: [...fixedHeaders, ...(_focusTrapProps.shards || [])], - }; + const focusTrapProps: EuiFlyoutProps['focusTrapProps'] = useMemo( + () => ({ + ..._focusTrapProps, + shards: [...fixedHeaders, ...(_focusTrapProps?.shards || [])], + }), + [fixedHeaders, _focusTrapProps] + ); /* * Provide meaningful screen reader instructions/details @@ -393,19 +414,22 @@ export const EuiFlyout = forwardRef( * or if `outsideClickCloses={true}` to close on clicks that target * (both mousedown and mouseup) the overlay mask. */ - const onClickOutside = (event: MouseEvent | TouchEvent) => { - // Do not close the flyout for any external click - if (outsideClickCloses === false) return undefined; - if (hasOverlayMask) { - // The overlay mask is present, so only clicks on the mask should close the flyout, regardless of outsideClickCloses - if (event.target === maskRef.current) return onClose(event); - } else { - // No overlay mask is present, so any outside clicks should close the flyout - if (outsideClickCloses === true) return onClose(event); - } - // Otherwise if ownFocus is false and outsideClickCloses is undefined, outside clicks should not close the flyout - return undefined; - }; + const onClickOutside = useCallback( + (event: MouseEvent | TouchEvent) => { + // Do not close the flyout for any external click + if (outsideClickCloses === false) return undefined; + if (hasOverlayMask) { + // The overlay mask is present, so only clicks on the mask should close the flyout, regardless of outsideClickCloses + if (event.target === maskRef.current) return onClose(event); + } else { + // No overlay mask is present, so any outside clicks should close the flyout + if (outsideClickCloses === true) return onClose(event); + } + // Otherwise if ownFocus is false and outsideClickCloses is undefined, outside clicks should not close the flyout + return undefined; + }, + [onClose, hasOverlayMask, outsideClickCloses] + ); let flyout = ( )} role={!isPushed ? 'dialog' : rest.role} diff --git a/upcoming_changelogs/7259.md b/upcoming_changelogs/7259.md new file mode 100644 index 00000000000..9beea48791b --- /dev/null +++ b/upcoming_changelogs/7259.md @@ -0,0 +1,3 @@ +**Bug fixes** + +- Fixed focus trap rerender issues in `EuiFlyout` with memoization