Skip to content

Commit 6b4c356

Browse files
authored
[ButtonBase] Fix native button detection (#47985)
1 parent 4c4b054 commit 6b4c356

2 files changed

Lines changed: 69 additions & 2 deletions

File tree

packages/mui-material/src/ButtonBase/ButtonBase.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,16 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) {
192192

193193
const isNonNativeButton = () => {
194194
const button = buttonRef.current;
195-
return component && component !== 'button' && !(button.tagName === 'A' && button.href);
195+
196+
if (!button) {
197+
return component && component !== 'button';
198+
}
199+
200+
if (button.tagName === 'BUTTON') {
201+
return false;
202+
}
203+
204+
return !(button.tagName === 'A' && button.href);
196205
};
197206

198207
const handleKeyDown = useEventCallback((event) => {
@@ -240,7 +249,8 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) {
240249
event.target === event.currentTarget &&
241250
isNonNativeButton() &&
242251
event.key === ' ' &&
243-
!event.defaultPrevented
252+
!event.defaultPrevented &&
253+
!disabled
244254
) {
245255
event.currentTarget.click();
246256
}

packages/mui-material/src/ButtonBase/ButtonBase.test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,32 @@ describe('<ButtonBase />', () => {
824824
setProps({ disabled: false });
825825
expect(button).not.to.have.attribute('aria-disabled');
826826
});
827+
828+
it('should not propagate click events when Space is released on a disabled non-native button', async () => {
829+
const parentClickSpy = spy();
830+
const buttonClickSpy = spy();
831+
832+
const { user } = render(
833+
<div onClick={parentClickSpy}>
834+
<ButtonBase component="span" disabled onClick={buttonClickSpy}>
835+
Hello
836+
</ButtonBase>
837+
</div>,
838+
);
839+
840+
const button = screen.getByRole('button');
841+
842+
// We don't use `user.tab()` because normal tab focus won't land on a disabled
843+
// ButtonBase, only programmatic focus can happen
844+
await act(async () => {
845+
button.focus();
846+
});
847+
848+
await user.keyboard(' ');
849+
850+
expect(buttonClickSpy.callCount).to.equal(0);
851+
expect(parentClickSpy.callCount).to.equal(0);
852+
});
827853
});
828854

829855
describe('prop: component', () => {
@@ -1182,6 +1208,37 @@ describe('<ButtonBase />', () => {
11821208
expect(onClickSpy.callCount).to.equal(0);
11831209
});
11841210

1211+
it('should preserve native button keyboard behavior when a custom component renders a native button', async () => {
1212+
const onClickSpy = spy();
1213+
const onKeyDownSpy = spy();
1214+
1215+
/** @type {React.ForwardRefExoticComponent<React.ButtonHTMLAttributes<HTMLButtonElement>>} */
1216+
const MyButton = React.forwardRef((props, ref) => <button ref={ref} {...props} />);
1217+
1218+
const { user } = render(
1219+
<ButtonBase component={MyButton} onClick={onClickSpy} onKeyDown={onKeyDownSpy}>
1220+
Hello
1221+
</ButtonBase>,
1222+
);
1223+
1224+
await user.tab();
1225+
1226+
await user.keyboard('{Enter}');
1227+
1228+
expect(onKeyDownSpy.callCount).to.equal(1);
1229+
expect(onClickSpy.callCount).to.equal(1);
1230+
expect(onKeyDownSpy.firstCall.args[0]).to.have.property('defaultPrevented', false);
1231+
1232+
onClickSpy.resetHistory();
1233+
onKeyDownSpy.resetHistory();
1234+
1235+
await user.keyboard(' ');
1236+
1237+
expect(onKeyDownSpy.callCount).to.equal(1);
1238+
expect(onClickSpy.callCount).to.equal(1);
1239+
expect(onKeyDownSpy.firstCall.args[0]).to.have.property('defaultPrevented', false);
1240+
});
1241+
11851242
it('prevents default on Enter with an anchor and empty href', async () => {
11861243
const onClickSpy = spy();
11871244
const onKeyDownSpy = spy();

0 commit comments

Comments
 (0)