From a8d90210f83fc581ee7d1ad71867d3e81ae1a612 Mon Sep 17 00:00:00 2001 From: Tomasz Kajtoch Date: Mon, 6 Oct 2025 09:53:35 +0200 Subject: [PATCH 1/3] WIP # Conflicts: # packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx # packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx --- .../components/flyout/flyout.component.tsx | 49 +----- .../src/components/flyout/flyout.styles.ts | 64 ++------ .../flyout/manager/flyout_manager.stories.tsx | 152 +++++++++--------- .../manager/flyout_sessions.stories.tsx | 148 ++++++++--------- .../src/components/flyout/use_open_state.ts | 72 +-------- 5 files changed, 180 insertions(+), 305 deletions(-) diff --git a/packages/eui/src/components/flyout/flyout.component.tsx b/packages/eui/src/components/flyout/flyout.component.tsx index 13cbd9e64af..92b1072bc05 100644 --- a/packages/eui/src/components/flyout/flyout.component.tsx +++ b/packages/eui/src/components/flyout/flyout.component.tsx @@ -78,26 +78,10 @@ import { import type { EuiFlyoutCloseEvent } from './types'; interface _EuiFlyoutComponentProps { - /** - * Whether the flyout is open (visible) or closed (hidden). - * It defaults to `true` for backwards compatibility. - * @default true - */ - isOpen?: boolean; /** * A required callback function fired when the flyout is closed. - * It fires after the closing animation is finished. - * - * Use this callback to toggle your internal `isOpen` flyout state. */ onClose: (event?: EuiFlyoutCloseEvent) => void; - /** - * An optional callback function fired when the flyout begins closing. - * - * Use in case you need to support any extra logic that relies on the flyout - * closing state. In most cases this callback doesn't need to be handled. - */ - onClosing?: (event?: EuiFlyoutCloseEvent) => void; /** * Defines the width of the panel. * Pass a predefined size of `s | m | l`, or pass any number/string compatible with the CSS `width` attribute @@ -223,9 +207,6 @@ const defaultElement = 'div'; const openStateToClassNameMap: Record = { opening: 'euiFlyout--opening', open: 'euiFlyout--open', - closing: 'euiFlyout--closing', - // No special class needed for the closed state - closed: '', }; type Props = CommonProps & { @@ -276,23 +257,14 @@ export const EuiFlyoutComponent = forwardRef( resizable = false, minWidth, onResize, - isOpen = true, - onClosing, onAnimationEnd: _onAnimationEnd, ...rest } = usePropsWithComponentDefaults('EuiFlyout', props); const { setGlobalCSSVariables } = useEuiThemeCSSVariables(); - const { - openState, - onAnimationEnd: onAnimationEndFlyoutOpenState, - closeFlyout, - } = useEuiFlyoutOpenState({ - isOpen, - onClose, - onClosing, - }); + const { openState, onAnimationEnd: onAnimationEndFlyoutOpenState } = + useEuiFlyoutOpenState(); const Element = as || defaultElement; const maskRef = useRef(null); @@ -433,10 +405,10 @@ export const EuiFlyoutComponent = forwardRef( (event: KeyboardEvent) => { if (!isPushed && event.key === keys.ESCAPE && shouldCloseOnEscape) { event.preventDefault(); - closeFlyout(event); + onClose(event); } }, - [closeFlyout, isPushed, shouldCloseOnEscape] + [onClose, isPushed, shouldCloseOnEscape] ); const siblingFlyoutWidth = useFlyoutWidth(siblingFlyoutId); @@ -600,15 +572,15 @@ export const EuiFlyoutComponent = forwardRef( 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 closeFlyout(event); + 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 closeFlyout(event); + if (outsideClickCloses === true) return onClose(event); } // Otherwise if ownFocus is false and outsideClickCloses is undefined, outside clicks should not close the flyout return undefined; }, - [closeFlyout, hasOverlayMask, outsideClickCloses] + [onClose, hasOverlayMask, outsideClickCloses] ); const maskCombinedRefs = useCombinedRefs([maskProps?.maskRef, maskRef]); @@ -621,11 +593,6 @@ export const EuiFlyoutComponent = forwardRef( [_onAnimationEnd, onAnimationEndFlyoutOpenState] ); - if (openState === 'closed') { - // Render null only if the flyout is completely closed - return null; - } - return ( diff --git a/packages/eui/src/components/flyout/flyout.styles.ts b/packages/eui/src/components/flyout/flyout.styles.ts index 2bebca12a3d..d7b2560c937 100644 --- a/packages/eui/src/components/flyout/flyout.styles.ts +++ b/packages/eui/src/components/flyout/flyout.styles.ts @@ -125,30 +125,15 @@ export const euiFlyoutStyles = (euiThemeContext: UseEuiTheme) => { clip-path: polygon(-50% 0, 100% 0, 100% 100%, -50% 100%); ${logicalCSS('right', 0)} - &.euiFlyout--opening { - /* - Jump animation states immediately unless - prefers-reduced-motion: reduce is *not* set - */ - animation: ${euiFlyoutSlideInRight} 0s ${euiTheme.animation.resistance} - forwards; - - ${euiCanAnimate} { - animation-duration: ${euiTheme.animation.normal}; - } - } - - &.euiFlyout--closing { - /* - Jump animation states immediately unless - prefers-reduced-motion: reduce is *not* set - */ - animation: ${euiFlyoutSlideOutRight} 0s ${euiTheme.animation.resistance} - forwards; - - ${euiCanAnimate} { - animation-duration: ${euiTheme.animation.normal}; - } + /* + Jump animation states immediately unless + prefers-reduced-motion: reduce is *not* set + */ + animation: ${euiFlyoutSlideInRight} 0s ${euiTheme.animation + .resistance} forwards; + + ${euiCanAnimate} { + animation-duration: ${euiTheme.animation.normal}; } &.euiFlyout--hasChild { @@ -160,30 +145,15 @@ export const euiFlyoutStyles = (euiThemeContext: UseEuiTheme) => { ${logicalCSS('left', 0)} clip-path: polygon(0 0, 150% 0, 150% 100%, 0 100%); - &.euiFlyout--opening { - /* - Jump animation states immediately unless - prefers-reduced-motion: reduce is *not* set - */ - animation: ${euiFlyoutSlideInLeft} 0s ${euiTheme.animation.resistance} - forwards; + /* + Jump animation states immediately unless + prefers-reduced-motion: reduce is *not* set + */ + animation: ${euiFlyoutSlideInLeft} 0s ${euiTheme.animation.resistance} + forwards; - ${euiCanAnimate} { - animation-duration: ${euiTheme.animation.normal}; - } - } - - &.euiFlyout--closing { - /* - Jump animation states immediately unless - prefers-reduced-motion: reduce is *not* set - */ - animation: ${euiFlyoutSlideOutLeft} 0s ${euiTheme.animation.resistance} - forwards; - - ${euiCanAnimate} { - animation-duration: ${euiTheme.animation.normal}; - } + ${euiCanAnimate} { + animation-duration: ${euiTheme.animation.normal}; } `, diff --git a/packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx b/packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx index 663017152ac..e54333e55c7 100644 --- a/packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx @@ -221,84 +221,86 @@ const StatefulFlyout: React.FC = ({ Open Main Flyout )} - - - -

