Skip to content

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

Open
tsullivan wants to merge 11 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 11 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'.

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').

How:

  • New EuiFlyoutCloseReason union + EuiFlyoutCloseMeta interface in flyout/types.ts.
  • Base flyout.component.tsx: widened the onClose signature; handleClose forwards an optional reason — 'escape' from the Escape handler, 'outside-click' from the overlay/outside‑click handler, and 'close-button' from the close button (base and menu). When no reason applies, meta is omitted entirely rather than guessing.
  • Managed flyout_managed.tsx: threads meta through the onClose wrapper. For closes that originate outside the flyout (a flyout removed from the manager store while still mounted, or swept up by a cascade), the reason comes from a store-level reason channel rather than being hardcoded at the call site.
  • store.ts: goBack() stamps the flyout(s) it removes with { reason: 'navigation-back' } (computed via a before/after diff of state.flyouts) into a transient pendingCloseMeta map. Managed flyouts read-and-clear it via a new consumeCloseMeta(flyoutId) store method, defaulting to 'navigation-cascade' when nothing was stamped. So 'navigation-back' is emitted only when goBack() actually caused the removal; every other store-driven removal (e.g. closeAllFlyouts) is 'navigation-cascade'.

Why the store owns the reason (not the component): only the store knows why a flyout was removed — goBack() vs. closeAllFlyouts() vs. a single closeFlyout(). An earlier approach inferred "removed while mounted ⇒ navigation-back" from the component, which mislabeled flyouts a cascade swept up while they were still mounted (e.g. a backgrounded main flyout in a multi-session stack reported navigation-back when another flyout's close cleared the group). Moving the decision to the store fixes that and removes reliance on React mount/unmount timing.

Forward compatibility: this channel is the handoff point for the upcoming extraction of history management into a separate package. That package will drive navigation by calling EUI store methods (e.g. goBack()) and rely on this mechanism to communicate the close reason — it never needs to touch pendingCloseMeta or know how the reason reaches the component. EUI stamps and flows the reason internally.

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.

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.
FlyoutManagerStore consumeCloseMeta Added New method (flyoutId: string) => EuiFlyoutCloseMeta | undefined. Reads and clears a one-shot close reason stamped by the store (e.g. by goBack). Primarily internal plumbing for managed flyouts.

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, the managed navigation-back / navigation-cascade paths, and the store-level reason channel (goBack stamping + consumeCloseMeta).
  • 🔧 Hard to integrate — No. Purely additive; no Kibana changes required to adopt.

Impact level: 🟢 Low

QA instructions for reviewer

  • Open the Multi-session Flyout Manager story and wire onClose to log meta.reason.
  • Click the X / close button on a flyout → logs close-button.
  • Press Escape → logs escape; click outside (overlay) → logs outside-click.
  • Open "Park hours", then "Facility reservations", then a child; click X → the clicked flyout logs close-button, while the child and the backgrounded "Park hours" log navigation-cascade (not navigation-back).
  • Use the Back button → the popped flyout logs navigation-back.

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 thread packages/eui/src/components/flyout/flyout.component.tsx
Comment thread packages/eui/src/components/flyout/manager/flyout_managed.tsx Outdated
Comment thread packages/eui/src/components/flyout/flyout.test.tsx

@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 thread packages/eui/src/components/flyout/manager/store.ts
* 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.

@elastic-vault-github-plugin-prod

Copy link
Copy Markdown
2 visual difference(s) found - expand to review, then click Approve visual changes to update baselines

euipopover (1 difference)

StoryBeforeAfterDiff
playground desktop

euidatagrid (1 difference)

StoryBeforeAfterDiff
additional controls options desktop

@elasticmachine

elasticmachine commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

💔 Build Failed

Failed CI Steps

History

@elasticmachine

Copy link
Copy Markdown
Collaborator

💔 Build Failed

Failed CI Steps

History

@tsullivan

Copy link
Copy Markdown
Member Author

@tkajtoch I've responded to the review from Copilot. I've also fixed a bug since you've last viewed.

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