diff --git a/packages/eui/src/components/flyout/manager/activity_stage.ts b/packages/eui/src/components/flyout/manager/activity_stage.ts index 080ef6b4141..da8a9dd92da 100644 --- a/packages/eui/src/components/flyout/manager/activity_stage.ts +++ b/packages/eui/src/components/flyout/manager/activity_stage.ts @@ -96,24 +96,44 @@ export const useFlyoutActivityStage = ({ }, [isActive, hasChild, layoutMode, level, ctx, flyoutId, stage]); /** - * OPENING / RETURNING -> ACTIVE - * CLOSING -> INACTIVE - * BACKGROUNDING -> BACKGROUNDED + * Get the stage to transition to for given current stage. + * Returns `null` if stage should remain unchanged. + * + * Stage transitions: + * - OPENING / RETURNING -> ACTIVE + * - CLOSING -> INACTIVE + * - BACKGROUNDING -> BACKGROUNDED + */ + const getNextStage = ( + stage: EuiFlyoutActivityStage + ): EuiFlyoutActivityStage | null => { + switch (stage) { + case STAGE_OPENING: + case STAGE_RETURNING: + return STAGE_ACTIVE; + + case STAGE_CLOSING: + return STAGE_INACTIVE; + + case STAGE_BACKGROUNDING: + return STAGE_BACKGROUNDED; + } + + return null; + }; + + /** + * onAnimationEnd event handler that must be passed to EuiFlyout. + * It handles transitions between stages and updates activity stage + * in EuiFlyoutManagerContext. */ const onAnimationEnd = useCallback(() => { - const s = stageRef.current; - const next: EuiFlyoutActivityStage | null = - s === STAGE_OPENING || s === STAGE_RETURNING - ? STAGE_ACTIVE - : s === STAGE_CLOSING - ? STAGE_INACTIVE - : s === STAGE_BACKGROUNDING - ? STAGE_BACKGROUNDED - : null; + const currentStage = stageRef.current; + const nextStage = getNextStage(currentStage); - if (next && next !== s) { - ctx?.dispatch?.(setActivityStage(flyoutId, next)); - stageRef.current = next; + if (nextStage && nextStage !== currentStage) { + ctx?.dispatch?.(setActivityStage(flyoutId, nextStage)); + stageRef.current = nextStage; } }, [ctx, flyoutId]); diff --git a/packages/eui/src/components/flyout/manager/flyout_child.stories.tsx b/packages/eui/src/components/flyout/manager/flyout_child.stories.tsx index 6bceb3d028b..89163c4cc48 100644 --- a/packages/eui/src/components/flyout/manager/flyout_child.stories.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_child.stories.tsx @@ -225,86 +225,84 @@ const StatefulFlyout: React.FC = ({ Open Main Flyout )} - {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? -

-
- + + + +

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 && ( - - - -

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 && ( - + {!isChildOpen ? ( + Open child panel + ) : ( + Close child panel + )} + + -

Main flyout footer

+

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 && ( + + +

Main flyout footer

+
+
+ )} + ); }; diff --git a/packages/eui/src/components/flyout/manager/flyout_fill_mode.stories.tsx b/packages/eui/src/components/flyout/manager/flyout_fill_mode.stories.tsx index 81bd5a1ab62..271eae8206a 100644 --- a/packages/eui/src/components/flyout/manager/flyout_fill_mode.stories.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_fill_mode.stories.tsx @@ -185,20 +185,49 @@ const Session: React.FC = (args) => { Open Main Flyout )} - {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 + )} +
-

This is the main flyout content.

+

This is the child flyout content.

