-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Problem
After opening a modal from the ActionMenu (sprite selector) and closing it, the tooltip "Choose a Sprite" reappears when it should remain hidden.
Environment:
- Affected:
packages/scratch-gui(React 18) - Working:
gui/smalruby3-gui(React 16) - Component:
packages/scratch-gui/src/components/action-menu/action-menu.jsx
Steps to Reproduce
- Start dev server and navigate to sprite selector
- Hover mouse over ActionMenu button → tooltip "Choose a Sprite" appears
- Move mouse up to expand action menu
- Click any action item (e.g., "Surprise", "Paint", "Choose a Sprite") to open modal
- Close the modal
- Bug: Tooltip "Choose a Sprite" reappears (should stay hidden)
Investigation Results
Initial Hypothesis (Partially Correct)
Race condition between React 18 event handling and multiple setTimeout operations:
- Zero-delay setTimeout resets
forceHide: falseimmediately - Phantom
onMouseLeaveevents during modal display trigger 300ms timeout - React 18's root-level event attachment causes different timing than React 16
Actual Root Causes (Discovered Through Testing)
-
File input buttons bypass
clickDelayer(line 186):onClick={hasFileInput ? handleClick : this.clickDelayer(handleClick)}
- "Upload Sprite" button has
fileInput, so it bypassed tooltip hiding logic - Only "Upload Sprite" worked correctly (file dialog steals focus)
- Other buttons ("Surprise", "Paint", "Choose a Sprite") showed the bug
- "Upload Sprite" button has
-
Automatic
forceHidereset after 300ms:setTimeout(() => this.setState({forceHide: false}), CLOSE_DELAY);
forceHide: falseresets while modal is still opening/closing- If mouse is still over ActionMenu area, ReactTooltip re-activates
- 300ms is too short - modal takes longer to open and capture focus
-
ReactTooltip Portal rendering:
- ReactTooltip renders in Portal outside ActionMenu DOM
- CSS
.force-hidden .tooltipdoesn't affect Portal-rendered tooltips - Need to control ReactTooltip state programmatically
Why It Works in React 16 but Not React 18
| Aspect | React 16 | React 18 |
|---|---|---|
| Event attachment | Document level | React root level |
| Event timing | After React synthetic events | May fire before synthetic events |
| Mouse event behavior | Predictable sequence | Can trigger during reconciliation |
| Tooltip management | Natural timing works | Requires explicit control |
Final Solution
1. Ensure All Buttons Use clickDelayer (line 206)
Before:
onClick={hasFileInput ? handleClick : this.clickDelayer(handleClick)}After:
onClick={this.clickDelayer(handleClick)}All sub-menu buttons (including file input) now go through clickDelayer.
2. Add ignoreMouseLeave Flag (line 29)
this.ignoreMouseLeave = false;Prevents phantom onMouseLeave events during modal display from scheduling close timeout.
3. Ignore Mouse Leave During Click (lines 55-61)
handleClosePopover () {
if (this.ignoreMouseLeave) {
return; // Ignore phantom events
}
this.closeTimeoutId = setTimeout(() => {
ReactTooltip.hide();
this.setState({isOpen: false});
this.closeTimeoutId = null;
}, CLOSE_DELAY);
}4. Set Flag in clickDelayer (lines 99, 115)
this.ignoreMouseLeave = true;Prevents subsequent handleClosePopover calls from scheduling timeouts.
5. Remove Automatic forceHide Reset (lines 122-124)
Before:
this.setState({forceHide: true, isOpen: false}, () => {
setTimeout(() => this.setState({forceHide: false}), CLOSE_DELAY);
});After:
this.setState({forceHide: true, isOpen: false});
// Don't reset forceHide automatically - wait for user to move mouse away and backforceHide now stays true until user explicitly hovers again.
6. Reset Flags on Re-Hover (line 82)
handleToggleOpenState () {
if (!this.state.isOpen) {
this.ignoreMouseLeave = false;
this.setState({
isOpen: true,
forceHide: false
});
}
}When user moves mouse away and hovers again, flags are reset and tooltip can show normally.
Implementation Details
Modified Files
packages/scratch-gui/src/components/action-menu/action-menu.jsx
Key Changes
- Added
ignoreMouseLeaveflag to prevent phantom mouse leave events - All buttons use
clickDelayer(removed file input bypass) - Removed automatic
forceHidereset (manual reset on re-hover only) - Added flag reset in
handleToggleOpenStateandcomponentWillUnmount - Added debug logging (to be removed before final commit)
Behavior After Fix
Expected sequence:
1. User hovers → tooltip appears
2. User clicks sub-menu item
3. clickDelayer sets: ignoreMouseLeave=true, forceHide=true
4. ReactTooltip.hide() called
5. Modal opens (DOM changes trigger phantom onMouseLeave)
6. handleClosePopover called but IGNORES (ignoreMouseLeave=true)
7. Modal closes
8. Tooltip stays hidden ✓
9. User moves mouse away and hovers again
10. handleToggleOpenState resets flags
11. Tooltip works normally again ✓
Verification Steps
After fix:
- Start dev server:
docker compose up app - Hover ActionMenu → tooltip appears ✓
- Expand menu → menu shows ✓
- Click each sub-menu item:
- Upload Sprite → modal opens → close → tooltip stays hidden ✓
- Surprise → modal opens → close → tooltip stays hidden ✓
- Paint → modal opens → close → tooltip stays hidden ✓
- Choose a Sprite → modal opens → close → tooltip stays hidden ✓
- Move mouse away and hover again → tooltip works normally ✓
Related Fix
Commit 7d2a8c7 fixed similar MenuBar issue using setTimeout(..., 0) pattern. This ActionMenu fix follows similar React 18 timing principles but with different implementation due to tooltip-specific requirements.
Status
✅ FIXED - All sub-menu buttons now correctly hide tooltip after modal closes. Tooltip only reappears when user explicitly re-hovers.