diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index 05491b212dd2..675e194e3ec6 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -5,6 +5,7 @@ _Released 2/27/2024 (PENDING)_
**Bugfixes:**
+- Fixed an issue where `.click()` commands on children of disabled elements would still produce "click" events -- even without `{ force: true }`. Fixes [#28788](https://github.com/cypress-io/cypress/issues/28788).
- Changed RequestBody type to allow for boolean and null literals to be passed as body values. [#28789](https://github.com/cypress-io/cypress/issues/28789)
## 13.6.6
diff --git a/packages/driver/cypress/e2e/commands/actions/click.cy.js b/packages/driver/cypress/e2e/commands/actions/click.cy.js
index 98650cb31341..bc679e8c6b9f 100644
--- a/packages/driver/cypress/e2e/commands/actions/click.cy.js
+++ b/packages/driver/cypress/e2e/commands/actions/click.cy.js
@@ -546,6 +546,30 @@ describe('src/cy/commands/actions/click', () => {
cy.getAll('span2', 'focus click mousedown').each(shouldNotBeCalled)
})
+ // https://github.com/cypress-io/cypress/issues/28788
+ it('no click when element is disabled', () => {
+ const btn = cy.$$('button:first')
+ const span = $('foooo')
+
+ attachFocusListeners({ btn, span })
+ attachMouseClickListeners({ btn, span })
+ attachMouseHoverListeners({ btn, span })
+
+ btn.html('')
+ btn.attr('disabled', true)
+ btn.append(span)
+
+ cy.get('button:first span').click()
+
+ if (Cypress.browser.name === 'chrome') {
+ cy.getAll('btn', 'mouseenter mousedown mouseup').each(shouldBeCalled)
+ }
+
+ cy.getAll('btn', 'focus click').each(shouldNotBeCalled)
+ cy.getAll('span', 'mouseenter mousedown mouseup').each(shouldBeCalled)
+ cy.getAll('span', 'focus click').each(shouldNotBeCalled)
+ })
+
it('no click when new element at coords is not ancestor', () => {
const btn = cy.$$('button:first')
const span1 = $('foooo')
diff --git a/packages/driver/src/cy/mouse.ts b/packages/driver/src/cy/mouse.ts
index 2a34656ac7ff..c87e4b110a4b 100644
--- a/packages/driver/src/cy/mouse.ts
+++ b/packages/driver/src/cy/mouse.ts
@@ -567,6 +567,21 @@ export const create = (state: StateFunc, keyboard: Keyboard, focused: IFocused,
return { skipClickEventReason: 'element was detached' }
}
+ // Only send click event if element is not disabled.
+ // First find an parent element that can actually be disabled
+ const findParentThatCanBeDisabled = (el: HTMLElement): HTMLElement | null => {
+ const elementsThatCanBeDisabled = ['button', 'input', 'select', 'textarea', 'optgroup', 'option', 'fieldset']
+
+ return elementsThatCanBeDisabled.includes($elements.getTagName(el)) ? el : null
+ }
+
+ const parentThatCanBeDisabled = $elements.findParent(mouseUpPhase.targetEl, findParentThatCanBeDisabled) || $elements.findParent(mouseDownPhase.targetEl, findParentThatCanBeDisabled)
+
+ // Then check if parent is indeed disabled
+ if (parentThatCanBeDisabled !== null && $elements.isDisabled($(parentThatCanBeDisabled))) {
+ return { skipClickEventReason: 'element was disabled' }
+ }
+
const commonAncestor = mouseUpPhase.targetEl &&
mouseDownPhase.targetEl &&
$elements.getFirstCommonAncestor(mouseUpPhase.targetEl, mouseDownPhase.targetEl)