Skip to content
GitLab
    • Explore Projects Groups Snippets
Projects Groups Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Sign in / Register
  • B bootstrap
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
  • Issues 263
    • Issues 263
    • List
    • Boards
    • Service Desk
    • Milestones
  • Merge requests 114
    • Merge requests 114
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Infrastructure Registry
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • CI/CD
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • Bootstrap
  • bootstrap
  • Merge requests
  • !30455

v4: Button checked state for back/refresh

  • Review changes

  • Download
  • Email patches
  • Plain diff
Closed Administrator requested to merge github/fork/icio/v4-button-input-change into v4-dev 5 years ago
  • Overview 1
  • Commits 4
  • Pipelines 0
  • Changes 2

Created by: icio

Unlike any of the other browsers, Firefox will restore <input>s' checked status after a page refresh. (This behaviour can be suppressed with autocomplete=off on the form or input.) You can see this in action on 4.4.1 by selecting the last example radio then refreshing.

Other browsers and Firefox will restore the <inputs>s' checked status after navigating away from a page then navigating back. There's #25122 (closed) documenting this, which we can resolve by updating the initial button state on window pageshow as-well as load.

Trying to fix all of those things lead me to this PR, where button toggles with label/input rely on change events to update state, and .btn[data-toggle=button] remains handled by click events.

First PR here so feedback on testing/style/anything gratefully received.


I made the mistake of branching off v4-dev. Apologies :relaxed: Hopefully #28463 lands and v5 drops the requirement for JavaScript for this control, otherwise I can rebase onto master.

If a CSS-only alternative proves difficult, the toggle control could be far simpler if only label>input based markup is supported because we can then toggle the active class through the change event alone.

Compare
  • v4-dev (base)

and
  • latest version
    222a41b9
    4 commits, 2 years ago

2 files
+ 105
- 97

    Preferences

    File browser
    Compare changes
j‎s‎
s‎rc‎
butt‎on.js‎ +82 -92
tests/‎visual‎
butto‎n.html‎ +23 -5
js/src/button.js
+ 82
- 92
  • View file @ 222a41b9


