Skip to content

[wip] flyout manager #8876

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

clintandrewhall
Copy link
Contributor

Summary

WIP

@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

History

// Generate or reuse a flyout id for this managed instance
const flyoutId = useRef(props.flyoutId || generateManagedFlyoutId());
// Check if this flyout is already rendered in the tree
const isRendered = useIsManagedFlyoutRendered();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does useIsManagedFlyoutRendered know if the flyout is already rendered?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the simple provider that wraps each rendered flyout, marked my comment with ⬆️

// Avoid adding duplicate flyouts
if (state.flyouts.find((f) => f.id === action.id)) return state;

// Add new flyout as active, set all others inactive, preserve order
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will have to make sure that opening and closing flyouts have their own actions, as opening and closing child flyouts should have no effect on the history.

  • Open a main flyout: adds an entry to the managed history
  • Close a main flyout: pops an entry off the managed history
  • Open or close a child flyout: doesn't change the number of items in history
  • Open a main flyout, then open a child flyout, then navigate to a subsequent flyout: the subsequent flyout should show without the child flyout from the previous flyout. If you go back to the previous flyout, the child flyout should re-open.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely something to cover with the concept of a "session". I'm working on that as we speak.

import type { EuiManagedFlyoutBaseProps } from './eui_flyout_managed';

export interface ManagedFlyoutState {
id: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should have mainId and childId instead of a single id, for the reasons stated here: https://github.com/elastic/eui/pull/8876/files#r2214360182

Comment on lines +42 to +50
/**
*
* The flyout system allows custom meta data to be provided by the consumer, in the "EuiManagedFlyoutSessionOpen*Options"
* interfaces. In the advanced use case, (WithHistoryApp), we're using metadata within the renderMainManagedFlyoutContent
* function as a conditional to determine which component to render in the main flyout.
*/
// interface WithHistoryAppMeta {
// ecommerceMainManagedFlyoutKey?: 'shoppingCart' | 'reviewOrder';
// }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment looks like a carryover from a different architecture. I see that the open flyout action can be dispatched with metadata, but I don't see why we would need it in this system.

onCloseProp(event);
};

const renderer = useCreateManagedFlyoutRenderer();
Copy link
Member

@tsullivan tsullivan Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like we would want a separate childFlyoutRenderer, because the props for main flyouts and child flyouts are sufficiently different. For example:

main flyout:

  • type can be overlay or push
  • If there is a child flyout, the main flyout size can't be l

child flyout:

  • type is hardcoded to overlay
  • backgroundStyle prop exists for "shaded" and "default" style variants
  • If the main flyout size is m, the child flyout can't also be m

The sizing will definitely become more flexible as we allow resizable and full-screen child flyouts, but I think the above restrictions are pretty basic and will still need to be in effect.

So I would think we would want something like

const { renderer, childRenderer } = useCreateManagedFlyoutRenderer();

