Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
eddbcc4
initial commit of flyout-child
tsullivan Jun 6, 2025
e9d9021
refine stories
tsullivan Jun 7, 2025
10b1370
calculate breakpoint size where layout changes from side-by-side to s…
tsullivan Jun 9, 2025
1458eee
Add childLayoutMode to context
tsullivan Jun 9, 2025
25f754e
EuiFlyoutChildManager
tsullivan Jun 10, 2025
a2a577e
Organization & cleanup
tsullivan Jun 10, 2025
5371f4e
Fix when different size child flyout stacked above parent
tsullivan Jun 10, 2025
eb0350e
Cleanup / comments
tsullivan Jun 10, 2025
ed6dded
add unit test
tsullivan Jun 10, 2025
6c8c91f
add dispatcher story example
tsullivan Jun 10, 2025
c97191e
clean up generics
tsullivan Jun 18, 2025
a89515f
clean up transition styles
tsullivan Jun 18, 2025
b0d385d
Clean up structures/exports
tsullivan Jun 18, 2025
f4ce5c4
add README for state management
tsullivan Jun 20, 2025
510e59d
Naming adjustments
tsullivan Jun 20, 2025
0bb44a8
changelog
tsullivan Jun 20, 2025
eb05842
Merge branch 'main' into flyout-child
tsullivan Jun 23, 2025
2a7fd09
Merge branch 'main' into flyout-child
tsullivan Jun 24, 2025
4b7a3ad
update "complete example" with a button that opens both parent and ch…
tsullivan Jun 24, 2025
80ffbc8
update comments and exports
tsullivan Jun 24, 2025
4036f18
Expose useEuiFlyoutSessionContext
tsullivan Jun 24, 2025
5d1d529
Make `meta` optional
tsullivan Jun 24, 2025
d0a1701
Add openFlyoutGroup
tsullivan Jun 24, 2025
42f8bf7
update sessions/README.md
tsullivan Jun 25, 2025
f02a3b9
fix the incorrect y-scroll style that added linear-gradient
tsullivan Jun 25, 2025
ec1a72c
Update packages/eui/src/components/flyout/flyout.tsx
tsullivan Jun 25, 2025
5d9020e
add fallback to type.displayName for EuiFlyoutBody
tsullivan Jun 25, 2025
89d79d4
Merge branch 'flyout-child' of github.com:tsullivan/eui into flyout-c…
tsullivan Jun 25, 2025
a3b4483
Remove redundant example
tsullivan Jun 25, 2025
70a5ea1
managed state: fix close handler for flyout child
tsullivan Jun 25, 2025
2a22870
Make room for more more flyout_provider stories
tsullivan Jun 25, 2025
f491476
Add story for `openFlyoutGroup`
tsullivan Jun 25, 2025
f12ba50
Set max width on child flyout, using helper from main flyout styles
tsullivan Jun 26, 2025
565cf2d
Merge branch 'main' into flyout-child
tsullivan Jun 26, 2025
4a97303
simplify render child flyout content function in "with History" story
tsullivan Jun 26, 2025
b62fb57
Merge branch 'main' into flyout-child
tsullivan Jun 27, 2025
180089c
Merge branch 'main' into flyout-child
tsullivan Jun 27, 2025
51c141d
Improve flyout action reducer’s handling “go back” actions
tsullivan Jun 27, 2025
d23b5ad
Flyout provider storybook polish
tsullivan Jun 27, 2025
8e8e8f2
Add more testing
tsullivan Jun 27, 2025
f660f6b
Merge branch 'flyout-child' of github.com:tsullivan/eui into flyout-c…
tsullivan Jun 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eui/changelogs/upcoming/8771.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added `EuiFlyoutChild` and `EuiFlyoutSessionProvider`
Copy link
Member

Choose a reason for hiding this comment

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

I believe in this case it's okay to not release the new EuiFlyoutChild as a beta component

Copy link
Member Author

Choose a reason for hiding this comment

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

What would be different here if EuiFlyoutChild is initially released as a beta component? I have a feeling it may be decided that we SHOULD have the initial release be for a beta.

Copy link
Member

Choose a reason for hiding this comment

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

From our perspective, it's just the communication around it. Since there's no public-facing documentation around it, we would kind of treat it as a non-public preview for Kibana purposes only. However, there's a changelog item (and EuiFlyoutChild will be included in next week's EUI release if we merge it this week), so it would be worth it adding a note it's a beta component for the time being

27 changes: 21 additions & 6 deletions packages/eui/src/components/flyout/flyout.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,24 @@ export const euiFlyoutStyles = (euiThemeContext: UseEuiTheme) => {
outline: none;
}

${euiMaxBreakpoint(euiThemeContext, FLYOUT_BREAKPOINT)} {
/* 1. Leave only a small sliver exposed on small screens so users understand that this is not a new page
2. If a custom maxWidth is set, we need to override it. */
${logicalCSS('max-width', '90vw !important')}
}
${maxedFlyoutWidth(euiThemeContext)}
`,

// Flyout sizes
// When a child flyout is stacked on top of the parent, the parent flyout size will match the child flyout size
s: css`
${composeFlyoutSizing(euiThemeContext, 's')}

&.euiFlyout--hasChild--stacked.euiFlyout--hasChild--m {
${composeFlyoutSizing(euiThemeContext, 'm')}
}
`,
m: css`
${composeFlyoutSizing(euiThemeContext, 'm')}

&.euiFlyout--hasChild--stacked.euiFlyout--hasChild--s {
${composeFlyoutSizing(euiThemeContext, 's')}
}
`,
l: css`
${composeFlyoutSizing(euiThemeContext, 'l')}
Expand All @@ -94,6 +99,10 @@ export const euiFlyoutStyles = (euiThemeContext: UseEuiTheme) => {
animation: ${euiFlyoutSlideInRight} ${euiTheme.animation.normal}
${euiTheme.animation.resistance};
}

&.euiFlyout--hasChild {
clip-path: none;
}
`,
// Left-side flyouts should only be used for navigation
left: css`
Expand Down Expand Up @@ -166,7 +175,13 @@ export const euiFlyoutStyles = (euiThemeContext: UseEuiTheme) => {
};
};

