Skip to content

Conversation

jleven
Copy link
Contributor

@jleven jleven commented Oct 7, 2025

What changes are proposed in this pull request?

Two changes:

  1. Stops propagation of the Escape keyDown event when looker is a Modal
  2. Using the Escape key to clear selections in the grid now first opens a confirmation modal
image

How is this patch tested? If it is not, please explain why.

Clicked around in the app:

  1. Select samples in the grid
  2. Open modal
  3. Close modal with escape key
  4. Verify samples are still selected

Selecting and Clearing:

  1. Select samples in the grid
  2. Press escape key
  3. Verify confirmation dialog pops up
  4. Verify "cancel" does not clear selections / Verify "ok" does clear selections

Release Notes

Is this a user-facing change that should be mentioned in the release notes?

  • No. You can skip the rest of this section.
  • Yes. Give a description of this change to be included in the release
    notes for FiftyOne users.

Fixed bug where grid selections would be cleared when closing a modal using the escape key. Also added a confirmation dialog when clearing grid selections using the escape key.

What areas of FiftyOne does this PR affect?

  • App: FiftyOne application changes
  • Build: Build and test infrastructure changes
  • Core: Core fiftyone Python library changes
  • Documentation: FiftyOne documentation changes
  • Other

Summary by CodeRabbit

  • New Features

    • Adds a confirmation dialog before clearing all selections to prevent accidental loss.
  • Bug Fixes

    • Pressing Escape while a modal is open no longer triggers actions in the underlying grid.
    • Improved Escape key handling to avoid unintended selection resets; clearing now requires explicit confirmation.

Copy link
Contributor

coderabbitai bot commented Oct 7, 2025

Walkthrough

Updates Escape key handling across Grid and Looker: Grid now prompts for confirmation before clearing selections when no modal is active; Looker stops Escape propagation when controls/modal are shown to prevent Grid from receiving the event.

Changes

Cohort / File(s) Summary of Changes
Escape handling: Grid & Looker
app/packages/core/src/components/Grid/useEscape.ts, app/packages/looker/src/elements/common/looker.ts
Grid: replaced unconditional reset on Escape when modal is null with a confirmation dialog and conditional reset(fos.selectedSamples); added TODO about modal state correctness. Looker: added conditional stopPropagation() for Escape when state.options.showControls === true to prevent event bubbling to Grid.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant L as Looker (keydown handler)
  participant G as Grid useEscape
  participant D as Dialog (confirm)

  rect rgba(230,240,255,0.6)
    note over U,L: Updated Escape handling
  end

  U->>L: Press Escape
  alt Controls/modal visible (state.options.showControls === true)
    L-->>L: stopPropagation()
    note right of L: Prevents Escape from reaching Grid
  else No controls/modal
    L-->>G: keydown event (Escape)
    alt modal == null
      G->>D: Show confirm "You are about to clear all selections..."
      alt User confirms
        D-->>G: Confirmed
        G-->>G: reset(fos.selectedSamples)
      else User cancels
        D-->>G: Canceled
      end
    else modal active
      G-->>G: No reset triggered
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I tapped Esc with gentle paws so light,
A question popped to guard my flight.
Confirm or cancel, nibble, decide,
Selections safe—no sudden slide.
I hopped away, content and bright. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title accurately and concisely describes the Escape key propagation change, which is one of the two main updates in the PR. It clearly states the primary behavior modification being implemented without unnecessary details or noise.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Description Check ✅ Passed The pull request description includes all required sections from the template with a clear summary of proposed changes, detailed testing instructions, a filled-in release notes section including the appropriate checkbox selection and descriptive text, and correctly specifies the affected areas of FiftyOne.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch josh/explore-modal-close-selection-clear

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jleven jleven changed the title Figured out the flow of events. I don't see an easy fix. Stop propagation of the Escape event when looker is a Modal Oct 7, 2025
@jleven jleven changed the title Stop propagation of the Escape event when looker is a Modal Stop propagation of the Escape event when looker is a Modal [FOEPD-862] Oct 7, 2025
@jleven jleven marked this pull request as ready for review October 7, 2025 02:27
@jleven jleven requested a review from a team as a code owner October 7, 2025 02:27
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/packages/core/src/components/Grid/useEscape.ts (1)

24-26: Replace native confirm() with a React modal component.

Using the browser's native confirm() dialog breaks the application's visual consistency and is blocking, which prevents React from updating during the confirmation.

Consider using the application's modal system instead:

// Import the modal action
import { showConfirmationModal } from "@fiftyone/components";

// Replace confirm() with:
const confirmed = await showConfirmationModal({
  title: "Clear Selections",
  message: "You are about to clear all selections. This cannot be undone. Are you sure?",
  confirmText: "Clear",
  cancelText: "Cancel"
});

if (confirmed) {
  reset(fos.selectedSamples);
}

Note: Verify the exact modal component API in your codebase.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01cd890 and bf3d2c3.

📒 Files selected for processing (2)
  • app/packages/core/src/components/Grid/useEscape.ts (1 hunks)
  • app/packages/looker/src/elements/common/looker.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

⚙️ CodeRabbit configuration file

Review the Typescript and React code for conformity with best practices in React, Recoil, Graphql, and Typescript. Highlight any deviations.

Files:

  • app/packages/looker/src/elements/common/looker.ts
  • app/packages/core/src/components/Grid/useEscape.ts