>
{children}
{renderedManagedFlyouts.map(({ id, element }) => (
<ManagedFlyoutIsRenderedProvider key={id}>
Copy link
Contributor Author

@clintandrewhall clintandrewhall Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tsullivan ⬆️


// After closing, activate the last flyout if any remain
setTimeout(() => {
if (state.flyouts.length > 1) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a slight possibility of a race condition here if we're unlucky. state.flyouts could technically change between event cycles

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this was Windsurf's idea. I've removed it since.

@clintandrewhall clintandrewhall requested a review from Copilot July 19, 2025 12:48
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This is a work-in-progress pull request that introduces a flyout manager system to the EUI component library. The changes implement a managed flyout system that allows multiple flyouts to be stacked and coordinated through a centralized context provider.

  • Adds a new ManagedFlyoutProvider to the main EuiProvider for global flyout management
  • Implements a complete flyout management system with context, reducer, and hooks
  • Creates a new managed flyout API that can be used alongside the existing unmanaged flyouts

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/eui/src/components/provider/provider.tsx Integrates ManagedFlyoutProvider into the main EuiProvider component tree
packages/eui/src/components/provider/component_defaults/component_defaults.tsx Adds commented-out type definitions for future managed flyout component defaults
packages/eui/src/components/flyout/managed/types.ts Defines TypeScript interfaces and types for the managed flyout system
packages/eui/src/components/flyout/managed/reducer.ts Implements the state management reducer for flyout operations
packages/eui/src/components/flyout/managed/managed_flyouts.stories.tsx Provides Storybook examples demonstrating managed flyout usage
packages/eui/src/components/flyout/managed/index.ts Exports the public API for the managed flyout system
packages/eui/src/components/flyout/managed/hooks.ts Implements React hooks for interacting with the flyout manager
packages/eui/src/components/flyout/managed/eui_flyout_managed.tsx Core managed flyout component with registration and lifecycle management
packages/eui/src/components/flyout/managed/eui_flyout.tsx Unified flyout component that supports both managed and unmanaged modes
packages/eui/src/components/flyout/managed/context.tsx React context provider and related components for flyout state management
packages/eui/src/components/flyout/index.ts Updates flyout exports with commented references to managed flyouts
packages/eui/src/components/flyout/flyout.tsx Exports internal props interface for use in managed flyouts
packages/eui/src/components/flyout/flyout.stories.tsx Adds commented-out examples for component defaults integration

Comment on lines +68 to +75
setTimeout(() => {
if (state.flyouts.length > 1) {
const last = state.flyouts.filter((f) => f.id !== id).at(-1);
if (last) {
dispatch({ type: 'SET_ACTIVE', id: last.id });
}
}
});
Copy link
Preview

Copilot AI Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using setTimeout without a delay can cause race conditions and unpredictable behavior. The flyout state update should be handled synchronously or with proper async handling.

Suggested change
setTimeout(() => {
if (state.flyouts.length > 1) {
const last = state.flyouts.filter((f) => f.id !== id).at(-1);
if (last) {
dispatch({ type: 'SET_ACTIVE', id: last.id });
}
}
});
const remainingFlyouts = state.flyouts.filter((f) => f.id !== id);
if (remainingFlyouts.length > 0) {
const last = remainingFlyouts.at(-1);
if (last) {
dispatch({ type: 'SET_ACTIVE', id: last.id });
}
}

Copilot uses AI. Check for mistakes.

isOpen.current = true;
}
if (isOpen.current && !isRegistered) {
onClose?.({ type: 'external', flyoutId } as any);
Copy link
Preview

Copilot AI Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 'as any' bypasses TypeScript's type checking. Define a proper type for the onClose event parameter instead of using type assertion.

Suggested change
onClose?.({ type: 'external', flyoutId } as any);
onClose?.({ type: 'external', flyoutId });

Copilot uses AI. Check for mistakes.

setRenderedManagedFlyouts((prev) => prev.filter((f) => f.id !== id));
dispatch({ type: 'CLOSE_FLYOUT', id });
if (userOnClose) {
userOnClose({ type: 'internal', flyoutId: id } as any);
Copy link
Preview

Copilot AI Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 'as any' bypasses TypeScript's type checking. Define a proper type for the onClose event parameter instead of using type assertion.

Copilot uses AI. Check for mistakes.

</EuiButton>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButton onClick={(e: any) => onClose(e)} color="danger">
Copy link
Preview

Copilot AI Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 'any' type for event parameter reduces type safety. Use the proper event type from React instead.

Suggested change
<EuiButton onClick={(e: any) => onClose(e)} color="danger">
<EuiButton onClick={(e: React.MouseEvent<HTMLButtonElement>) => onClose(e)} color="danger">

Copilot uses AI. Check for mistakes.

Go back
</EuiButton>
)}{' '}
<EuiButton onClick={(e: any) => props.onClose(e)} color="danger">
Copy link
Preview

Copilot AI Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 'any' type for event parameter reduces type safety. Use the proper event type from React instead.

Suggested change
<EuiButton onClick={(e: any) => props.onClose(e)} color="danger">
<EuiButton onClick={(e: React.MouseEvent<HTMLButtonElement>) => props.onClose(e)} color="danger">

Copilot uses AI. Check for mistakes.

</EuiText>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButton onClick={(e: any) => onClose(e)} color="danger">
Copy link
Preview

Copilot AI Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 'any' type for event parameter reduces type safety. Use the proper event type from React instead.

Suggested change
<EuiButton onClick={(e: any) => onClose(e)} color="danger">
<EuiButton onClick={(e: React.MouseEvent<HTMLButtonElement>) => onClose(e)} color="danger">

Copilot uses AI. Check for mistakes.

function isChildManagedFlyout(
props: EuiManagedFlyoutBaseProps
): props is ChildFlyoutProps {
return (props as any).level === 'child';
Copy link
Preview

Copilot AI Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 'as any' bypasses TypeScript's type checking. Use proper type narrowing or define a discriminated union for better type safety.

Suggested change
return (props as any).level === 'child';
return props.level === 'child';

Copilot uses AI. Check for mistakes.

@clintandrewhall clintandrewhall force-pushed the exploration/flyout_manager branch from aa98d9b to 3a467a8 Compare July 23, 2025 00:52
@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

History

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants