Skip to content

[Flyout Manager] Expose the close source as a parameter to the onClose#9716

Open
tsullivan wants to merge 9 commits into
elastic:mainfrom
tsullivan:flyouts/reason-for-close
Open

[Flyout Manager] Expose the close source as a parameter to the onClose#9716
tsullivan wants to merge 9 commits into
elastic:mainfrom
tsullivan:flyouts/reason-for-close

Conversation

@tsullivan

@tsullivan tsullivan commented Jun 4, 2026

Copy link
Copy Markdown
Member

Summary

What: Adds an optional second meta argument to EuiFlyout's onClose callback — onClose(event, { reason }) — so consumers can tell why a flyout closed. The reason is one of 'close-button', 'escape', 'outside-click', and, for managed flyouts, 'navigation-back' or 'navigation-cascade'. Also exports the supporting types (EuiFlyoutCloseEvent, EuiFlyoutCloseReason, EuiFlyoutCloseMeta) — EuiFlyoutCloseEvent was previously not part of the public API.

Why: Managed-flyout consumers had no way to distinguish close sources at the event level. The tab‑switch cascade close and the back‑button close both fired an identical synthetic MouseEvent('navigation'). Kibana Discover's traces experience needs to preserve restorable flyout state on a tab switch but clear it on a back‑button click — impossible without fragile workarounds (inferring the source from React mount/unmount timing, or sniffing event.type === 'navigation', which is an internal detail and not public API).

How:

  • New EuiFlyoutCloseReason union + EuiFlyoutCloseMeta interface in flyout/types.ts.
  • Base flyout.component.tsx: widened the onClose signature; handleClose now forwards a reason (defaulting to 'close-button'), with 'escape' from the Escape handler, 'outside-click' from the overlay/outside‑click handler, and 'close-button' from the close button.
  • Managed flyout_managed.tsx: threads meta through the onClose wrapper, and emits 'navigation-cascade' on the parent‑unmount cascade path and 'navigation-back' on the back‑button path.
  • Exported the new types from flyout/index.ts.

Non‑obvious decision for reviewers: the synthetic MouseEvent('navigation') is intentionally kept as the first argument so any existing consumer relying on event.type === 'navigation' is not broken; they can migrate to meta.reason incrementally. The second meta argument is optional, so existing (event) => void handlers remain valid and behaviorally unchanged.

API Changes

component / parent prop / child change description
EuiFlyout / EuiFlyoutComponentProps onClose Changed (non-breaking) Signature widened to (event, meta?: { reason: EuiFlyoutCloseReason }) => void. Optional second arg; existing (event) => void handlers are unaffected.
Flyout types EuiFlyoutCloseReason Added New exported union: 'close-button' | 'escape' | 'outside-click' | 'navigation-back' | 'navigation-cascade'.
Flyout types EuiFlyoutCloseMeta Added New exported interface { reason: EuiFlyoutCloseReason } passed as onClose's second argument.
Flyout types EuiFlyoutCloseEvent Added (export) Existing type, now exported from the public API.

Screenshots

No visual changes — this is an API/behavior‑only change (a new optional callback argument). Nothing about flyout rendering, markup, or styling changes, so there is nothing to show before/after.

Impact Assessment

  • 🔴 Breaking changes — None. The new meta argument is optional; existing onClose={(event) => …} handlers compile and behave exactly as before.
  • 💅 Visual changes — None. No DOM, class, or style changes.
  • 🧪 Test impact — Low. No HTML structure / class-name / default-value changes, so snapshot and functional tests are unaffected. Adds new unit tests asserting the reason for close button, Escape, and the managed navigation-back / navigation-cascade paths.
  • 🔧 Hard to integrate — No. Purely additive; no Kibana changes required to adopt.

Impact level: 🟢 Low


A couple of things to fill in before you post:

  • Issue link — add Fixes #<issue> in the Summary if there's a tracking issue for the Discover request.
  • Changelog/PR number — the changelog file is currently changelogs/upcoming/9715.md (placeholder). Rename it to the real PR number once it's open, since the filename becomes the PR link.

Want me to also draft a short, conventional commit body for the squash-merge message?
─── Write your reply below this line ──────────────────────────

QA instructions for reviewer

Checklist before marking Ready for Review

Reviewer checklist

  • Approved Impact Assessment — Acceptable to merge given the consumer impact.
  • Approved Release Readiness — Docs, Figma, and migration info are sufficient to ship.

Copilot AI review requested due to automatic review settings June 4, 2026 22:59
@tsullivan tsullivan requested a review from a team as a code owner June 4, 2026 22:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR expands the EuiFlyout close callback API to optionally include close-source metadata (onClose(event, meta?: { reason })), enabling consumers to distinguish between close causes (close button, Escape, outside click, and managed-flyout navigation closes). It also exports supporting close-related types as part of the public flyout API.

Changes:

  • Added EuiFlyoutCloseReason + EuiFlyoutCloseMeta types and exported them (along with EuiFlyoutCloseEvent) from the flyout public index.
  • Updated EuiFlyoutComponent to forward a close reason for Escape, outside click, and the default close button.
  • Updated managed flyouts to pass navigation close reasons (navigation-back / navigation-cascade) and added/updated unit tests + changelog entry.

Reviewed changes

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

