Skip to content

Commit 68b8592

Browse files
[wip] flyout manager
1 parent 1fb4398 commit 68b8592

File tree

13 files changed

+868
-6
lines changed

13 files changed

+868
-6
lines changed

packages/eui/src/components/flyout/flyout.stories.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ import React, { useRef, useState } from 'react';
1010
import type { Meta, StoryObj } from '@storybook/react';
1111
import { action } from '@storybook/addon-actions';
1212

13-
import { EuiButton, EuiCallOut, EuiSpacer, EuiText, EuiTitle } from '../index';
13+
import {
14+
EuiButton,
15+
EuiCallOut,
16+
// EuiComponentDefaultsProvider,
17+
EuiSpacer,
18+
EuiText,
19+
EuiTitle,
20+
} from '../index';
1421

1522
import {
1623
EuiFlyout,
@@ -68,6 +75,9 @@ const StatefulFlyout = (
6875
};
6976

7077
return (
78+
// <EuiComponentDefaultsProvider
79+
// componentDefaults={{ EuiFlyout: { managed: false } }}
80+
// >
7181
<>
7282
<EuiButton size="s" onClick={() => handleToggle(!_isOpen)}>
7383
Toggle flyout
@@ -82,6 +92,7 @@ const StatefulFlyout = (
8292
/>
8393
)}
8494
</>
95+
// </EuiComponentDefaultsProvider>
8596
);
8697
};
8798

