diff --git a/js/src/button.js b/js/src/button.js index fcf805502a..6225137019 100644 --- a/js/src/button.js +++ b/js/src/button.js @@ -81,15 +81,16 @@ class Button { $(activeElement).removeClass(ClassName.ACTIVE) } } + } else if (input.type === 'checkbox') { + if (this._element.tagName === 'LABEL' && input.checked === this._element.classList.contains(ClassName.ACTIVE)) { + triggerChangeEvent = false + } + } else { + // if it's not a radio button or checkbox don't add a pointless/invalid checked property to the input + triggerChangeEvent = false } if (triggerChangeEvent) { - if (input.hasAttribute('disabled') || - rootElement.hasAttribute('disabled') || - input.classList.contains('disabled') || - rootElement.classList.contains('disabled')) { - return - } input.checked = !this._element.classList.contains(ClassName.ACTIVE) $(input).trigger('change') } @@ -99,13 +100,15 @@ class Button { } } - if (addAriaPressed) { - this._element.setAttribute('aria-pressed', - !this._element.classList.contains(ClassName.ACTIVE)) - } + if (!(this._element.hasAttribute('disabled') || this._element.classList.contains('disabled'))) { + if (addAriaPressed) { + this._element.setAttribute('aria-pressed', + !this._element.classList.contains(ClassName.ACTIVE)) + } - if (triggerChangeEvent) { - $(this._element).toggleClass(ClassName.ACTIVE) + if (triggerChangeEvent) { + $(this._element).toggleClass(ClassName.ACTIVE) + } } } @@ -140,15 +143,24 @@ class Button { $(document) .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => { - event.preventDefault() - let button = event.target if (!$(button).hasClass(ClassName.BUTTON)) { - button = $(button).closest(Selector.BUTTON) + button = $(button).closest(Selector.BUTTON)[0] } - Button._jQueryInterface.call($(button), 'toggle') + if (!button || button.hasAttribute('disabled') || button.classList.contains('disabled')) { + event.preventDefault() // work around Firefox bug #1540995 + } else { + const inputBtn = button.querySelector(Selector.INPUT) + + if (inputBtn && (inputBtn.hasAttribute('disabled') || inputBtn.classList.contains('disabled'))) { + event.preventDefault() // work around Firefox bug #1540995 + return + } + + Button._jQueryInterface.call($(button), 'toggle') + } }) .on(Event.FOCUS_BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => { const button = $(event.target).closest(Selector.BUTTON)[0] diff --git a/js/tests/unit/button.js b/js/tests/unit/button.js index 724545a532..324e940113 100644 --- a/js/tests/unit/button.js +++ b/js/tests/unit/button.js @@ -61,6 +61,22 @@ $(function () { assert.strictEqual($btn.attr('aria-pressed'), 'true', 'btn aria-pressed state is true') }) + QUnit.test('should not toggle aria-pressed on buttons with disabled class', function (assert) { + assert.expect(2) + var $btn = $('') + assert.strictEqual($btn.attr('aria-pressed'), 'false', 'btn aria-pressed state is false') + $btn.bootstrapButton('toggle') + assert.strictEqual($btn.attr('aria-pressed'), 'false', 'btn aria-pressed state is still false') + }) + + QUnit.test('should not toggle aria-pressed on buttons that are disabled', function (assert) { + assert.expect(2) + var $btn = $('') + assert.strictEqual($btn.attr('aria-pressed'), 'false', 'btn aria-pressed state is false') + $btn.bootstrapButton('toggle') + assert.strictEqual($btn.attr('aria-pressed'), 'false', 'btn aria-pressed state is still false') + }) + QUnit.test('should toggle aria-pressed on buttons with container', function (assert) { assert.expect(1) var groupHTML = '
' + @@ -139,29 +155,6 @@ $(function () { assert.ok($btn2.find('input').prop('checked'), 'btn2 is checked') }) - QUnit.test('should only toggle selectable inputs', function (assert) { - assert.expect(6) - var groupHTML = '
' + - '' + - '
' - var $group = $(groupHTML).appendTo('#qunit-fixture') - - var $btn = $group.children().eq(0) - var $hidden = $btn.find('input#option1-default') - var $cb = $btn.find('input#option1') - - assert.ok($btn.hasClass('active'), 'btn has active class') - assert.ok($cb.prop('checked'), 'btn is checked') - assert.ok(!$hidden.prop('checked'), 'hidden is not checked') - $btn.trigger('click') - assert.ok(!$btn.hasClass('active'), 'btn does not have active class') - assert.ok(!$cb.prop('checked'), 'btn is not checked') - assert.ok(!$hidden.prop('checked'), 'hidden is not checked') // should not be changed - }) - QUnit.test('should not add aria-pressed on labels for radio/checkbox inputs in a data-toggle="buttons" group', function (assert) { assert.expect(2) var groupHTML = '
' + @@ -181,10 +174,10 @@ $(function () { }) QUnit.test('should handle disabled attribute on non-button elements', function (assert) { - assert.expect(2) + assert.expect(4) var groupHTML = '
' + - '
' var $group = $(groupHTML).appendTo('#qunit-fixture') @@ -192,9 +185,121 @@ $(function () { var $btn = $group.children().eq(0) var $input = $btn.children().eq(0) - $btn.trigger('click') + assert.ok($btn.is(':not(.active)'), 'button is initially not active') + assert.ok(!$input.prop('checked'), 'checkbox is initially not checked') + $btn[0].click() // fire a real click on the DOM node itself, not a click() on the jQuery object that just aliases to trigger('click') assert.ok($btn.is(':not(.active)'), 'button did not become active') - assert.ok(!$input.is(':checked'), 'checkbox did not get checked') + assert.ok(!$input.prop('checked'), 'checkbox did not get checked') + }) + + QUnit.test('should not set active class if inner hidden checkbox is disabled but author forgot to set disabled class on outer button', function (assert) { + assert.expect(4) + var groupHTML = '
' + + '' + + '
' + var $group = $(groupHTML).appendTo('#qunit-fixture') + + var $btn = $group.children().eq(0) + var $input = $btn.children().eq(0) + + assert.ok($btn.is(':not(.active)'), 'button is initially not active') + assert.ok(!$input.prop('checked'), 'checkbox is initially not checked') + $btn[0].click() // fire a real click on the DOM node itself, not a click() on the jQuery object that just aliases to trigger('click') + assert.ok($btn.is(':not(.active)'), 'button did not become active') + assert.ok(!$input.prop('checked'), 'checkbox did not get checked') + }) + + QUnit.test('should correctly set checked state on input and active class on label when using structure', function (assert) { + assert.expect(4) + var groupHTML = '
' + + '' + + '
' + var $group = $(groupHTML).appendTo('#qunit-fixture') + + var $label = $group.children().eq(0) + var $input = $label.children().eq(0) + + assert.ok($label.is(':not(.active)'), 'label is initially not active') + assert.ok(!$input.prop('checked'), 'checkbox is initially not checked') + $label[0].click() // fire a real click on the DOM node itself, not a click() on the jQuery object that just aliases to trigger('click') + assert.ok($label.is('.active'), 'label is active after click') + assert.ok($input.prop('checked'), 'checkbox is checked after click') + }) + + QUnit.test('should correctly set checked state on input and active class on the faked button when using
structure', function (assert) { + assert.expect(4) + var groupHTML = '
' + + '
' + + '' + + '
' + + '
' + var $group = $(groupHTML).appendTo('#qunit-fixture') + + var $btn = $group.children().eq(0) + var $input = $btn.children().eq(0) + + assert.ok($btn.is(':not(.active)'), '
is initially not active') + assert.ok(!$input.prop('checked'), 'checkbox is initially not checked') + $btn[0].click() // fire a real click on the DOM node itself, not a click() on the jQuery object that just aliases to trigger('click') + assert.ok($btn.is('.active'), '
is active after click') + assert.ok($input.prop('checked'), 'checkbox is checked after click') + }) + + QUnit.test('should not do anything if the click was just sent to the outer container with data-toggle', function (assert) { + assert.expect(4) + var groupHTML = '
' + + '' + + '
' + var $group = $(groupHTML).appendTo('#qunit-fixture') + + var $label = $group.children().eq(0) + var $input = $label.children().eq(0) + + assert.ok($label.is(':not(.active)'), 'label is initially not active') + assert.ok(!$input.prop('checked'), 'checkbox is initially not checked') + $group[0].click() // fire a real click on the DOM node itself, not a click() on the jQuery object that just aliases to trigger('click') + assert.ok($label.is(':not(.active)'), 'label is not active after click') + assert.ok(!$input.prop('checked'), 'checkbox is not checked after click') + }) + + QUnit.test('should not try and set checked property on an input of type="hidden"', function (assert) { + assert.expect(2) + var groupHTML = '
' + + '' + + '
' + var $group = $(groupHTML).appendTo('#qunit-fixture') + + var $label = $group.children().eq(0) + var $input = $label.children().eq(0) + + assert.ok(!$input.prop('checked'), 'hidden input initially has no checked property') + $label[0].click() // fire a real click on the DOM node itself, not a click() on the jQuery object that just aliases to trigger('click') + assert.ok(!$input.prop('checked'), 'hidden input does not have a checked property') + }) + + QUnit.test('should not try and set checked property on an input that is not a radio button or checkbox', function (assert) { + assert.expect(2) + var groupHTML = '
' + + '' + + '
' + var $group = $(groupHTML).appendTo('#qunit-fixture') + + var $label = $group.children().eq(0) + var $input = $label.children().eq(0) + + assert.ok(!$input.prop('checked'), 'text input initially has no checked property') + $label[0].click() // fire a real click on the DOM node itself, not a click() on the jQuery object that just aliases to trigger('click') + assert.ok(!$input.prop('checked'), 'text input does not have a checked property') }) QUnit.test('dispose should remove data and the element', function (assert) {