Skip to content
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

v5: Move from JS input button group toggling to all CSS #28463

Closed
wants to merge 46 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
496b0ed
Cherry-pick from ysds
mdo Mar 12, 2019
63473a0
Update docs to match new markup, drop JS
mdo Mar 12, 2019
047b231
Update button.js
XhmikosR Mar 12, 2019
d6f7341
Update scss/_button-group.scss
ysds Mar 12, 2019
0acbe5a
Fix input types
mdo Mar 12, 2019
c8036f0
Update scss/_button-group.scss
ysds Mar 13, 2019
cbd79ea
Remove unit tests for faked radio button/checkbox buttons
patrickhlauke Jun 6, 2019
7c862c7
Add unit test for focus class handling
patrickhlauke Jun 6, 2019
fc84638
Initial update of button documentation to match new reality
patrickhlauke Jun 6, 2019
2f2dcbd
Add handling of disabled buttons
patrickhlauke Jun 6, 2019
f17b00a
Unit test for disabled buttons and for btn lacking initial aria-pressed
patrickhlauke Jun 6, 2019
6863eda
Remove obsolete mention of super-powered/JS functionality for button …
patrickhlauke Jun 6, 2019
c55ddbb
Reorganise/reword button documentation
patrickhlauke Jun 6, 2019
5097359
Remove obsolete unit test
patrickhlauke Jun 6, 2019
fb146c7
Simplify button.js selector and add comment for click behavior
patrickhlauke Jun 6, 2019
4d2d96a
Use proper simulated click in one test, move related unit tests close…
patrickhlauke Jun 6, 2019
787a748
Redo button visual test
patrickhlauke Jun 6, 2019
420257e
Change unit tests to test actual simulated click
patrickhlauke Jun 6, 2019
09b1fc9
Reintroduce one unit test for bootstrapButton('toggle')
patrickhlauke Jun 6, 2019
1f83fe8
Merge branch 'master' into v5-input-btn-group
patrickhlauke Jun 6, 2019
7590df0
Remove unnecessary autocomplete="off"
patrickhlauke Jun 6, 2019
6129413
Tweak focus/blur test
patrickhlauke Jun 6, 2019
6d530ad
Expand button group description
patrickhlauke Jun 6, 2019
a8f4aa4
Further tweak to unit test
patrickhlauke Jun 6, 2019
57cdbb6
Reintroduce element checks for focus/blur, tweak unit test again
patrickhlauke Jun 6, 2019
5c004ac
Remove bespoke focus styling
patrickhlauke Jun 6, 2019
038c174
Expand automated and manual tests
patrickhlauke Jun 6, 2019
03939db
Expand checkbox/radio button explanation
patrickhlauke Jun 6, 2019
687f579
Add automated and visual test for disabled <a href=#"> faked button
patrickhlauke Jun 6, 2019
e834129
Tighten and combine related unit tests
patrickhlauke Jun 6, 2019
846c007
Stupid typos/unnecessary ids in visual test
patrickhlauke Jun 6, 2019
d63e00f
Rename btn-group-input to just btn-input
patrickhlauke Jun 7, 2019
11ecbe5
Use + next-sibling combinator not ~ subsequent-sibling combinator
patrickhlauke Jun 7, 2019
e1f9e3f
Update/extend buttons and button-group docs
patrickhlauke Jun 7, 2019
8c83b1d
Move btn-input to more logical place, fix button group styles to allo…
patrickhlauke Jun 7, 2019
698f6f6
Merge branch 'master' into v5-input-btn-group
patrickhlauke Jun 7, 2019
bfe562f
Update button.html
XhmikosR Jun 7, 2019
c4bedfd
Add new `button-input-wrapper` wrapper, expand CSS, expand docs
patrickhlauke Jun 10, 2019
8bc5e96
Add disabled variants to the checkbox, radio button and toggle button…
patrickhlauke Jun 11, 2019
c90e312
Add missing :disabled styles for checkbox/radio button buttons
patrickhlauke Jun 11, 2019
34580c4
Expand examples of buttons further
patrickhlauke Jun 11, 2019
9ed0803
Remove generic focus for checkbox/radio .btn
patrickhlauke Jun 11, 2019
620992e
Move callout to more logical place in button-group doc
patrickhlauke Jun 12, 2019
7a1e7cf
Merge branch 'master' into v5-input-btn-group
patrickhlauke Jun 13, 2019
49a9c44
Merge branch 'master' into v5-input-btn-group
patrickhlauke Jun 19, 2019
47a4abc
Merge branch 'master' into v5-input-btn-group
patrickhlauke Jun 24, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 6 additions & 74 deletions js/src/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,15 @@ const DATA_API_KEY = '.data-api'

