-
Notifications
You must be signed in to change notification settings - Fork 856
[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
base: main
Are you sure you want to change the base?
[wip] flyout manager #8876
Conversation
💚 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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
/** | ||
* | ||
* 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'; | ||
// } |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 beoverlay
orpush
- If there is a child flyout, the main flyout size can't be
l
child flyout:
type
is hardcoded tooverlay
backgroundStyle
prop exists for "shaded" and "default" style variants- If the main flyout size is
m
, the child flyout can't also bem
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}> |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
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 }); | ||
} | ||
} | ||
}); |
There was a problem hiding this comment.
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.
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); |
There was a problem hiding this comment.
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.
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); |
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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.
<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"> |
There was a problem hiding this comment.
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.
<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"> |
There was a problem hiding this comment.
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.
<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'; |
There was a problem hiding this comment.
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.
return (props as any).level === 'child'; | |
return props.level === 'child'; |
Copilot uses AI. Check for mistakes.
aa98d9b
to
3a467a8
Compare
💚 Build SucceededHistory
|
Summary
WIP