primitives
primitives copied to clipboard
[Dropdown Menu] WCAG2.2 SC 2.5.2 violation - Dropdown Menu opens on `onPointerDown`
Bug report
The Radix Dropdown Menu component opens on onPointerDown (which means as soon as a mouse navigation user presses the triggering element of the dropdown menu), but there is not available a function on the up-event, or a mechanism to abort the function before completion or to undo the function after completion. You can take a look at the Understanding SC 2.5.2: Pointer Cancellation (Level A) article for more information.
The intent of this success criterion is to make it easier for users to prevent accidental or erroneous pointer input. People with various disabilities can inadvertently initiate touch or mouse events with unwanted results. Each of the following subsections roughly aligns with the bullets of this Success Criterion, and outlines a means of allowing users to cancel pointer operations.
Source: Understanding SC 2.5.2: Pointer Cancellation (Level A)
Current behavior
- A mouse navigation user clicks (and holds) the triggering element of the dropdown menu.
- Without releasing, moves the mouse outside of both the triggering element or the dropdown menu area.
- The dropdown menu is not closed (cancelling the function).
Expected behavior
- A mouse navigation user clicks (and holds) the triggering element of the dropdown menu.
- The dropdown menu is not yet open.
- The user releases the mouse click while still hovering the triggering element of the dropdown menu.
- The dropdown menu opens.
You can see the behaviour in the W3C Menu Button Pattern or in the W3C Menu Bar Pattern
or moving the mouse outside of the dropdown menu triggering and content should cancel the onPointerDown function.
- A mouse navigation user clicks (and holds) the triggering element of the dropdown menu.
- The dropdown menu opens.
- Without releasing, moves the mouse outside of both the triggering element or the dropdown menu area.
- The dropdown menu closes.
- The user releases the mouse click while hovering outside of both the triggering element or the dropdown menu area.
- The dropdown menu is still closed.
Reproducible example
Suggested solution
Change the opening of the dropdown menu to onPointerUp or onClick instead of using onPointerDown.
Additional context
Here there are some examples of Radix components which meet the criteria, such as:
- Radix Accordion Component (only opens accordion item on
onPointerUp) - Radix Checkbox Component (only checks box on
onPointerUp) - Radix Switch Component (only operates switch on
onPointerUp)
Note: It might be related to https://github.com/radix-ui/primitives/issues/2700 (not 100% sure about it though)
Your environment
| Software | Name(s) | Version |
|---|---|---|
| Radix Package(s) | @radix-ui/react-dropdown-menu | ^2.0.5 |
| React | n/a | 18.2.0 |
| Browser | Google Chrome | 128.0.6613.138 |
| Assistive tech | ||
| Node | n/a | |
| npm/yarn | ||
| Operating System | MacOS | 14.6.1 |
There's a possible "workaround" that might work (from the side of the consumer), but unfortunately it will only work for mouse users, it breaks the keyboard navigation. I'm still investigating if there's a nicer workaround for both cases.
I have a solution that's going to stay backward compatible and won't break anything.
const handler = useRef("pointer")
onPointerDown={(ev) => {
if (ev.pointerType === "touch") {
handler.current = "click"
document.body.addEventListener("pointerup", () => {
queueMicrotask(() => {
handler.current = "pointer"
})
}, { once: true })
return
}
// ...old logic
}}
onClick={() => {
if (handler.current !== 'click') return
// ...old logic of onPointerDown
}}
So basically if pointerdown is initiated by touch, listen to the click handler instead.
This is the event order, which is the reason queueMicrotask is necessary:
pointerdown
pointerup
click
Happy to make a PR if this solution is acceptable.
Btw, I wish useDrodpwnMenuContext would be exported so we could create our own Trigger. This is true for basically any context, it'd would be handy in a lot of cases.
UPDATE
Now I realized that this is a solution for https://github.com/radix-ui/primitives/issues/1912 which is where I got here from in the first place. Nevertheless, this would solve our touch related issues, which is a pretty huge problem for us.
+1
+1
+1
I’d go even further and suggest exposing a way to choose which event should trigger the dropdown. In my case, I had a button that needed to be draggable with dnd-kit and also show the Radix dropdown on right-click. There was a conflict because both libraries use the onPointerDown event, so attempting to drag the button would open the dropdown, with no apparent way to change this behavior.
I propose there should be an option on the DropdownMenu.Trigger component to define which event should be used to trigger the dropdown.