+

Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum neque sequi illo, cum rerum quia ab animi velit sit incidunt @@ -206,40 +235,9 @@ const Session: React.FC = (args) => { optio quam?

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

This is the child 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? -

-
-
-
- )}
- )} +
); }; diff --git a/packages/eui/src/components/flyout/manager/flyout_managed.styles.ts b/packages/eui/src/components/flyout/manager/flyout_managed.styles.ts index ecacc6de0af..a4199a29455 100644 --- a/packages/eui/src/components/flyout/manager/flyout_managed.styles.ts +++ b/packages/eui/src/components/flyout/manager/flyout_managed.styles.ts @@ -10,6 +10,7 @@ import { css, keyframes } from '@emotion/react'; import { euiCanAnimate, logicalCSS } from '../../../global_styling'; import { UseEuiTheme } from '../../../services'; import { + LEVEL_MAIN, STAGE_ACTIVE, STAGE_BACKGROUNDED, STAGE_BACKGROUNDING, @@ -18,9 +19,8 @@ import { STAGE_OPENING, STAGE_RETURNING, } from './const'; -import { euiFlyoutSlideInLeft, euiFlyoutSlideInRight } from '../flyout.styles'; import { _EuiFlyoutSide, DEFAULT_SIDE } from '../const'; -import { EuiFlyoutActivityStage } from './types'; +import type { EuiFlyoutActivityStage, EuiFlyoutLevel } from './types'; /** * Emotion styles for managed flyouts. @@ -33,7 +33,8 @@ export const euiManagedFlyoutStyles = (euiThemeContext: UseEuiTheme) => { return { stage: ( activeStage: EuiFlyoutActivityStage, - side: _EuiFlyoutSide = DEFAULT_SIDE + side: _EuiFlyoutSide = DEFAULT_SIDE, + level: EuiFlyoutLevel ) => { // Animation for moving flyout backwards in 3D space (z-axis) when inactive const euiFlyoutSlideBack3D = keyframes` @@ -83,16 +84,6 @@ export const euiManagedFlyoutStyles = (euiThemeContext: UseEuiTheme) => { } `; - const openingTransition = css` - ${euiCanAnimate} { - animation: ${side === 'left' - ? euiFlyoutSlideInLeft - : euiFlyoutSlideInRight} - ${euiTheme.animation.normal} ${euiTheme.animation.resistance} - forwards; - } - `; - const noTransition = css` ${euiCanAnimate} { animation: none; @@ -116,21 +107,27 @@ export const euiManagedFlyoutStyles = (euiThemeContext: UseEuiTheme) => { switch (activeStage) { case STAGE_OPENING: - return [activeFlyout, openingTransition]; + // Apply a higher z-index to opening main flyouts for seamless + // transitions from previously active main flyouts + return [level === LEVEL_MAIN && activeFlyout]; case STAGE_ACTIVE: return [activeFlyout, noTransition]; - case STAGE_CLOSING: case STAGE_BACKGROUNDING: return [inactiveTransition]; - case STAGE_INACTIVE: case STAGE_BACKGROUNDED: return [inactiveFlyout, noTransition]; case STAGE_RETURNING: return [activeFlyout, returningTransition]; + + case STAGE_INACTIVE: + return [inactiveFlyout, noTransition]; + + case STAGE_CLOSING: + return [inactiveTransition]; } }, managedFlyout: css` diff --git a/packages/eui/src/components/flyout/manager/flyout_managed.test.tsx b/packages/eui/src/components/flyout/manager/flyout_managed.test.tsx index b65ebf5bbe1..e66f78e06a0 100644 --- a/packages/eui/src/components/flyout/manager/flyout_managed.test.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_managed.test.tsx @@ -28,7 +28,7 @@ jest.mock('../flyout.component', () => { const React = require('react'); return { EuiFlyoutComponent: React.forwardRef(function MockFlyout( - props: any, + { isOpen, ...props }: any, ref: any ) { // Extract flyoutMenuProps to prevent it from being passed to DOM @@ -37,6 +37,7 @@ jest.mock('../flyout.component', () => { ref, ...domProps, 'data-test-subj': 'managed-flyout', + 'data-is-open': isOpen, onClick: () => props.onClose && props.onClose({} as any), }); }), diff --git a/packages/eui/src/components/flyout/manager/flyout_managed.tsx b/packages/eui/src/components/flyout/manager/flyout_managed.tsx index a21b7a5b4dc..d3a385f149f 100644 --- a/packages/eui/src/components/flyout/manager/flyout_managed.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_managed.tsx @@ -70,6 +70,7 @@ export const EuiManagedFlyout = ({ level, size, css: customCss, + isOpen = true, flyoutMenuProps: _flyoutMenuProps, ...props }: EuiManagedFlyoutProps) => { @@ -116,12 +117,12 @@ export const EuiManagedFlyout = ({ // Register/unregister with flyout manager context useEffect(() => { - addFlyout(flyoutId, title!, level, size as string); + if (isOpen) { + addFlyout(flyoutId, title!, level, size as string); - return () => { - closeFlyout(flyoutId); - }; - }, [size, flyoutId, title, level, addFlyout, closeFlyout]); + return () => closeFlyout(flyoutId); + } + }, [isOpen, flyoutId, size, title, level, addFlyout, closeFlyout]); // Track width changes for flyouts const { width } = useResizeObserver( @@ -160,7 +161,7 @@ export const EuiManagedFlyout = ({ css={[ styles.managedFlyout, customCss, - styles.stage(activityStage, props.side), + styles.stage(activityStage, props.side, level), ]} {...{ ...props, @@ -168,6 +169,7 @@ export const EuiManagedFlyout = ({ size, flyoutMenuProps, onAnimationEnd, + isOpen, [PROPERTY_FLYOUT]: true, [PROPERTY_LAYOUT_MODE]: layoutMode, [PROPERTY_LEVEL]: level, 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 a8cfe9a9b17..792f9d5c253 100644 --- a/packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx @@ -43,7 +43,7 @@ interface ECommerceContentProps { interface ShoppingCartProps extends ECommerceContentProps, - Pick { + Pick { onQuantityChange: (delta: number) => void; } @@ -53,6 +53,7 @@ const ShoppingCartFlyout = ({ onClose, ownFocus, side, + isOpen, }: ShoppingCartProps) => { const [isItemDetailsOpen, setIsItemDetailsOpen] = useState(false); const [isReviewCartOpen, setIsReviewCartOpen] = useState(false); @@ -65,7 +66,7 @@ const ShoppingCartFlyout = ({ ownFocus={ownFocus} side={side} aria-label="Shopping cart" - {...{ onClose }} + {...{ onClose, isOpen }} > @@ -99,22 +100,18 @@ const ShoppingCartFlyout = ({ > {isReviewCartOpen ? 'Close review' : 'Proceed to review'} - {isItemDetailsOpen && ( - setIsItemDetailsOpen(false)} - itemQuantity={itemQuantity} - side={side} - /> - )} - {isReviewCartOpen && ( - <> - setIsReviewCartOpen(false)} - itemQuantity={itemQuantity} - side={side} - /> - - )} + setIsItemDetailsOpen(false)} + itemQuantity={itemQuantity} + side={side} + /> + setIsReviewCartOpen(false)} + itemQuantity={itemQuantity} + side={side} + /> {} + Pick {} const ReviewOrderFlyout = ({ itemQuantity, @@ -199,17 +196,19 @@ const ReviewOrderFlyout = ({ interface ItemDetailsProps extends ECommerceContentProps, - Pick {} + Pick {} const ItemDetailsFlyout = ({ onClose, itemQuantity, id = 'item-details-flyout', side = DEFAULT_SIDE, + isOpen, }: ItemDetailsProps) => { return ( - {isShoppingCartOpen && ( - setIsShoppingCartOpen(false)} - onQuantityChange={(delta: number) => - setItemQuantity(itemQuantity + delta) - } - itemQuantity={itemQuantity} - ownFocus={shoppingCartOwnFocus} - side={side} - /> - )} - {isReviewCartOpen && ( - setIsReviewCartOpen(false)} - itemQuantity={itemQuantity} - side={side} - /> - )} - {isItemDetailsOpen && ( - setIsItemDetailsOpen(false)} - itemQuantity={itemQuantity} - side={side} - /> - )} + setIsShoppingCartOpen(false)} + onQuantityChange={(delta: number) => + setItemQuantity(itemQuantity + delta) + } + itemQuantity={itemQuantity} + ownFocus={shoppingCartOwnFocus} + side={side} + /> + setIsReviewCartOpen(false)} + itemQuantity={itemQuantity} + side={side} + /> + setIsItemDetailsOpen(false)} + itemQuantity={itemQuantity} + side={side} + /> ); };