adobe / adobe/spectrum-web-components
[Bug]: Action Menu (mobile Tray) doesn't reopen after being dismissed by an outside tap
- Dominant language
- TypeScript
- Stars
- 1.5k
- Forks
- 262
- Avg merge
- 3d 10h
- Merged PRs (30d)
- 68
Description
### Code of conduct
- [x] I agree to follow this project's code of conduct.
### Impacted component(s)
Action Menu (`sp-action-menu`)
### Library version
1.12.2
### Expected behavior
On a mobile/touch device, tapping the action button should reopen the menu, even if the previous time it was open, it was dismissed by tapping outside of it rather than by selecting a menu item or moving focus elsewhere.
### Actual behavior
On a mobile device (or Chrome DevTools device emulation), opening the Action Menu renders it in a tray. If the tray is dismissed by tapping outside of it (instead of selecting a menu item), tapping the action button again does **not** reopen the tray. The button becomes unresponsive to taps until focus is moved to some other element and back.
### Screenshots
See linked Slack thread / screen recording (internal): reproduces directly on the SWC docs site action-menu example.
### What browsers are you seeing the problem in?
Chrome
### How can we reproduce this issue?
1. Go to https://opensource.adobe.com/spectrum-web-components/components/action-menu/
2. Open the page with Chrome DevTools device emulation using an iPhone 16 Pro Max (or any touch/mobile emulation).
3. Tap the action button. The menu opens in a tray.
4. Tap outside the tray to dismiss it (without selecting a menu item).
5. Tap the action button again.
6. **Expected:** the tray reopens. **Actual:** nothing happens; the tray does not reopen until focus is moved elsewhere first.
### Sample code or abstract reproduction which illustrates the problem
```html
Deselect
Select Inverse
```
No special markup is required; this reproduces with the default Action Menu example on the docs site, as long as the mobile/touch `MobileController` code path is active (viewport matches the mobile media query).
### Severity
SEV 4
### Root cause (found during investigation)
In `packages/picker/src/Picker.ts`, `ExpandableElement.handleBeforetoggle` handles the overlay's native `beforetoggle` event when it closes. When the close is triggered externally (e.g. tapping outside the mobile Tray, which closes via the Tray's underlay `close` event rather than a click on the trigger button) while `strategy.preventNextToggle === 'no'`, the handler does:
```ts
} else if (this.strategy?.preventNextToggle === 'no') {
this.open = false;
}
```
This sets the host's own `open` property (so the Tray visually closes) but never syncs `this.strategy.open`, unlike the sibling branch just above it (`if (!this.open) { ... this.strategy.open = false; }`), which does sync both.
Because of that, `MobileController`'s internally tracked `open` state (`InteractionController.open`, which is separate from `host.open`) stays stale at `true`. On the very next tap, `MobileController.handlePointerdown()` reads that stale value and sets `preventNextToggle = 'yes'` (believing the menu is still open), which causes `MobileController.handleClick()` to skip calling `toggle()` entirely - so the tap does nothing.
This doesn't affect desktop because `DesktopController.handleActivate()` unconditionally calls `host.toggle()`, which recomputes fresh from the host's own `open` property and resyncs `strategy.open` as a side effect (self-healing). `MobileController.handleClick()` instead gates the `toggle()` call itself on the stale controller state, so it never reaches that self-healing path.
**Suggested fix:** also sync `this.strategy.open = false;` in that `else if` branch, mirroring the branch above it.
### Would you like to track this issue in Jira?
- [x] Yes, please tell me the ticket number!
Contributor guide
Research direction
Start in packages/picker/src/Picker.ts, reading ExpandableElement.handleBeforetoggle and the MobileController open-state flow. Reproduce with the Action Menu docs example in Chrome DevTools iPhone emulation, then verify that dismissing the tray by tapping outside allows the action button to reopen it immediately.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100