This is the main flyout content.

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum - neque sequi illo, cum rerum quia ab animi velit sit incidunt - inventore temporibus eaque nam veritatis amet maxime maiores optio - quam? -

-
- + {isMainOpen && ( + + + +

This is the main flyout content.

+

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum + neque sequi illo, cum rerum quia ab animi velit sit incidunt + inventore temporibus eaque nam veritatis amet maxime maiores + optio quam? +

+
+ - {!isChildOpen ? ( - Open child panel - ) : ( - Close child panel - )} - - + {!isChildOpen ? ( + Open child panel + ) : ( + Close child panel + )} + {isChildOpen && ( + + + +

This is the child flyout content.

+

Size restrictions apply:

+
    +
  • When main panel is 's', child can be 's', or 'm'
  • +
  • When main panel is 'm', child is limited to 's'
  • +
+ +

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. + Dolorum neque sequi illo, cum rerum quia ab animi velit + sit incidunt inventore temporibus eaque nam veritatis amet + maxime maiores optio quam? +

+
+
+ {showFooter && ( + + +

Child flyout footer

+
+
+ )} + {/* Footer is optional */} +
+ )} +
+ {showFooter && ( + -

This is the child flyout content.

-

Size restrictions apply:

-
    -
  • When main panel is 's', child can be 's', or 'm'
  • -
  • When main panel is 'm', child is limited to 's'
  • -
- -

- Lorem ipsum dolor sit amet consectetur adipisicing elit. - Dolorum neque sequi illo, cum rerum quia ab animi velit sit - incidunt inventore temporibus eaque nam veritatis amet maxime - maiores optio quam? -

+

Main flyout footer

-
- {showFooter && ( - - -

Child flyout footer

-
-
- )} - {/* Footer is optional */} -
-
- {showFooter && ( - - -

Main flyout footer

-
-
- )} -
+ + )} + + )} ); }; diff --git a/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx b/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx index 3c3683ee731..514d8298b40 100644 --- a/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx @@ -96,80 +96,80 @@ const FlyoutSession: React.FC = React.memo((props) => { Open {title} - - - -

This is the content of {title}.

- - - {childSize && ( - - Open child flyout - - )} -
-
- {childSize && ( - - - -

This is the content of the child flyout of {title}.

- - -
-
-
- )} -
+ {isFlyoutVisible && ( + + + +

This is the content of {title}.

+ + + {childSize && ( + + Open child flyout + + )} +
+
+ {childSize && isChildFlyoutVisible && ( + + + +

This is the content of the child flyout of {title}.

+ + +
+
+
+ )} +
+ )} ); }); diff --git a/packages/eui/src/components/flyout/use_open_state.ts b/packages/eui/src/components/flyout/use_open_state.ts index 644e064e40d..a7c1ff747df 100644 --- a/packages/eui/src/components/flyout/use_open_state.ts +++ b/packages/eui/src/components/flyout/use_open_state.ts @@ -6,85 +6,21 @@ * Side Public License, v 1. */ -import { AnimationEventHandler, useCallback, useEffect, useState } from 'react'; -import type { EuiFlyoutProps } from './flyout'; -import { useIsInManagedFlyout } from './manager'; -import type { EuiFlyoutCloseEvent } from './types'; +import { AnimationEventHandler, useCallback, useState } from 'react'; -export type EuiFlyoutOpenState = 'opening' | 'open' | 'closing' | 'closed'; +export type EuiFlyoutOpenState = 'opening' | 'open'; -interface UseEuiFlyoutOpenStateArgs { - isOpen: EuiFlyoutProps['isOpen']; - onClose: EuiFlyoutProps['onClose']; - onClosing: EuiFlyoutProps['onClosing']; -} - -export const useEuiFlyoutOpenState = ({ - isOpen, - onClose, - onClosing, -}: UseEuiFlyoutOpenStateArgs) => { - const [openState, setOpenState] = useState( - isOpen ? 'open' : 'closed' - ); - const isInManagedFlyout = useIsInManagedFlyout(); - - useEffect(() => { - // Check for matching state - if ( - (isOpen && openState === 'open') || - (!isOpen && openState === 'closed') - ) { - return; - } - - if (isOpen && (openState === 'closing' || openState === 'closed')) { - setOpenState('opening'); - } - - if (!isOpen && (openState === 'opening' || openState === 'open')) { - setOpenState('closing'); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen]); - - useEffect(() => { - // For managed flyouts, don't auto-call onClose - let the manager handle it - if (openState === 'closed' && !isInManagedFlyout) { - onClose(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [openState, isInManagedFlyout]); +export const useEuiFlyoutOpenState = () => { + const [openState, setOpenState] = useState('opening'); const onAnimationEnd = useCallback(() => { - if (openState === 'closing') { - setOpenState('closed'); - } - if (openState === 'opening') { setOpenState('open'); } }, [openState, setOpenState]); - const closeFlyout = useCallback( - (event?: EuiFlyoutCloseEvent) => { - if (openState === 'closed' || openState === 'closing') { - return; - } - - onClosing?.(event); - - setOpenState('closing'); - - // onClose() will be called by the effect above when openState === 'closed' - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [openState, setOpenState] - ); - return { openState, onAnimationEnd, - closeFlyout, }; }; From bfaf51b2b03eb671305707a0a702eb5e99c42138 Mon Sep 17 00:00:00 2001 From: Tomasz Kajtoch Date: Tue, 7 Oct 2025 00:38:47 +0200 Subject: [PATCH 2/3] feat(EuiFlyout): remove `isOpen` usages --- .../components/flyout/flyout.component.tsx | 29 ++----------------- .../src/components/flyout/flyout.stories.tsx | 28 ++++++++---------- .../flyout/manager/flyout_managed.tsx | 19 ++++++------ 3 files changed, 23 insertions(+), 53 deletions(-) diff --git a/packages/eui/src/components/flyout/flyout.component.tsx b/packages/eui/src/components/flyout/flyout.component.tsx index 92b1072bc05..d85ba5d198d 100644 --- a/packages/eui/src/components/flyout/flyout.component.tsx +++ b/packages/eui/src/components/flyout/flyout.component.tsx @@ -22,7 +22,6 @@ import React, { MutableRefObject, ReactNode, JSX, - AnimationEventHandler, } from 'react'; import classnames from 'classnames'; @@ -71,10 +70,6 @@ import { useIsPushed } from './hooks'; import { EuiFlyoutMenu, EuiFlyoutMenuProps } from './flyout_menu'; import { EuiFlyoutResizeButton } from './_flyout_resize_button'; import { useEuiFlyoutResizable } from './use_flyout_resizable'; -import { - useEuiFlyoutOpenState, - type EuiFlyoutOpenState, -} from './use_open_state'; import type { EuiFlyoutCloseEvent } from './types'; interface _EuiFlyoutComponentProps { @@ -204,11 +199,6 @@ interface _EuiFlyoutComponentProps { const defaultElement = 'div'; -const openStateToClassNameMap: Record = { - opening: 'euiFlyout--opening', - open: 'euiFlyout--open', -}; - type Props = CommonProps & { /** * Sets the HTML element for `EuiFlyout` @@ -257,15 +247,12 @@ export const EuiFlyoutComponent = forwardRef( resizable = false, minWidth, onResize, - onAnimationEnd: _onAnimationEnd, + onAnimationEnd, ...rest } = usePropsWithComponentDefaults('EuiFlyout', props); const { setGlobalCSSVariables } = useEuiThemeCSSVariables(); - const { openState, onAnimationEnd: onAnimationEndFlyoutOpenState } = - useEuiFlyoutOpenState(); - const Element = as || defaultElement; const maskRef = useRef(null); @@ -447,11 +434,7 @@ export const EuiFlyoutComponent = forwardRef( styles[side], ]; - const classes = classnames( - 'euiFlyout', - openStateToClassNameMap[openState], - className - ); + const classes = classnames('euiFlyout', className); const flyoutToggle = useRef(document.activeElement); const [focusTrapShards, setFocusTrapShards] = useState([]); @@ -585,14 +568,6 @@ export const EuiFlyoutComponent = forwardRef( const maskCombinedRefs = useCombinedRefs([maskProps?.maskRef, maskRef]); - const onAnimationEnd = useCallback( - (event) => { - onAnimationEndFlyoutOpenState(event); - _onAnimationEnd?.(event); - }, - [_onAnimationEnd, onAnimationEndFlyoutOpenState] - ); - return ( ; const onClose = action('onClose'); -const onClosing = action('onClosing'); - const StatefulFlyout = ( - props: Partial< - EuiFlyoutProps & { isOpen: boolean; onToggle: (open: boolean) => void } - > + props: Partial void }> ) => { - const { isOpen, onToggle } = props; - const [_isOpen, setIsOpen] = useState(isOpen ?? true); + const { onToggle } = props; + const [_isOpen, setIsOpen] = useState(true); const handleToggle = (open: boolean) => { setIsOpen(open); @@ -74,15 +70,15 @@ const StatefulFlyout = ( handleToggle(!_isOpen)}> Toggle flyout - { - handleToggle(false); - onClose(); - }} - onClosing={onClosing} - /> + {_isOpen && ( + { + handleToggle(false); + onClose(); + }} + /> + )} ); }; diff --git a/packages/eui/src/components/flyout/manager/flyout_managed.tsx b/packages/eui/src/components/flyout/manager/flyout_managed.tsx index 76014ef2735..49f1994a8d2 100644 --- a/packages/eui/src/components/flyout/manager/flyout_managed.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_managed.tsx @@ -76,7 +76,6 @@ export const EuiManagedFlyout = ({ level, size = 'm', css: customCss, - isOpen = true, flyoutMenuProps: _flyoutMenuProps, ...props }: EuiManagedFlyoutProps) => { @@ -141,29 +140,30 @@ export const EuiManagedFlyout = ({ // Register with flyout manager context when open, remove when closed useEffect(() => { - if (isOpen) { - addFlyout(flyoutId, title!, level, size as string); - } else { + addFlyout(flyoutId, title!, level, size as string); + + return () => { closeFlyout(flyoutId); + // Reset navigation tracking when explicitly closed via isOpen=false wasRegisteredRef.current = false; - } - }, [isOpen, flyoutId, title, level, size, addFlyout, closeFlyout]); + }; + }, [flyoutId, title, level, size, addFlyout, closeFlyout]); // Detect when flyout has been removed from manager state (e.g., via Back button) // and trigger onClose callback to notify the parent component useEffect(() => { - if (isOpen && flyoutExistsInManager) { + if (flyoutExistsInManager) { wasRegisteredRef.current = true; } // If flyout was previously registered, is marked as open, but no longer exists in manager state, // it was removed via navigation (Back button) - trigger close callback - if (wasRegisteredRef.current && isOpen && !flyoutExistsInManager) { + if (wasRegisteredRef.current && !flyoutExistsInManager) { onCloseCallbackRef.current?.(new MouseEvent('navigation')); wasRegisteredRef.current = false; // Reset to avoid repeated calls } - }, [flyoutExistsInManager, isOpen, flyoutId]); + }, [flyoutExistsInManager, flyoutId]); // Monitor current session changes and fire onActive callback when this flyout becomes active useEffect(() => { @@ -248,7 +248,6 @@ export const EuiManagedFlyout = ({ size, flyoutMenuProps, onAnimationEnd, - isOpen, [PROPERTY_FLYOUT]: true, [PROPERTY_LAYOUT_MODE]: layoutMode, [PROPERTY_LEVEL]: level, From a9cbb704803204c5bf49be6a0532a70f508f980b Mon Sep 17 00:00:00 2001 From: Tomasz Kajtoch Date: Tue, 7 Oct 2025 22:08:38 +0200 Subject: [PATCH 3/3] test(EuiFlyout): remove `euiFlyout--open` class name from snapshots --- .../collapsible_nav.test.tsx.snap | 18 +++---- .../collapsible_nav_beta.test.tsx.snap | 4 +- .../flyout/__snapshots__/flyout.test.tsx.snap | 50 +++++++++---------- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/eui/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap b/packages/eui/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap index b52b75952fb..88bb47022fd 100644 --- a/packages/eui/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap +++ b/packages/eui/src/components/collapsible_nav/__snapshots__/collapsible_nav.test.tsx.snap @@ -14,7 +14,7 @@ exports[`EuiCollapsibleNav close button can be hidden 1`] = `