Show a summary per file
File Description
packages/eui/src/components/flyout/types.ts Adds new close reason/meta types to support passing close-source info.
packages/eui/src/components/flyout/flyout.component.tsx Extends onClose signature and threads close reason through core close paths.
packages/eui/src/components/flyout/manager/flyout_managed.tsx Threads meta through managed flyout closes and adds navigation-specific reasons.
packages/eui/src/components/flyout/manager/flyout_managed.test.tsx Adds assertions for managed navigation close reasons.
packages/eui/src/components/flyout/flyout.test.tsx Adds unit tests for base flyout close reasons (close button, Escape).
packages/eui/src/components/flyout/index.ts Exports new close-related types from the public flyout barrel.
packages/eui/changelogs/upcoming/9715.md Documents the additive onClose(event, meta) API change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +89 to +93
* The optional second `meta` argument describes why the flyout closed via
* `meta.reason` (e.g. `'close-button'`, `'escape'`, `'outside-click'`, and,
* for managed flyouts, `'navigation-back'` or `'navigation-cascade'`).
*/
onClose: (event: EuiFlyoutCloseEvent) => void;
onClose: (event: EuiFlyoutCloseEvent, meta?: EuiFlyoutCloseMeta) => void;
Comment on lines 307 to 311
wasRegisteredRef.current = false; // Prevent cleanup from double-firing onClose
if (onCloseCallbackRef.current) {
const event = e || new MouseEvent('click');
onCloseCallbackRef.current(event);
onCloseCallbackRef.current(event, meta);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

duplicate feedback, #9716 (comment) is the same

Comment on lines +1068 to +1073
it("passes reason 'escape' when the Escape key is pressed", () => {
const onClose = jest.fn();
render(<EuiFlyout onClose={onClose} />);

fireEvent.keyDown(document, { key: 'Escape' });

@tkajtoch tkajtoch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The changes look good, thank you for the PR! @tsullivan Before I approve, could you please review Copilot's comments please?

// flyouts it removes so the managed flyout can report `navigation-back`; any
// other removal (e.g. closeAllFlyouts cascade) leaves no stamp and defaults to
// `navigation-cascade`. Kept off reducer state because it is a per-close
// annotation, not persistent state.

@tsullivan tsullivan Jun 5, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This approach may feel a bit overcomplicated, but it serves two worthwhile purposes:

  • A transient "reason-channel" in the store is the correct approach because the store is the only place that knows why a flyout was removed — whether it was via goBack(), closeAllFlyouts() cascade, or some other path. The managed flyout component can't reliably infer the reason from the event alone or from mount/unmount timing; it needs the store to explicitly signal the cause at the moment of removal.
  • The reason channel prepares the code for the history management refactor because after the refactor extracts history management to a package outside of EUI, that package will call EUI's store methods (like goBack()) to navigate. The upcoming history package will communicate why a flyout closed by passing the reason through store.goBack(), and EUI flows the reason through the pendingCloseMeta channel.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Comment on lines +264 to +276
goBack: () => {
// Capture which flyouts goBack removes so they report `navigation-back`
// (vs. the default `navigation-cascade`). dispatch updates state
// synchronously, so a before/after diff reliably identifies them.
const removedBefore = currentState.flyouts.map((f) => f.flyoutId);
dispatch(goBackAction());
const remaining = new Set(currentState.flyouts.map((f) => f.flyoutId));
removedBefore.forEach((flyoutId) => {
if (!remaining.has(flyoutId)) {
pendingCloseMeta.set(flyoutId, { reason: 'navigation-back' });
}
});
},
* internally by managed flyouts to report the correct close reason; defaults
* to `navigation-cascade` when nothing was stashed.
*/
consumeCloseMeta: (flyoutId: string) => EuiFlyoutCloseMeta | undefined;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The EUI Flyout System is marked as a beta feature, so additive behavior should be OK.

Comment on lines +1056 to +1078
describe('onClose reason', () => {
it("passes reason 'close-button' when the close button is clicked", () => {
const onClose = jest.fn();
const { getByTestSubject } = render(<EuiFlyout onClose={onClose} />);

fireEvent.click(getByTestSubject('euiFlyoutCloseButton'));

expect(onClose).toHaveBeenCalledWith(expect.anything(), {
reason: 'close-button',
});
});

it("passes reason 'escape' when the Escape key is pressed", () => {
const onClose = jest.fn();
render(<EuiFlyout onClose={onClose} />);

fireEvent.keyDown(window, { key: 'Escape' });

expect(onClose).toHaveBeenCalledWith(expect.anything(), {
reason: 'escape',
});
});
});

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

'outside-click' does not originate in EUI code, and the test path is not reachable in jsdom. The functionality is supported by the third-party react-focus-on library, which EuiFocusTrap wraps. Looking at react-focus-on/dist/es5/Effect.js, its document-level mousedown listener:

  1. is only attached once the focus lock has activated (activeNode is set), and
  2. activation is driven by the focus-lock "medium" sidecar reacting to real focus events.

In jsdom this activation path doesn't reliably run, so the onClickOutside callback that carries the 'outside-click' reason never fires.

@elasticmachine

Copy link
Copy Markdown
Collaborator

💚 Build Succeeded

History

@elasticmachine

Copy link
Copy Markdown
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