primitives icon indicating copy to clipboard operation
primitives copied to clipboard

[Dropdown Menu] WCAG2.2 SC 2.5.2 violation - Dropdown Menu opens on `onPointerDown`

Open bolonio opened this issue 1 year ago • 5 comments

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

  1. A mouse navigation user clicks (and holds) the triggering element of the dropdown menu.
  2. Without releasing, moves the mouse outside of both the triggering element or the dropdown menu area.
  3. The dropdown menu is not closed (cancelling the function).

CleanShot 2024-09-18 at 15 40 57

Expected behavior

  1. A mouse navigation user clicks (and holds) the triggering element of the dropdown menu.
  2. The dropdown menu is not yet open.
  3. The user releases the mouse click while still hovering the triggering element of the dropdown menu.
  4. The dropdown menu opens.

CleanShot 2024-09-18 at 15 45 29

You can see the behaviour in the W3C Menu Button Pattern or in the W3C Menu Bar Pattern

CleanShot 2024-09-18 at 15 42 00

or moving the mouse outside of the dropdown menu triggering and content should cancel the onPointerDown function.

  1. A mouse navigation user clicks (and holds) the triggering element of the dropdown menu.
  2. The dropdown menu opens.
  3. Without releasing, moves the mouse outside of both the triggering element or the dropdown menu area.
  4. The dropdown menu closes.
  5. The user releases the mouse click while hovering outside of both the triggering element or the dropdown menu area.
  6. The dropdown menu is still closed.

Reproducible example

Radix Dropdown Menu component

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:

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

bolonio avatar Sep 18 '24 13:09 bolonio

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.

bolonio avatar Oct 03 '24 08:10 bolonio

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.

wintercounter avatar Nov 16 '24 18:11 wintercounter

+1

programming-with-ia avatar Dec 31 '24 16:12 programming-with-ia

+1

NoraHNIKT avatar May 20 '25 09:05 NoraHNIKT

+1

flex-mookeun avatar May 26 '25 09:05 flex-mookeun

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.

asumaran avatar Jun 26 '25 02:06 asumaran