@@ -31,16 +31,20 @@ const Selector = {
DATA_TOGGLES : '[data-toggle="buttons"]',
DATA_TOGGLE : '[data-toggle="button"]',
DATA_TOGGLES_BUTTONS : '[data-toggle="buttons"] .btn',
DATA_TOGGLES_INPUTS : '[data-toggle="buttons"] input:not([type="hidden"])',
DATA_TOGGLE_BUTTONS : '[data-toggle="button"], [data-toggle="buttons"] .btn',
INPUT : 'input:not([type="hidden"])',
ACTIVE : '.active',
BUTTON : '.btn'
}
const Event = {
CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,
FOCUS_BLUR_DATA_API : `focus${EVENT_KEY}${DATA_API_KEY} ` +
`blur${EVENT_KEY}${DATA_API_KEY}`,
LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`
CHANGE_DATA_API : `change${EVENT_KEY}${DATA_API_KEY}`,
CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,
FOCUS_BLUR_DATA_API : `focus${EVENT_KEY}${DATA_API_KEY} ` +
`blur${EVENT_KEY}${DATA_API_KEY}`,
LOAD_PAGESHOW_DATA_API : `load${EVENT_KEY}${DATA_API_KEY} ` +
`pageshow${EVENT_KEY}${DATA_API_KEY}`
}
/**
@@ -63,51 +67,18 @@ class Button {
// Public
toggle() {
let triggerChangeEvent = true
let addAriaPressed = true
const rootElement = $(this._element).closest(
Selector.DATA_TOGGLES
)[0]
if (rootElement) {
const input = this._element.querySelector(Selector.INPUT)
if (input) {
if (input.type === 'radio') {
if (input.checked &&
this._element.classList.contains(ClassName.ACTIVE)) {
triggerChangeEvent = false
} else {
const activeElement = rootElement.querySelector(Selector.ACTIVE)
if (activeElement) {
$(activeElement).removeClass(ClassName.ACTIVE)
}
}
}
if (triggerChangeEvent) {
// if it's not a radio button or checkbox don't add a pointless/invalid checked property to the input
if (input.type === 'checkbox' || input.type === 'radio') {
input.checked = !this._element.classList.contains(ClassName.ACTIVE)
}
$(input).trigger('change')
}
input.focus()
addAriaPressed = false
}
if (Button._disabled(this._element)) {
return
}
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)
}
const c = !Button._checked(this._element)
const input = this._element.querySelector(Selector.INPUT)
if (input) {
input.checked = c
$(input).trigger('change')
} else {
this._element.setAttribute('aria-pressed', c)
Button._update(this._element)
}
}
@@ -118,10 +89,33 @@ class Button {
// Static
static _update(e) {
e.classList.toggle(ClassName.ACTIVE, Button._checked(e))
}
static _checked(e) {
const input = e.querySelector(Selector.INPUT)
if (input) {
return input.checked
}
return e.getAttribute('aria-pressed') === 'true'
}
static _disabled(e) {
if (e.hasAttribute('disabled') || e.classList.contains('disabled')) {
return true
}
const input = e.querySelector(Selector.INPUT)
return input && (
input.hasAttribute('disabled') ||
input.classList.contains('disabled')
)
}
static _jQueryInterface(config) {
return this.each(function () {
let data = $(this).data(DATA_KEY)
if (!data) {
data = new Button(this)
$(this).data(DATA_KEY, data)
@@ -141,60 +135,56 @@ class Button {
*/
$(document)
.on(Event.CHANGE_DATA_API, Selector.DATA_TOGGLES_INPUTS, function (event) {
// Update all related inputs on change.
let t = $(this)
if (event.target.type === 'radio') {
t = t.add(document.getElementsByName(event.target.name))
}
t.closest(Selector.BUTTON).each(function () {
Button._update(this)
})
})
.on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {
let button = event.target
const initialButton = button
if (!$(button).hasClass(ClassName.BUTTON)) {
button = $(button).closest(Selector.BUTTON)[0]
// Find the containing .btn.
let btn = $(event.target)
if (!btn.hasClass(ClassName.BUTTON)) {
btn = btn.closest(Selector.BUTTON)
}
if (!btn.length) {
return
}
if (!button || button.hasAttribute('disabled') || button.classList.contains('disabled')) {
// Don't allow disabled buttons to be toggled.
if (Button._disabled(btn[0])) {
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
}
return
}
if (initialButton.tagName === 'LABEL' && inputBtn && inputBtn.type === 'checkbox') {
event.preventDefault() // work around event sent to label and input
}
Button._jQueryInterface.call($(button), 'toggle')
// label.btn > input will trigger a change event for the same click so we
// don't repeat the toggle here.
if (event.target.tagName === 'INPUT' || btn.length && btn[0].tagName === 'LABEL') {
return
}
// Toggle the btn.
Button._jQueryInterface.call(btn, 'toggle')
// div.btn may not accept focus, so we give it to its input.
btn.find(Selector.INPUT).trigger('focus')
})
.on(Event.FOCUS_BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {
const button = $(event.target).closest(Selector.BUTTON)[0]
$(button).toggleClass(ClassName.FOCUS, /^focus(in)?$/.test(event.type))
// Add/remove class "focus" on focus[in]/focusout events.
$(event.target)
.closest(Selector.BUTTON)
.toggleClass(ClassName.FOCUS, /^focus(in)?$/.test(event.type))
})
$(window).on(Event.LOAD_DATA_API, () => {
// ensure correct active class is set to match the controls' actual values/states
// find all checkboxes/readio buttons inside data-toggle groups
let buttons = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLES_BUTTONS))
for (let i = 0, len = buttons.length; i < len; i++) {
const button = buttons[i]
const input = button.querySelector(Selector.INPUT)
if (input.checked || input.hasAttribute('checked')) {
button.classList.add(ClassName.ACTIVE)
} else {
button.classList.remove(ClassName.ACTIVE)
}
}
// find all button toggles
buttons = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))
for (let i = 0, len = buttons.length; i < len; i++) {
const button = buttons[i]
if (button.getAttribute('aria-pressed') === 'true') {
button.classList.add(ClassName.ACTIVE)
} else {
button.classList.remove(ClassName.ACTIVE)
}
}
$(window).on(Event.LOAD_PAGESHOW_DATA_API, () => {
// Ensure correct active class is set to match the controls' actual values/states.
$(Selector.DATA_TOGGLE_BUTTONS).each(function () {
Button._update(this)
})
})
/**
js/tests/visual/button.html
+ 23
- 5
  • View file @ 222a41b9


@@ -10,15 +10,20 @@
<div class="container">
<h1>Button <small>Bootstrap Visual Test</small></h1>
<button type="button" class="btn btn-primary" data-toggle="button" aria-pressed="false">
<button type="button" class="btn btn-outline-primary" data-toggle="button" aria-pressed="false">
Single toggle
</button>
<button type="button" class="btn btn-outline-primary disabled" data-toggle="button" aria-pressed="false">
Disabled toggle
</button>
<p>For checkboxes and radio buttons, ensure that keyboard behavior is functioning correctly.</p>
<p>Navigate to the checkboxes with the keyboard (generally, using <kbd>TAB</kbd> / <kbd>SHIFT + TAB</kbd>), and ensure that <kbd>SPACE</kbd> toggles the currently focused checkbox. Click on one of the checkboxes using the mouse, ensure that focus was correctly set on the actual checkbox, and that <kbd>SPACE</kbd> toggles the checkbox again.</p>
<p>Browers will check or uncheck inputs to match the user's last selection when the page is navigated back to or refreshed (in Firefox's case). Try <a href="https://getbootstrap.com/">navigating away</a> and pressing back.</p>
<p>Navigate to the checkboxes with the keyboard (generally, using <kbd>TAB</kbd> / <kbd>SHIFT + TAB</kbd>), and ensure that <kbd>SPACE</kbd> toggles the currently focused checkbox. Click on one of the checkboxes using the mouse, ensure that focus was correctly set on the actual checkbox, and that <kbd>SPACE</kbd> toggles the checkbox again. After refreshing the page, the checkbox state should be persisted by the browser.</p>
<div class="btn-group" data-toggle="buttons">
<label class="btn btn-primary active">
<label class="btn btn-primary">
<input type="checkbox" checked> Checkbox 1 (pre-checked)
</label>
<label class="btn btn-primary">
@@ -29,10 +34,10 @@
</label>
</div>
<p>Navigate to the radio button group with the keyboard (generally, using <kbd>TAB</kbd> / <kbd>SHIFT + TAB</kbd>). If no radio button was initially set to be selected, the first/last radio button should receive focus (depending on whether you navigated "forward" to the group with <kbd>TAB</kbd> or "backwards" using <kbd>SHIFT + TAB</kbd>). If a radio button was already selected, navigating with the keyboard should set focus to that particular radio button. Only one radio button in a group should receive focus at any given time. Ensure that the selected radio button can be changed by using the <kbd>←</kbd> and <kbd>→</kbd> arrow keys. Click on one of the radio buttons with the mouse, ensure that focus was correctly set on the actual radio button, and that <kbd>←</kbd> and <kbd>→</kbd> change the selected radio button again.</p>
<p>Navigate to the radio button group with the keyboard (generally, using <kbd>TAB</kbd> / <kbd>SHIFT + TAB</kbd>). If no radio button was initially set to be selected, the first/last radio button should receive focus (depending on whether you navigated "forward" to the group with <kbd>TAB</kbd> or "backwards" using <kbd>SHIFT + TAB</kbd>). If a radio button was already selected, navigating with the keyboard should set focus to that particular radio button. Only one radio button in a group should receive focus at any given time. Ensure that the selected radio button can be changed by using the <kbd>←</kbd> and <kbd>→</kbd> arrow keys. Click on one of the radio buttons with the mouse, ensure that focus was correctly set on the actual radio button, and that <kbd>←</kbd> and <kbd>→</kbd> change the selected radio button again. After changing the selected radio button, refreshing the page should result in the last-selected toggle button shown as active.</p>
<div class="btn-group" data-toggle="buttons">
<label class="btn btn-primary active">
<label class="btn btn-primary">
<input type="radio" name="options" id="option1" checked> Radio 1 (preselected)
</label>
<label class="btn btn-primary">
@@ -41,7 +46,20 @@
<label class="btn btn-primary">
<input type="radio" name="options" id="option3"> Radio 3
</label>
<label class="btn btn-primary disabled">
<input type="radio" name="options" id="option4" disabled> Radio 4
</label>
</div>
<p>We also support <code>div.btn &gt; input</code> structures:</p>
<div class="btn-group" data-toggle="buttons">
<div class="btn btn-primary">
<input type="checkbox" aria-label="Hello">
<span>Check</span>
</div>
</div>
</div>
<script src="../../../node_modules/jquery/dist/jquery.slim.min.js"></script>
0 Assignees
None
Assign to
0 Reviewers
None
Request review from
Labels
0
None
0
None
    Assign labels
  • Manage project labels

Milestone
No milestone
None
None
Time tracking
No estimate or time spent
Lock merge request
Unlocked
0
0 participants
Reference: firstcontributions/first-contributions!57199
Source branch: github/fork/icio/v4-button-input-change

Menu

Explore Projects Groups Snippets