const ClassName = {
ACTIVE: 'active',
BUTTON: 'btn',
FOCUS: 'focus'
BUTTON: 'btn'
}

const Selector = {
DATA_TOGGLE_CARROT: '[data-toggle^="button"]',
DATA_TOGGLE: '[data-toggle="buttons"]',
INPUT: 'input:not([type="hidden"])',
ACTIVE: '.active',
BUTTON: '.btn'
BUTTON: '.btn[data-toggle="button"]'
}

const Event = {
CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`,
FOCUS_DATA_API: `focus${EVENT_KEY}${DATA_API_KEY}`,
BLUR_DATA_API: `blur${EVENT_KEY}${DATA_API_KEY}`
CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`
}

/**
Expand All @@ -63,54 +56,10 @@ class Button {
// Public

toggle() {
let triggerChangeEvent = true
let addAriaPressed = true

const rootElement = SelectorEngine.closest(
this._element,
Selector.DATA_TOGGLE
)

if (rootElement) {
const input = SelectorEngine.findOne(Selector.INPUT, this._element)

if (input) {
if (input.type === 'radio') {
if (input.checked &&
this._element.classList.contains(ClassName.ACTIVE)) {
triggerChangeEvent = false
} else {
const activeElement = SelectorEngine.findOne(Selector.ACTIVE, rootElement)

if (activeElement) {
activeElement.classList.remove(ClassName.ACTIVE)
}
}
}

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)
EventHandler.trigger(input, 'change')
}

input.focus()
addAriaPressed = false
}
}

if (addAriaPressed) {
if (!(this._element.hasAttribute('disabled') || this._element.classList.contains('disabled'))) {
this._element.setAttribute('aria-pressed',
!this._element.classList.contains(ClassName.ACTIVE))
}

if (triggerChangeEvent) {
this._element.classList.toggle(ClassName.ACTIVE)
}
}
Expand Down Expand Up @@ -147,11 +96,10 @@ class Button {
* ------------------------------------------------------------------------
*/

EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, event => {
event.preventDefault()

EventHandler.on(document, Event.CLICK_DATA_API, Selector.BUTTON, event => {
let button = event.target
if (!button.classList.contains(ClassName.BUTTON)) {
// the event.target is a child element of the actual toggle button
button = SelectorEngine.closest(button, Selector.BUTTON)
}

Expand All @@ -164,22 +112,6 @@ EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, eve
data.toggle()
})

EventHandler.on(document, Event.FOCUS_DATA_API, Selector.DATA_TOGGLE_CARROT, event => {
const button = SelectorEngine.closest(event.target, Selector.BUTTON)

if (button) {
button.classList.add(ClassName.FOCUS)
}
})

EventHandler.on(document, Event.BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, event => {
const button = SelectorEngine.closest(event.target, Selector.BUTTON)

if (button) {
button.classList.remove(ClassName.FOCUS)
}
})

/**
* ------------------------------------------------------------------------
* jQuery
Expand Down
248 changes: 88 additions & 160 deletions js/tests/unit/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,174 +35,102 @@ $(function () {
assert.strictEqual($button[0], $el[0], 'collection contains element')
})

QUnit.test('should toggle active', function (assert) {
assert.expect(2)
var $btn = $('<button class="btn" data-toggle="button">mdo</button>')
assert.ok(!$btn.hasClass('active'), 'btn does not have active class')
QUnit.test('should toggle active and aria-pressed (testing using bootStrapButton(\'toggle\') rather than click())', function (assert) {
assert.expect(4)
var $btn = $('<button class="btn" data-toggle="button" aria-pressed="false">mdo</button>')
assert.ok(!$btn.hasClass('active'), 'initial btn does not have active class')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'initial btn aria-pressed state is false')
$btn.bootstrapButton('toggle')
assert.ok($btn.hasClass('active'), 'btn has class active')
})

QUnit.test('should toggle active when btn children are clicked', function (assert) {
assert.expect(2)
var $btn = $('<button class="btn" data-toggle="button">mdo</button>')
assert.ok($btn.hasClass('active'), 'after toggle btn has class active')
assert.strictEqual($btn.attr('aria-pressed'), 'true', 'after toggle btn aria-pressed state is true')
})

QUnit.test('should toggle active and aria-pressed for <button>', function (assert) {
assert.expect(4)
var $btn = $('<button class="btn" data-toggle="button" aria-pressed="false">mdo</button>')
$btn.appendTo('#qunit-fixture')
assert.ok(!$btn.hasClass('active'), 'initial btn does not have active class')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'initial btn aria-pressed state is false')
$btn[0].click()
assert.ok($btn.hasClass('active'), 'after click btn has class active')
assert.strictEqual($btn.attr('aria-pressed'), 'true', 'after click btn aria-pressed state is true')
})

QUnit.test('should toggle active and aria-pressed for <div> faked button', function (assert) {
assert.expect(4)
var $btn = $('<div tabindex="0" role="button" class="btn" data-toggle="button" aria-pressed="false">faker</div>')
$btn.appendTo('#qunit-fixture')
assert.ok(!$btn.hasClass('active'), 'initial btn does not have active class')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'initial btn aria-pressed state is false')
$btn[0].click() // in real-world use, authors will need to add custom keyboard handling to actually fire the click() when pressing ENTER/SPACE
assert.ok($btn.hasClass('active'), 'after click btn has class active')
assert.strictEqual($btn.attr('aria-pressed'), 'true', 'after click btn aria-pressed state is true')
})

QUnit.test('should toggle active and aria-pressed for <a href="#"> faked button', function (assert) {
assert.expect(4)
var $btn = $('<a href="#" tabindex="0" role="button" class="btn" data-toggle="button" aria-pressed="false">faker</a>')
$btn.appendTo('#qunit-fixture')
assert.ok(!$btn.hasClass('active'), 'initial btn does not have active class')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'initial btn aria-pressed state is false')
$btn[0].click()
assert.ok($btn.hasClass('active'), 'after click btn has class active')
assert.strictEqual($btn.attr('aria-pressed'), 'true', 'after click btn aria-pressed state is true')
})

QUnit.test('should toggle active and aria-pressed when btn children are clicked', function (assert) {
assert.expect(4)
var $btn = $('<button class="btn" data-toggle="button" aria-pressed="false">mdo</button>')
var $inner = $('<i/>')
$btn
.append($inner)
.appendTo('#qunit-fixture')
assert.ok(!$btn.hasClass('active'), 'btn does not have active class')
$inner.trigger('click')
assert.ok($btn.hasClass('active'), 'btn has class active')
})

QUnit.test('should toggle aria-pressed', function (assert) {
assert.expect(2)
var $btn = $('<button class="btn" data-toggle="button" aria-pressed="false">redux</button>')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'btn aria-pressed state is false')
$btn.bootstrapButton('toggle')
assert.strictEqual($btn.attr('aria-pressed'), 'true', 'btn aria-pressed state is true')
assert.ok(!$btn.hasClass('active'), 'initial btn does not have active class')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'initial btn aria-pressed state is false')
$inner[0].click()
assert.ok($btn.hasClass('active'), 'after click btn has class active')
assert.strictEqual($btn.attr('aria-pressed'), 'true', 'after click btn aria-pressed state is true')
})

QUnit.test('should toggle aria-pressed on buttons with container', function (assert) {
QUnit.test('should add aria-pressed and set to true if original button didn\'t have it', function (assert) {
assert.expect(1)
var groupHTML = '<div class="btn-group" data-toggle="buttons">' +
'<button id="btn1" class="btn btn-secondary" type="button">One</button>' +
'<button class="btn btn-secondary" type="button">Two</button>' +
'</div>'
$('#qunit-fixture').append(groupHTML)
$('#btn1').bootstrapButton('toggle')
assert.strictEqual($('#btn1').attr('aria-pressed'), 'true')
})

QUnit.test('should toggle aria-pressed when btn children are clicked', function (assert) {
assert.expect(2)
var $btn = $('<button class="btn" data-toggle="button" aria-pressed="false">redux</button>')
var $inner = $('<i/>')
$btn
.append($inner)
.appendTo('#qunit-fixture')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'btn aria-pressed state is false')
$inner.trigger('click')
assert.strictEqual($btn.attr('aria-pressed'), 'true', 'btn aria-pressed state is true')
})

QUnit.test('should trigger input change event when toggled button has input field', function (assert) {
assert.expect(1)
var done = assert.async()

var groupHTML = '<div class="btn-group" data-toggle="buttons">' +
'<label class="btn btn-primary">' +
'<input type="radio" id="radio" autocomplete="off">Radio' +
'</label>' +
'</div>'
var $group = $(groupHTML).appendTo('#qunit-fixture')

$group.find('input').on('change', function (e) {
e.preventDefault()
assert.ok(true, 'change event fired')
done()
})

$group.find('label').trigger('click')
})

QUnit.test('should check for closest matching toggle', function (assert) {
assert.expect(12)
var groupHTML =
'<div class="btn-group" data-toggle="buttons">' +
' <label class="btn btn-primary active">' +
' <input type="radio" name="options" id="option1" checked="true"> Option 1' +
' </label>' +
' <label class="btn btn-primary">' +
' <input type="radio" name="options" id="option2"> Option 2' +
' </label>' +
' <label class="btn btn-primary">' +
' <input type="radio" name="options" id="option3"> Option 3' +
' </label>' +
'</div>'

var $group = $(groupHTML).appendTo('#qunit-fixture')

var $btn1 = $group.children().eq(0)
var $btn2 = $group.children().eq(1)
var inputBtn2 = $btn2.find('input')[0]

assert.ok($btn1.hasClass('active'), 'btn1 has active class')
assert.ok($btn1.find('input').prop('checked'), 'btn1 is checked')
assert.ok(!$btn2.hasClass('active'), 'btn2 does not have active class')
assert.ok(!inputBtn2.checked, 'btn2 is not checked')

inputBtn2.dispatchEvent(new Event('click'))

assert.ok(!$btn1.hasClass('active'), 'btn1 does not have active class')
assert.ok(!$btn1.find('input').prop('checked'), 'btn1 is not checked')
assert.ok($btn2.hasClass('active'), 'btn2 has active class')
assert.ok(inputBtn2.checked, 'btn2 is checked')

inputBtn2.dispatchEvent(new Event('click')) // clicking an already checked radio should not un-check it

assert.ok(!$btn1.hasClass('active'), 'btn1 does not have active class')
assert.ok(!$btn1.find('input').prop('checked'), 'btn1 is not checked')
assert.ok($btn2.hasClass('active'), 'btn2 has active class')
assert.ok(inputBtn2.checked, 'btn2 is checked')
})

QUnit.test('should only toggle selectable inputs', function (assert) {
assert.expect(6)
var groupHTML = '<div class="btn-group" data-toggle="buttons">' +
'<label class="btn btn-primary active">' +
'<input type="hidden" name="option1" id="option1-default" value="false">' +
'<input type="checkbox" name="option1" id="option1" checked="true"> Option 1' +
'</label>' +
'</div>'
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 = '<div class="btn-group" data-toggle="buttons">' +
'<label class="btn btn-primary"><input type="checkbox" autocomplete="off"> Checkbox</label>' +
'<label class="btn btn-primary"><input type="radio" name="options" autocomplete="off"> Radio</label>' +
'</div>'
var $group = $(groupHTML).appendTo('#qunit-fixture')

var $btn1 = $group.children().eq(0)
var $btn2 = $group.children().eq(1)

$btn1.find('input').trigger('click')
assert.ok($btn1.is(':not([aria-pressed])'), 'label for nested checkbox input has not been given an aria-pressed attribute')

$btn2.find('input').trigger('click')
assert.ok($btn2.is(':not([aria-pressed])'), 'label for nested radio input has not been given an aria-pressed attribute')
})

QUnit.test('should handle disabled attribute on non-button elements', function (assert) {
assert.expect(2)
var groupHTML = '<div class="btn-group disabled" data-toggle="buttons" aria-disabled="true" disabled>' +
'<label class="btn btn-danger disabled" aria-disabled="true" disabled>' +
'<input type="checkbox" aria-disabled="true" autocomplete="off" disabled class="disabled"/>' +
'</label>' +
'</div>'
var $group = $(groupHTML).appendTo('#qunit-fixture')

var $btn = $group.children().eq(0)
var $input = $btn.children().eq(0)

$btn.trigger('click')
assert.ok($btn.is(':not(.active)'), 'button did not become active')
assert.ok(!$input.is(':checked'), 'checkbox did not get checked')
var $btn = $('<button class="btn" data-toggle="button">forgetful</button>')
$btn.appendTo('#qunit-fixture')
$btn[0].click()
assert.strictEqual($btn.attr('aria-pressed'), 'true', 'btn has aria-pressed and it\'s set to true')
})

QUnit.test('should not toggle active nor aria-pressed on buttons with disabled class', function (assert) {
assert.expect(4)
var $btn = $('<button class="btn disabled" data-toggle="button" aria-pressed="false">redux</button>')
$btn.appendTo('#qunit-fixture')
assert.ok(!$btn.hasClass('active'), 'initial btn does not have active class')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'initial btn aria-pressed state is false')
$btn[0].click()
assert.ok(!$btn.hasClass('active'), 'after click btn still does not have active class')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'after click btn aria-pressed state is still false')
})

QUnit.test('should not toggle active nor aria-pressed on buttons that are disabled', function (assert) {
assert.expect(4)
var $btn = $('<button class="btn" data-toggle="button" aria-pressed="false" disabled>redux</button>')
$btn.appendTo('#qunit-fixture')
assert.ok(!$btn.hasClass('active'), 'initial btn does not have active class')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'initial btn aria-pressed state is false')
$btn[0].click()
assert.ok(!$btn.hasClass('active'), 'after click btn still does not have active class')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'after click btn aria-pressed state is still false')
})

QUnit.test('should not toggle active nor aria-pressed for <a href="#"> faked button with disabled class', function (assert) {
assert.expect(4)
var $btn = $('<a tabindex="0" role="button" class="btn disabled" data-toggle="button" aria-pressed="false">faker</a>')
$btn.appendTo('#qunit-fixture')
assert.ok(!$btn.hasClass('active'), 'initial btn does not have active class')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'initial btn aria-pressed state is false')
$btn[0].click()
assert.ok(!$btn.hasClass('active'), 'after click btn still does not have active class')
assert.strictEqual($btn.attr('aria-pressed'), 'false', 'after click btn aria-pressed state is still false')
})

QUnit.test('dispose should remove data and the element', function (assert) {
Expand Down
Loading