-
-
Notifications
You must be signed in to change notification settings - Fork 95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Menu] Fix prop merging issues #1445
[Menu] Fix prop merging issues #1445
Conversation
✅ Deploy Preview for base-ui ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
This comment was marked as resolved.
This comment was marked as resolved.
If I understand correctly, this: |
This comment was marked as resolved.
This comment was marked as resolved.
Yup, that's right. |
} | ||
}, | ||
onKeyDown(event: React.KeyboardEvent) { | ||
onMouseDown(event: React.MouseEvent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed the behavior of disabled non-interactive triggers
I also encountered this when these are still <button>
s but "disabled" using aria-disabled+data-disabled and not the disabled attr to keep them focusable-when-disabled:
base-ui/packages/react/src/use-button/useButton.ts
Lines 92 to 129 in 6a00daf
onKeyDown(event: React.KeyboardEvent) { | |
if ( | |
disabled || | |
(event.target === event.currentTarget && !isNativeButton() && event.key === ' ') | |
) { | |
event.preventDefault(); | |
} | |
// Keyboard accessibility for non interactive elements | |
if ( | |
event.target === event.currentTarget && | |
!isNativeButton() && | |
!isValidLink() && | |
event.key === 'Enter' && | |
!disabled | |
) { | |
onClickProp(event); | |
event.preventDefault(); | |
} | |
}, | |
onKeyUp(event: React.KeyboardEvent) { | |
// calling preventDefault in keyUp on a <button> will not dispatch a click event if Space is pressed | |
// https://codesandbox.io/p/sandbox/button-keyup-preventdefault-dn7f0 | |
// Keyboard accessibility for non interactive elements | |
if ( | |
event.target === event.currentTarget && | |
!isNativeButton() && | |
!disabled && | |
event.key === ' ' | |
) { | |
onClickProp(event); | |
} | |
}, | |
onPointerDown(event: React.PointerEvent) { | |
if (disabled) { | |
event.preventDefault(); | |
} | |
}, |
This fix looks more robust 😄
In mine, when the disabled menu trigger is focused, Up/Down arrows are still opening the menu (Enter/Space doesn't)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I figured that the disabled button shouldn't respond to key presses the same way it doesn't respond to clicks.
* Event handlers returned by the functions are not automatically prevented when `preventBaseUIHandler` is called. | ||
* They must check `event.baseUIHandlerPrevented` themselves and bail out if it's true. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate more on this? Will all event handlers specified in our internal prop getters for components work by default without needing a check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whenever a function is passed to mergeReactProps
, it will be called with the merged props and the preventBaseUIHandler
is not taken into consideration. Most of our prop getter functions actually call mergeReactProps
internally, with one notable exception: useButton
. In its case, the logic for prop merging is more complex as it depends on the button's disabled state.
In the case of functions returned from Floating UI's useInteractions
, they don't know anything about preventBaseUIHandler
, so I'm calling them without parameters and provide the result to mergeReactProps
so our merging logic is run on them. This alone should be sufficient to fix 1., but I refactored mergeReactProps
to avoid having to nest prop getters and help prevent bugs like 2.
Co-authored-by: atomiks <[email protected]> Signed-off-by: Michał Dudak <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ordering logic makes sense to me
Was just testing this and found that dialog also has this issue: I think triggers that currently don't use useButton (Dialog, AlertDialog, Popover) need to start using it with the fixes in this PR |
Yes, there's possibly more components with these problems. I started with the Menu to keep the PR size managable. |
Co-authored-by: atomiks <[email protected]>
preventBaseUIHandler
in event handlers (fixes [Menu] Trigger's default behavior cannot be prevented #1441) (before / after).<Menu.Trigger disabled render={<span />} />
). Clicking on such a trigger used to open the menu anyway. (before / after)mergeReactProps
calls where they were not necessary.disabled
attribute fromSubmenuTrigger
. The submenus should be disabled by settingdisabled
on the Root part.The main problem in 1. was that event handlers returned from Floating UI's
useInteractions
didn't respect preventBaseUIHandler as they have their own prop merging logic.Instead of using these prop getters directly, I now call them without arguments and pass the result to our
mergeReactProps
function.Another issue I found was that we had to nest
mergeReactProps
calls and props getters, such asgetButtonProps(mergeReactProps(external, internal))
. In some cases this was confusing and error-prone (point 2.), so I updatedmergeReactProps
to accept either props directly or a props getter function.When a function is provided to
mergeReactProps
, it will be called with all the props defined to the right of it, somergeReactProps(obj1, obj2, fn, obj3, obj4)
, will be equivalent tomergeReactProps(obj, obj2, fn(mergeReactProps(obj3, obj4)))
.I assume that in such cases it is the props getter functionality to merge incoming and internal props and prevent default logic if needed.