🧬 Code graph analysis (1)
app/packages/core/src/components/Grid/useEscape.ts (1)
app/packages/looker/src/lookers/imavid/ima-vid-frame-samples.ts (1)
  • reset (237-243)
🔇 Additional comments (1)
app/packages/looker/src/elements/common/looker.ts (1)

44-45: Escape key propagation is already stopped synchronously. The update callback (including matchedControl.action) and the subsequent e.stopPropagation() run in the same call stack before the event bubbles, so Escape cannot escape before being stopped.

Comment on lines +21 to +30
// TODO: modal is always `null` here right after a modal closes, so this isn't the condition we want
if (modal === null) {
if (
confirm(
"You are about to clear all selections. This cannot be undone. Are you sure?"
)
) {
reset(fos.selectedSamples);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Critical: Acknowledged race condition makes this logic unreliable.

The TODO on line 21 explicitly states the condition is incorrect: modal is always null immediately after a modal closes due to a race condition. This means:

  1. The check modal === null doesn't reliably distinguish "no modal was open" from "modal just closed"
  2. Users closing a modal with Escape might immediately see a confirmation dialog, which is confusing UX
  3. The intended behavior (confirm before clearing when NO modal is open) may not work correctly

This requires a proper fix rather than shipping with a known bug. Consider:

  1. Use event propagation state: Since looker.ts now calls stopPropagation() for modal Escapes, check event.defaultPrevented or use a flag to determine if Escape came from a modal
  2. Use escapeKeyHandlerIdsAtom: This atom (checked on line 13-17) already tracks active escape handlers. Register the modal as an escape handler rather than relying on modal state
  3. Add a frame delay: Use requestAnimationFrame to defer the check, allowing modal state to update

Run the following to understand the modal lifecycle:


🏁 Script executed:

#!/bin/bash
# Description: Examine modal state management and escape handling

# Find modal selector definition and usage
rg -n --type=ts -C5 'modalSelector'

# Find escapeKeyHandlerIdsAtom usage
rg -n --type=ts -C5 'escapeKeyHandlerIdsAtom'

# Check if modals register as escape handlers
rg -n --type=ts -C3 'escapeKeyHandlerId.*modal|modal.*escapeKeyHandlerId'

Length of output: 34416


Grid/useEscape.ts: fix Escape handler race condition and replace native confirm

  • Checking modal === null immediately after a modal closes misfires, since modalSelector is reset on close; use event.defaultPrevented or track active handlers via escapeKeyHandlerIdsAtom instead of modal === null.
  • Replace the blocking window.confirm() with a non-blocking React modal/dialog for consistent UX.
  • Optionally defer the clear check (e.g. via requestAnimationFrame) to allow modalSelector state to settle.

Comment on lines +41 to +46
// If this is a Modal, we don't want Escape to bubble up to the Grid
// Hack: `lookerOptions` only sets showControl=true when a Modal
const isModal = state.options.showControls === true;
if (e.key === "Escape" && isModal) {
e.stopPropagation();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the modal detection approach and document the coupling.

The comment explicitly labels this as a "Hack" and relies on the assumption that lookerOptions only sets showControls=true for modals. This creates fragile coupling that could break if the options usage changes elsewhere.

Run the following script to verify that showControls is only used for modal contexts:

Additionally, consider:

  1. Extracting an explicit isModal property in the state/options to make this relationship explicit rather than relying on a side effect
  2. Documenting this coupling in both the looker options definition and this usage site

🏁 Script executed:

#!/bin/bash
# Description: Check all usages of showControls to verify it's only used for modals

# Search for all assignments to showControls
rg -n --type=ts -C3 'showControls\s*[:=]' 

# Search for all usages of options.showControls
rg -n --type=ts -C3 'options\.showControls'

Length of output: 10101


Introduce an explicit isModal flag and use it for Escape handling instead of showControls. state.options.showControls is also toggled for hover and initial display, so it isn’t a reliable modal indicator. Add an isModal property to your looker options/state and update this handler to check state.options.isModal rather than showControls.

🤖 Prompt for AI Agents
In app/packages/looker/src/elements/common/looker.ts around lines 41 to 46,
replace the current modal-detection that relies on state.options.showControls
with an explicit state.options.isModal flag: add isModal to the looker options
interface and default initialization (preserve existing showControls behavior),
update any code that constructs/merges looker options to accept and propagate
isModal, and change the Escape key handler to check state.options.isModal (and
call e.stopPropagation() only when true). Ensure type definitions and any
consumers are updated for the new flag and maintain backward compatibility by
defaulting isModal to false when not provided.

1. Stops propagation when looker is a Modal
2. Using the Escape key to clear selections in
   the grid now first opens a confirmation modal

Hack: used `showControls === true` as an isModal check

Ideally, looker would have an explicit isModel flag to check
@jleven jleven force-pushed the josh/explore-modal-close-selection-clear branch from bf3d2c3 to c1f3ddc Compare October 7, 2025 15:15
Copy link
Contributor

@brimoor brimoor left a comment

Choose a reason for hiding this comment

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

Just gave this a test locally and have a couple comments:

  1. Can we please update to not show the dialog box if there is no current selection?
  2. (for technical reviewers) may need to confirm that the dialog box strategy works in FOE too

if (modal === null) {
if (
confirm(
"You are about to clear all selections. This cannot be undone. Are you sure?"
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can we use this copy?

Are you sure you want to clear your current selection?

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.

2 participants