1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Prevent event propogation if element is disabled when event chain begins.

The existing UJS event behavior relies on browsers not sending events for
various events when an element is disabled. For example, imagine the following:

    <button type="submit" disabled="disabled">Click me</button>

The above button is disabled, so browsers will not trigger a click event and
all UJS behavior is prevented. However, imagine a button like this:

    <button type="submit" disabled="disabled"><strong>Click me</strong></button>

The above is treated differently by browsers such as Chrome/Safari. These
browsers do not consider the strong tag to be disabled, and will trigger click
events. UJS has logic to walk up the DOM to find an associated element subject
to UJS behavior. But, this logic does not take into account the disabled
status of the element.

I originally thought we could simply change the selectors used to match
elements to ignore disabled elements. However, UJS disables some elements as
part of the event chain. So, an element might match early in the chain and
then fail to match later. Instead of changing the selectors I added a callback
to the chain that calls `stopEverything` if an element is disabled when the
event chain begins.
This commit is contained in:
Patrick Toomey 2017-03-09 23:06:18 -07:00
parent 9f9e57339b
commit 9ca712348e
No known key found for this signature in database
GPG key ID: 0096BD01AA78248E
3 changed files with 38 additions and 1 deletions

View file

@ -2,6 +2,10 @@
{ matches, getData, setData, stopEverything, formElements } = Rails { matches, getData, setData, stopEverything, formElements } = Rails
Rails.handleDisabledElement = (e) ->
element = this
stopEverything(e) if element.disabled
# Unified function to enable an element (link, button and form) # Unified function to enable an element (link, button and form)
Rails.enableElement = (e) -> Rails.enableElement = (e) ->
element = if e instanceof Event then e.target else e element = if e instanceof Event then e.target else e

View file

@ -12,7 +12,7 @@
fire, delegate fire, delegate
getData, $ getData, $
refreshCSRFTokens, CSRFProtection refreshCSRFTokens, CSRFProtection
enableElement, disableElement enableElement, disableElement, handleDisabledElement
handleConfirm handleConfirm
handleRemote, formSubmitButtonClick, handleMetaClick handleRemote, formSubmitButtonClick, handleMetaClick
handleMethod handleMethod
@ -44,19 +44,23 @@ Rails.start = ->
delegate document, Rails.buttonDisableSelector, 'ajax:complete', enableElement delegate document, Rails.buttonDisableSelector, 'ajax:complete', enableElement
delegate document, Rails.buttonDisableSelector, 'ajax:stopped', enableElement delegate document, Rails.buttonDisableSelector, 'ajax:stopped', enableElement
delegate document, Rails.linkClickSelector, 'click', handleDisabledElement
delegate document, Rails.linkClickSelector, 'click', handleConfirm delegate document, Rails.linkClickSelector, 'click', handleConfirm
delegate document, Rails.linkClickSelector, 'click', handleMetaClick delegate document, Rails.linkClickSelector, 'click', handleMetaClick
delegate document, Rails.linkClickSelector, 'click', disableElement delegate document, Rails.linkClickSelector, 'click', disableElement
delegate document, Rails.linkClickSelector, 'click', handleRemote delegate document, Rails.linkClickSelector, 'click', handleRemote
delegate document, Rails.linkClickSelector, 'click', handleMethod delegate document, Rails.linkClickSelector, 'click', handleMethod
delegate document, Rails.buttonClickSelector, 'click', handleDisabledElement
delegate document, Rails.buttonClickSelector, 'click', handleConfirm delegate document, Rails.buttonClickSelector, 'click', handleConfirm
delegate document, Rails.buttonClickSelector, 'click', disableElement delegate document, Rails.buttonClickSelector, 'click', disableElement
delegate document, Rails.buttonClickSelector, 'click', handleRemote delegate document, Rails.buttonClickSelector, 'click', handleRemote
delegate document, Rails.inputChangeSelector, 'change', handleDisabledElement
delegate document, Rails.inputChangeSelector, 'change', handleConfirm delegate document, Rails.inputChangeSelector, 'change', handleConfirm
delegate document, Rails.inputChangeSelector, 'change', handleRemote delegate document, Rails.inputChangeSelector, 'change', handleRemote
delegate document, Rails.formSubmitSelector, 'submit', handleDisabledElement
delegate document, Rails.formSubmitSelector, 'submit', handleConfirm delegate document, Rails.formSubmitSelector, 'submit', handleConfirm
delegate document, Rails.formSubmitSelector, 'submit', handleRemote delegate document, Rails.formSubmitSelector, 'submit', handleRemote
# Normal mode submit # Normal mode submit
@ -65,6 +69,7 @@ Rails.start = ->
delegate document, Rails.formSubmitSelector, 'ajax:send', disableElement delegate document, Rails.formSubmitSelector, 'ajax:send', disableElement
delegate document, Rails.formSubmitSelector, 'ajax:complete', enableElement delegate document, Rails.formSubmitSelector, 'ajax:complete', enableElement
delegate document, Rails.formInputClickSelector, 'click', handleDisabledElement
delegate document, Rails.formInputClickSelector, 'click', handleConfirm delegate document, Rails.formInputClickSelector, 'click', handleConfirm
delegate document, Rails.formInputClickSelector, 'click', formSubmitButtonClick delegate document, Rails.formInputClickSelector, 'click', formSubmitButtonClick

View file

@ -26,6 +26,13 @@ module('data-confirm', {
'data-confirm': 'Are you absolutely sure?' 'data-confirm': 'Are you absolutely sure?'
})) }))
$('#qunit-fixture').append($('<button />', {
type: 'submit',
form: 'confirm',
disabled: 'disabled',
'data-confirm': 'Are you absolutely sure?'
}))
this.windowConfirm = window.confirm this.windowConfirm = window.confirm
}, },
teardown: function() { teardown: function() {
@ -286,3 +293,24 @@ asyncTest('clicking on the children of a link should also trigger a confirm', 6,
.find('strong') .find('strong')
.triggerNative('click') .triggerNative('click')
}) })
asyncTest('clicking on the children of a disabled button should not trigger a confirm.', 1, function() {
var message
// auto-decline:
window.confirm = function(msg) { message = msg; return false }
$('button[data-confirm][disabled]')
.html("<strong>Click me</strong>")
.bindNative('confirm', function() {
App.assertCallbackNotInvoked('confirm')
})
.find('strong')
.bindNative('click', function() {
App.assertCallbackInvoked('click')
})
.triggerNative('click')
setTimeout(function() {
start()
}, 50)
})