packages/eui/src/components/flyout/flyout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ function isEuiFlyoutSizeNamed(value: any): value is EuiFlyoutSize {
7373
export const PADDING_SIZES = ['none', 's', 'm', 'l'] as const;
7474
export type _EuiFlyoutPaddingSize = (typeof PADDING_SIZES)[number];
7575

76-
interface _EuiFlyoutProps {
76+
export interface _EuiFlyoutProps {
7777
onClose: (event: MouseEvent | TouchEvent | KeyboardEvent) => void;
7878
/**
7979
* Defines the width of the panel.

packages/eui/src/components/flyout/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66
* Side Public License, v 1.
77
*/
88

9-
export type { EuiFlyoutProps, EuiFlyoutSize } from './flyout';
109
export { EuiFlyout } from './flyout';
10+
export type { EuiFlyoutProps } from './flyout';
11+
12+
// When props can be better aligned, we can switch to `managed`.
13+
// export { EuiFlyout } from './managed';
14+
// export type { EuiFlyoutProps } from './managed';
1115

1216
export type { EuiFlyoutBodyProps } from './flyout_body';
1317
export { EuiFlyoutBody } from './flyout_body';
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import React, {
10+
createContext,
11+
useReducer,
12+
ReactNode,
13+
useCallback,
14+
} from 'react';
15+
import { generateManagedFlyoutId } from './eui_flyout';
16+
import type {
17+
ManagedFlyoutContextValue,
18+
RenderedManagedFlyout,
19+
RenderManagedFlyoutParams,
20+
RenderManagedFlyoutCallback,
21+
RenderManagedFlyoutProps,
22+
} from './types';
23+
import { reducer, initialState } from './reducer';
24+
25+
/**
26+
* Context for managing flyouts.
27+
*/
28+
export const ManagedFlyoutContext = createContext<
29+
ManagedFlyoutContextValue | undefined
30+
>(undefined);
31+
32+
/**
33+
* Provider component for ManagedFlyout context.
34+
*/
35+
export const ManagedFlyoutProvider = ({
36+
children,
37+
}: {
38+
children: ReactNode;
39+
}) => {
40+
const [state, dispatch] = useReducer(reducer, initialState);
41+
const [renderedManagedFlyouts, setRenderedManagedFlyouts] = React.useState<
42+
RenderedManagedFlyout[]
43+
>([]);
44+
45+
/**
46+
* Add a new flyout and set it as active.
47+
*/
48+
const addManagedFlyout = useCallback(
49+
(id: string, meta?: Record<string, any>) => {
50+
if (state.flyouts.find((f) => f.id === id)) {
51+
return;
52+
}
53+
dispatch({ type: 'ADD_FLYOUT', id, meta });
54+
dispatch({ type: 'SET_ACTIVE', id });
55+
},
56+
[state.flyouts, dispatch]
57+
);
58+
59+
/**
60+
* Close a flyout by id. Activates the last flyout if any remain.
61+
*/
62+
const closeManagedFlyout = useCallback(
63+
(id: string) => {
64+
dispatch({ type: 'CLOSE_FLYOUT', id });
65+
setRenderedManagedFlyouts((prev) => prev.filter((f) => f.id !== id));
66+
67+
// After closing, activate the last flyout if any remain
68+
const remainingFlyouts = state.flyouts.filter((f) => f.id !== id);
69+
70+
if (remainingFlyouts.length > 0) {
71+
const last = remainingFlyouts.at(-1);
72+
if (last) {
73+
dispatch({ type: 'SET_ACTIVE', id: last.id });
74+
}
75+
}
76+
},
77+
[state.flyouts, dispatch]
78+
);
79+
80+
/**
81+
* Returns a callback to render a managed flyout.
82+
*/
83+
const createManagedFlyoutRenderer = (
84+
params?: RenderManagedFlyoutParams
85+
): RenderManagedFlyoutCallback => {
86+
const { id: providedId, onClose: userOnClose } = params || {};
87+
const id = providedId || generateManagedFlyoutId();
88+
89+
const renderer: RenderManagedFlyoutCallback = (
90+
fn: (props: Required<RenderManagedFlyoutProps>) => React.ReactNode
91+
) => {
92+
if (renderedManagedFlyouts.find((f) => f.id === id)) {
93+
return id;
94+
}
95+
96+
const onClose = () => {
97+
setRenderedManagedFlyouts((prev) => prev.filter((f) => f.id !== id));
98+
dispatch({ type: 'CLOSE_FLYOUT', id });
99+
if (userOnClose) {
100+
userOnClose({ type: 'internal', flyoutId: id } as any);
101+
}
102+
};
103+
104+
setRenderedManagedFlyouts((prev) => [
105+
...prev,
106+
{
107+
id,
108+
onClose,
109+
element: fn({ id, onClose }),
110+
},
111+
]);
112+
return id;
113+
};
114+
115+
return renderer;
116+
};
117+
118+
return (
119+
<ManagedFlyoutContext.Provider
120+
value={{
121+
flyouts: state.flyouts,
122+
activeFlyoutId: state.activeFlyoutId,
123+
addManagedFlyout,
124+
closeManagedFlyout,
125+
createManagedFlyoutRenderer,
126+
}}
127+
>
128+
{children}
129+
{renderedManagedFlyouts.map(({ id, element }) => (
130+
<ManagedFlyoutIsRenderedProvider key={id}>
131+
{element}
132+
</ManagedFlyoutIsRenderedProvider>
133+
))}
134+
</ManagedFlyoutContext.Provider>
135+
);
136+
};
137+
138+
/**
139+
* Context to track if a flyout is rendered.
140+
*/
141+
export const ManagedFlyoutIsRenderedContext = createContext<boolean>(false);
142+
143+
/**
144+
* Provider to indicate a flyout is rendered.
145+
*/
146+
export const ManagedFlyoutIsRenderedProvider = ({
147+
children,
148+
}: {
149+
children: ReactNode;
150+
}) => (
151+
<ManagedFlyoutIsRenderedContext.Provider value={true}>
152+
{children}
153+
</ManagedFlyoutIsRenderedContext.Provider>
154+
);
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import React, { useRef, useEffect } from 'react';
10+
import {
11+
EuiFlyout as EuiUnmanagedFlyout,
12+
_EuiFlyoutProps as EuiUnmanagedFlyoutProps,
13+
} from '../flyout';
14+
import {
15+
EuiManagedFlyout,
16+
EuiManagedFlyoutBaseProps,
17+
} from './eui_flyout_managed';
18+
// import { usePropsWithComponentDefaults } from '../../provider/component_defaults';
19+
import { htmlIdGenerator } from '../../../services';
20+
import {
21+
useCreateManagedFlyoutRenderer,
22+
useIsManagedFlyoutRendered,
23+
} from './hooks';
24+
25+
/**
26+
* Props for an unmanaged flyout. Used when `managed` is false or undefined.
27+
*/
28+
interface UnmanagedProps extends EuiUnmanagedFlyoutProps {
29+
managed?: false;
30+
}
31+
32+
/**
33+
* Props for a managed flyout. Used when `managed` is true.
34+
*/
35+
type ManagedProps = EuiManagedFlyoutBaseProps & {
36+
flyoutId?: string;
37+
managed?: true;
38+
};
39+
40+
/**
41+
* Props for the EuiFlyout component. Can be either managed or unmanaged.
42+
*/
43+
export type EuiFlyoutProps = UnmanagedProps | ManagedProps;
44+
45+
/**
46+
* Generates a unique managed flyout id, optionally namespaced by a provided id.
47+
*/
48+
export const generateManagedFlyoutId = (id?: string) =>
49+
htmlIdGenerator('euiManagedFlyout')(id);
50+
51+
/**
52+
* Type guard for managed flyout props.
53+
*/
54+
// function isManaged(props: EuiFlyoutProps): props is ManagedProps {
55+
// return props.managed === true;
56+
// }
57+
58+
/**
59+
* Internal component for rendering a managed flyout. Handles registration and rendering
60+
* of the flyout instance in the flyout manager context.
61+
*/
62+
const InternalManagedFlyout = (props: ManagedProps) => {
63+
// Generate or reuse a flyout id for this managed instance
64+
const flyoutId = useRef(props.flyoutId || generateManagedFlyoutId());
65+
// Check if this flyout is already rendered in the tree
66+
const isRendered = useIsManagedFlyoutRendered();
67+
68+
// Get a callback that will render this flyout when invoked
69+
const createManagedFlyoutRenderer = useCreateManagedFlyoutRenderer({
70+
id: flyoutId.current,
71+
onClose: props.onClose,
72+
});
73+
74+
useEffect(() => {
75+
// Only create a renderer for the flyout if not already rendered
76+
if (!isRendered) {
77+
flyoutId.current = createManagedFlyoutRenderer(
78+
({ onClose, id: flyoutId }) => (
79+
<EuiManagedFlyout {...{ ...props, onClose, flyoutId }} />
80+
)
81+
);
82+
}
83+
}, [isRendered, props, createManagedFlyoutRenderer]);
84+
85+
return <EuiManagedFlyout {...{ ...props, flyoutId: flyoutId.current }} />;
86+
};
87+
88+
/**
89+
* EuiFlyout component. Renders either a managed or unmanaged flyout based on props.
90+
*
91+
* - If `managed` is true, uses InternalManagedFlyout (which uses context for stacking, etc).
92+
* - Otherwise, renders the legacy/unmanaged flyout.
93+
*/
94+
export const EuiFlyout = (initialProps: EuiFlyoutProps) => {
95+
// const propsWithDefaults = usePropsWithComponentDefaults(
96+
// 'EuiFlyout',
97+
// initialProps
98+
// );
99+
100+
const { managed, ...props } = initialProps;
101+
102+
if (managed) {
103+
return <InternalManagedFlyout {...props} />;
104+
} else {
105+
return <EuiUnmanagedFlyout {...props} />;
106+
}
107+
};

0 commit comments

Comments
 (0)