const composeFlyoutSizing = (
export const maxedFlyoutWidth = (euiThemeContext: UseEuiTheme) => `
${euiMaxBreakpoint(euiThemeContext, FLYOUT_BREAKPOINT)} {
${logicalCSS('max-width', '90vw !important')}
}
`;

export const composeFlyoutSizing = (
euiThemeContext: UseEuiTheme,
size: EuiFlyoutSize
) => {
Expand Down
119 changes: 103 additions & 16 deletions packages/eui/src/components/flyout/flyout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import React, {
ComponentProps,
useEffect,
useRef,
useMemo,
Expand Down Expand Up @@ -45,6 +46,8 @@ import { EuiScreenReaderOnly } from '../accessibility';

import { EuiFlyoutCloseButton } from './_flyout_close_button';
import { euiFlyoutStyles } from './flyout.styles';
import { EuiFlyoutChild } from './flyout_child';
import { EuiFlyoutChildProvider } from './flyout_child_manager';

export const TYPES = ['push', 'overlay'] as const;
type _EuiFlyoutType = (typeof TYPES)[number];
Expand Down Expand Up @@ -182,7 +185,7 @@ export const EuiFlyout = forwardRef(
as,
hideCloseButton = false,
closeButtonProps,
closeButtonPosition = 'inside',
closeButtonPosition: _closeButtonPosition = 'inside',
onClose,
ownFocus = true,
side = 'right',
Expand All @@ -208,6 +211,54 @@ export const EuiFlyout = forwardRef(
const Element = as || defaultElement;
const maskRef = useRef<HTMLDivElement>(null);

// Ref for the main flyout element to pass to context
const internalParentFlyoutRef = useRef<HTMLDivElement>(null);

const [isChildFlyoutOpen, setIsChildFlyoutOpen] = useState(false);
const [childLayoutMode, setChildLayoutMode] = useState<
'side-by-side' | 'stacked'
>('side-by-side');

// Check for child flyout
const childFlyoutElement = React.Children.toArray(children).find(
(child) =>
React.isValidElement(child) &&
(child.type === EuiFlyoutChild ||
(child.type as any).displayName === 'EuiFlyoutChild')
Copy link
Member

Choose a reason for hiding this comment

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

Q: Is the displayName fallback needed at all here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I considered that when code is minified/optimized in production builds, component references can change, making direct component type comparisons unreliable. However, if the check only referred to type.displayName, it would be slightly less efficient since that direct check stands a chance of passing

) as React.ReactElement<ComponentProps<typeof EuiFlyoutChild>> | undefined;

const hasChildFlyout = !!childFlyoutElement;

// Validate props, determine close button position and set child flyout classes
let closeButtonPosition: 'inside' | 'outside';
let childFlyoutClasses: string[] = [];
if (hasChildFlyout) {
if (side !== 'right') {
throw new Error(
'EuiFlyout: When an EuiFlyoutChild is present, the `side` prop of EuiFlyout must be "right".'
);
}
if (!isEuiFlyoutSizeNamed(size) || !['s', 'm'].includes(size)) {
throw new Error(
`EuiFlyout: When an EuiFlyoutChild is present, the \`size\` prop of EuiFlyout must be "s" or "m". Received "${size}".`
);
}
if (_closeButtonPosition !== 'inside') {
throw new Error(
'EuiFlyout: When an EuiFlyoutChild is present, the `closeButtonPosition` prop of EuiFlyout must be "inside".'
);
}

closeButtonPosition = 'inside';
childFlyoutClasses = [
'euiFlyout--hasChild',
Copy link
Member

Choose a reason for hiding this comment

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

nit: We prefer to compose styles using Emotion's css prop instead of class names to easier track unused styles, but I'm fine leaving it as is for now

`euiFlyout--hasChild--${childLayoutMode}`,
`euiFlyout--hasChild--${childFlyoutElement.props.size || 's'}`,
];
} else {
closeButtonPosition = _closeButtonPosition;
}

const windowIsLargeEnoughToPush =
useIsWithinMinBreakpoint(pushMinBreakpoint);
const isPushed = type === 'push' && windowIsLargeEnoughToPush;
Expand All @@ -219,7 +270,11 @@ export const EuiFlyout = forwardRef(
const [resizeRef, setResizeRef] = useState<ComponentPropsWithRef<T> | null>(
null
);
const setRef = useCombinedRefs([setResizeRef, ref]);
const setRef = useCombinedRefs([
setResizeRef,
ref,
internalParentFlyoutRef,
]);
const { width } = useResizeObserver(isPushed ? resizeRef : null, 'width');

useEffect(() => {
Expand Down Expand Up @@ -289,11 +344,19 @@ export const EuiFlyout = forwardRef(
styles[side],
];

const classes = classnames('euiFlyout', className);
const classes = classnames('euiFlyout', ...childFlyoutClasses, className);

/*
* If not disabled, automatically add fixed EuiHeaders as shards
* to EuiFlyout focus traps, to prevent focus fighting
* Trap focus even when `ownFocus={false}`, otherwise closing
* the flyout won't return focus to the originating button.
*
* Set `clickOutsideDisables={true}` when `ownFocus={false}`
* to allow non-keyboard users the ability to interact with
* elements outside the flyout.
*
* Set `onClickOutside={onClose}` when `ownFocus` and `type` are the defaults,
* or if `outsideClickCloses={true}` to close on clicks that target
* (both mousedown and mouseup) the overlay mask.
*/
const flyoutToggle = useRef<Element | null>(document.activeElement);
const [fixedHeaders, setFixedHeaders] = useState<HTMLDivElement[]>([]);
Expand Down Expand Up @@ -323,7 +386,7 @@ export const EuiFlyout = forwardRef(
..._focusTrapProps,
shards: [...fixedHeaders, ...(_focusTrapProps?.shards || [])],
}),
[fixedHeaders, _focusTrapProps]
[_focusTrapProps, fixedHeaders]
);

/*
Expand Down Expand Up @@ -389,6 +452,30 @@ export const EuiFlyout = forwardRef(
[onClose, hasOverlayMask, outsideClickCloses]
);

const closeButton = !hideCloseButton && (
<EuiFlyoutCloseButton
{...closeButtonProps}
onClose={onClose}
closeButtonPosition={closeButtonPosition}
side={side}
/>
);

// render content within EuiFlyoutChildProvider if childFlyoutElement is present
let contentToRender: React.ReactElement = children;
if (hasChildFlyout && childFlyoutElement) {
contentToRender = (
<EuiFlyoutChildProvider
parentSize={size as 's' | 'm'}
parentFlyoutRef={internalParentFlyoutRef}
childElement={childFlyoutElement}
childrenToRender={children}
reportIsChildOpen={setIsChildFlyoutOpen}
reportChildLayoutMode={setChildLayoutMode}
/>
);
}

return (
<EuiFlyoutWrapper
hasOverlayMask={hasOverlayMask}
Expand All @@ -400,10 +487,17 @@ export const EuiFlyout = forwardRef(
>
<EuiWindowEvent event="keydown" handler={onKeyDown} />
<EuiFocusTrap
disabled={isPushed}
disabled={isPushed || (ownFocus && isChildFlyoutOpen)}
scrollLock={hasOverlayMask}
clickOutsideDisables={!ownFocus}
onClickOutside={onClickOutside}
returnFocus={() => {
if (!isChildFlyoutOpen && flyoutToggle.current) {
(flyoutToggle.current as HTMLElement).focus();
return false; // We've handled focus
}
return true;
}}
{...focusTrapProps}
>
<Element
Expand All @@ -419,15 +513,8 @@ export const EuiFlyout = forwardRef(
data-autofocus={!isPushed || undefined}
>
{!isPushed && screenReaderDescription}
{!hideCloseButton && onClose && (
<EuiFlyoutCloseButton
{...closeButtonProps}
onClose={onClose}
closeButtonPosition={closeButtonPosition}
side={side}
/>
)}
{children}
{closeButton}
{contentToRender}
</Element>
</EuiFocusTrap>
</EuiFlyoutWrapper>
Expand Down
Loading