Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 5 additions & 7 deletions js/src/collapse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_COLLAPSE = 'collapse'
const CLASS_NAME_COLLAPSING = 'collapsing'
const CLASS_NAME_COLLAPSED = 'collapsed'
const CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`
const CLASS_NAME_HORIZONTAL = 'collapse-horizontal'

Expand Down Expand Up @@ -88,7 +87,7 @@ class Collapse extends BaseComponent {
this._initializeChildren()

if (!this._config.parent) {
this._addAriaAndCollapsedClass(this._triggerArray, this._isShown())
this._setAriaExpanded(this._triggerArray, this._isShown())
}

if (this._config.toggle) {
Expand Down Expand Up @@ -152,7 +151,7 @@ class Collapse extends BaseComponent {

this._element.style[dimension] = 0 as unknown as string

this._addAriaAndCollapsedClass(this._triggerArray, true)
this._setAriaExpanded(this._triggerArray, true)
this._isTransitioning = true

const complete = () => {
Expand Down Expand Up @@ -196,7 +195,7 @@ class Collapse extends BaseComponent {
const element = SelectorEngine.getElementFromSelector(trigger)

if (element && !this._isShown(element)) {
this._addAriaAndCollapsedClass([trigger], false)
this._setAriaExpanded([trigger], false)
}
}

Expand Down Expand Up @@ -240,7 +239,7 @@ class Collapse extends BaseComponent {
const selected = SelectorEngine.getElementFromSelector(element)

if (selected) {
this._addAriaAndCollapsedClass([element], this._isShown(selected))
this._setAriaExpanded([element], this._isShown(selected))
}
}
}
Expand All @@ -251,13 +250,12 @@ class Collapse extends BaseComponent {
return SelectorEngine.find(selector, this._config.parent as HTMLElement).filter(element => !children.includes(element))
}

protected _addAriaAndCollapsedClass(triggerArray: HTMLElement[], isOpen: boolean): void {
protected _setAriaExpanded(triggerArray: HTMLElement[], isOpen: boolean): void {
if (!triggerArray.length) {
return
}

for (const element of triggerArray) {
element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen)
setAriaAttribute(element, 'aria-expanded', isOpen)
}
}
Expand Down
33 changes: 7 additions & 26 deletions js/tests/unit/collapse.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ describe('Collapse', () => {
it('should prevent url change if click on nested elements', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<a role="button" data-bs-toggle="collapse" class="collapsed" href="#collapse">',
'<a role="button" data-bs-toggle="collapse" href="#collapse">',
' <span id="nested"></span>',
'</a>',
'<div id="collapse" class="collapse"></div>'
Expand All @@ -518,7 +518,7 @@ describe('Collapse', () => {
it('should show multiple collapsed elements', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<a role="button" data-bs-toggle="collapse" class="collapsed" href=".multi"></a>',
'<a role="button" data-bs-toggle="collapse" href=".multi"></a>',
'<div id="collapse1" class="collapse multi"></div>',
'<div id="collapse2" class="collapse multi"></div>'
].join('')
Expand All @@ -529,7 +529,6 @@ describe('Collapse', () => {

collapse2.addEventListener('shown.bs.collapse', () => {
expect(trigger.getAttribute('aria-expanded')).toEqual('true')
expect(trigger).not.toHaveClass('collapsed')
expect(collapse1).toHaveClass('show')
expect(collapse1).toHaveClass('show')
resolve()
Expand All @@ -553,7 +552,6 @@ describe('Collapse', () => {

collapse2.addEventListener('hidden.bs.collapse', () => {
expect(trigger.getAttribute('aria-expanded')).toEqual('false')
expect(trigger).toHaveClass('collapsed')
expect(collapse1).not.toHaveClass('show')
expect(collapse1).not.toHaveClass('show')
resolve()
Expand All @@ -563,11 +561,11 @@ describe('Collapse', () => {
})
})

it('should remove "collapsed" class from target when collapse is shown', () => {
it('should set aria-expanded to true on triggers when collapse is shown', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<a id="link1" role="button" data-bs-toggle="collapse" class="collapsed" href="#" data-bs-target="#test1"></a>',
'<a id="link2" role="button" data-bs-toggle="collapse" class="collapsed" href="#" data-bs-target="#test1"></a>',
'<a id="link1" role="button" data-bs-toggle="collapse" href="#" data-bs-target="#test1"></a>',
'<a id="link2" role="button" data-bs-toggle="collapse" href="#" data-bs-target="#test1"></a>',
'<div id="test1"></div>'
].join('')

Expand All @@ -578,16 +576,14 @@ describe('Collapse', () => {
collapseTest1.addEventListener('shown.bs.collapse', () => {
expect(link1.getAttribute('aria-expanded')).toEqual('true')
expect(link2.getAttribute('aria-expanded')).toEqual('true')
expect(link1).not.toHaveClass('collapsed')
expect(link2).not.toHaveClass('collapsed')
resolve()
})

link1.click()
})
})

it('should add "collapsed" class to target when collapse is hidden', () => {
it('should set aria-expanded to false on triggers when collapse is hidden', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<a id="link1" role="button" data-bs-toggle="collapse" href="#" data-bs-target="#test1"></a>',
Expand All @@ -602,8 +598,6 @@ describe('Collapse', () => {
collapseTest1.addEventListener('hidden.bs.collapse', () => {
expect(link1.getAttribute('aria-expanded')).toEqual('false')
expect(link2.getAttribute('aria-expanded')).toEqual('false')
expect(link1).toHaveClass('collapsed')
expect(link2).toHaveClass('collapsed')
resolve()
})

Expand Down Expand Up @@ -696,20 +690,16 @@ describe('Collapse', () => {

collapseOneEl.addEventListener('shown.bs.collapse', () => {
expect(collapseOneEl).toHaveClass('show')
expect(triggerEl).not.toHaveClass('collapsed')
expect(triggerEl.getAttribute('aria-expanded')).toEqual('true')

expect(collapseTwoEl).not.toHaveClass('show')
expect(triggerTwoEl).toHaveClass('collapsed')
expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('false')

collapseTwoEl.addEventListener('shown.bs.collapse', () => {
expect(collapseOneEl).not.toHaveClass('show')
expect(triggerEl).toHaveClass('collapsed')
expect(triggerEl.getAttribute('aria-expanded')).toEqual('false')

expect(collapseTwoEl).toHaveClass('show')
expect(triggerTwoEl).not.toHaveClass('collapsed')
expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('true')
resolve()
})
Expand Down Expand Up @@ -860,7 +850,7 @@ describe('Collapse', () => {
})
})

it('should add "collapsed" class and set aria-expanded to triggers only when all the targeted collapse are hidden', () => {
it('should set aria-expanded on triggers only when all the targeted collapse are hidden', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<a id="trigger1" role="button" data-bs-toggle="collapse" href="#test1"></a>',
Expand All @@ -877,33 +867,24 @@ describe('Collapse', () => {
const target2 = fixtureEl.querySelector(`#${CSS.escape('0/my/id')}`)

const target2Shown = () => {
expect(trigger1).not.toHaveClass('collapsed')
expect(trigger1.getAttribute('aria-expanded')).toEqual('true')

expect(trigger2).not.toHaveClass('collapsed')
expect(trigger2.getAttribute('aria-expanded')).toEqual('true')

expect(trigger3).not.toHaveClass('collapsed')
expect(trigger3.getAttribute('aria-expanded')).toEqual('true')

target2.addEventListener('hidden.bs.collapse', () => {
expect(trigger1).not.toHaveClass('collapsed')
expect(trigger1.getAttribute('aria-expanded')).toEqual('true')

expect(trigger2).toHaveClass('collapsed')
expect(trigger2.getAttribute('aria-expanded')).toEqual('false')

expect(trigger3).not.toHaveClass('collapsed')
expect(trigger3.getAttribute('aria-expanded')).toEqual('true')

target1.addEventListener('hidden.bs.collapse', () => {
expect(trigger1).toHaveClass('collapsed')
expect(trigger1.getAttribute('aria-expanded')).toEqual('false')

expect(trigger2).toHaveClass('collapsed')
expect(trigger2.getAttribute('aria-expanded')).toEqual('false')

expect(trigger3).toHaveClass('collapsed')
expect(trigger3.getAttribute('aria-expanded')).toEqual('false')
resolve()
})
Expand Down
6 changes: 3 additions & 3 deletions js/tests/visual/collapse.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ <h5 class="mb-0">
<div class="card" role="presentation">
<div class="card-header" role="tab" id="headingTwo">
<h5 class="mb-0">
<a class="collapsed" data-bs-toggle="collapse" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<a data-bs-toggle="collapse" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
Collapsible Group Item #2
</a>
</h5>
Expand All @@ -43,7 +43,7 @@ <h5 class="mb-0">
<div class="card" role="presentation">
<div class="card-header" role="tab" id="headingThree">
<h5 class="mb-0">
<a class="collapsed" data-bs-toggle="collapse" href="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
<a data-bs-toggle="collapse" href="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
Collapsible Group Item #3
</a>
</h5>
Expand All @@ -57,7 +57,7 @@ <h5 class="mb-0">
<div class="card" role="presentation">
<div class="card-header" role="tab" id="headingFour">
<h5 class="mb-0">
<a class="collapsed" data-bs-toggle="collapse" href="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
<a data-bs-toggle="collapse" href="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
Collapsible Group Item with XSS in data-bs-parent
</a>
</h5>
Expand Down
2 changes: 1 addition & 1 deletion site/src/assets/examples/cheatsheet/cheatsheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function setActiveItem() {

link.classList.add('active')

if (parent.classList.contains('collapsed')) {
if (parent.getAttribute('aria-expanded') === 'false') {
parent.click()
}

Expand Down
6 changes: 3 additions & 3 deletions site/src/assets/examples/cheatsheet/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const body_class = 'bg-1'
<nav class="small" id="toc">
<ul class="list-unstyled">
<li class="my-2">
<button type="button" class="btn d-inline-flex align-items-center collapsed border-0" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#contents-collapse" aria-controls="contents-collapse">Contents</button>
<button type="button" class="btn d-inline-flex align-items-center border-0" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#contents-collapse" aria-controls="contents-collapse">Contents</button>
<ul class="list-unstyled ps-3 collapse" id="contents-collapse">
<li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#typography">Typography</a></li>
<li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#images">Images</a></li>
Expand All @@ -32,7 +32,7 @@ export const body_class = 'bg-1'
</ul>
</li>
<li class="my-2">
<button type="button" class="btn d-inline-flex align-items-center collapsed border-0" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#forms-collapse" aria-controls="forms-collapse">Forms</button>
<button type="button" class="btn d-inline-flex align-items-center border-0" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#forms-collapse" aria-controls="forms-collapse">Forms</button>
<ul class="list-unstyled ps-3 collapse" id="forms-collapse">
<li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#overview">Overview</a></li>
<li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#disabled-forms">Disabled forms</a></li>
Expand All @@ -43,7 +43,7 @@ export const body_class = 'bg-1'
</ul>
</li>
<li class="my-2">
<button type="button" class="btn d-inline-flex align-items-center collapsed border-0" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#components-collapse" aria-controls="components-collapse">Components</button>
<button type="button" class="btn d-inline-flex align-items-center border-0" data-bs-toggle="collapse" aria-expanded="false" data-bs-target="#components-collapse" aria-controls="components-collapse">Components</button>
<ul class="list-unstyled ps-3 collapse" id="components-collapse">
<li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#accordion">Accordion</a></li>
<li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#alerts">Alerts</a></li>
Expand Down
8 changes: 4 additions & 4 deletions site/src/assets/examples/sidebars/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export const extra_js = [{src: 'sidebars.js'}]
</a>
<ul class="list-unstyled ps-0">
<li class="mb-1">
<button class="btn btn-toggle d-inline-flex align-items-center rounded border-0 collapsed" data-bs-toggle="collapse" data-bs-target="#home-collapse" aria-expanded="true">
<button class="btn btn-toggle d-inline-flex align-items-center rounded border-0" data-bs-toggle="collapse" data-bs-target="#home-collapse" aria-expanded="true">
Home
</button>
<div class="collapse show" id="home-collapse">
Expand All @@ -210,7 +210,7 @@ export const extra_js = [{src: 'sidebars.js'}]
</div>
</li>
<li class="mb-1">
<button class="btn btn-toggle d-inline-flex align-items-center rounded border-0 collapsed" data-bs-toggle="collapse" data-bs-target="#dashboard-collapse" aria-expanded="false">
<button class="btn btn-toggle d-inline-flex align-items-center rounded border-0" data-bs-toggle="collapse" data-bs-target="#dashboard-collapse" aria-expanded="false">
Dashboard
</button>
<div class="collapse" id="dashboard-collapse">
Expand All @@ -223,7 +223,7 @@ export const extra_js = [{src: 'sidebars.js'}]
</div>
</li>
<li class="mb-1">
<button class="btn btn-toggle d-inline-flex align-items-center rounded border-0 collapsed" data-bs-toggle="collapse" data-bs-target="#orders-collapse" aria-expanded="false">
<button class="btn btn-toggle d-inline-flex align-items-center rounded border-0" data-bs-toggle="collapse" data-bs-target="#orders-collapse" aria-expanded="false">
Orders
</button>
<div class="collapse" id="orders-collapse">
Expand All @@ -237,7 +237,7 @@ export const extra_js = [{src: 'sidebars.js'}]
</li>
<li class="border-top my-3"></li>
<li class="mb-1">
<button class="btn btn-toggle d-inline-flex align-items-center rounded border-0 collapsed" data-bs-toggle="collapse" data-bs-target="#account-collapse" aria-expanded="false">
<button class="btn btn-toggle d-inline-flex align-items-center rounded border-0" data-bs-toggle="collapse" data-bs-target="#account-collapse" aria-expanded="false">
Account
</button>
<div class="collapse" id="account-collapse">
Expand Down
2 changes: 2 additions & 0 deletions site/src/content/docs/components/collapse.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ Conversely, multiple `<button>` or `<a>` elements can show and hide the same ele

Be sure to add `aria-expanded` to the control element. This attribute explicitly conveys the current state of the collapsible element tied to the control to screen readers and similar assistive technologies. If the collapsible element is closed by default, the attribute on the control element should have a value of `aria-expanded="false"`. If you’ve set the collapsible element to be open by default using the `show` class, set `aria-expanded="true"` on the control instead. The plugin will automatically toggle this attribute on the control based on whether or not the collapsible element has been opened or closed (via JavaScript, or because the user triggered another control element also tied to the same collapsible element). If the control element’s HTML element is not a button (e.g., an `<a>` or `<div>`), the attribute `role="button"` should be added to the element.

Collapse trigger state is represented by `aria-expanded`.

If your control element is targeting a single collapsible element – i.e. the `data-bs-target` attribute is pointing to an `id` selector – you should add the `aria-controls` attribute to the control element, containing the `id` of the collapsible element. Modern screen readers and similar assistive technologies make use of this attribute to provide users with additional shortcuts to navigate directly to the collapsible element itself.

Note that Bootstrap’s current implementation does not cover the various *optional* keyboard interactions described in the [ARIA Authoring Practices Guide accordion pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/) - you will need to include these yourself with custom JavaScript.
Expand Down
1 change: 1 addition & 0 deletions site/src/content/docs/guides/migration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ Bootstrap 6 is a major release with many breaking changes to modernize our codeb
</nav>
```

- **Collapse triggers now use `aria-expanded` as the state signal.** Bootstrap v6 no longer adds or removes a `.collapsed` class on collapse trigger elements. If you styled or queried trigger state with `.collapsed`, migrate those selectors to `[aria-expanded="false"]` and open-state selectors to `[aria-expanded="true"]`.
- **Navbar toggler icon now uses a CSS mask.** `.navbar-toggler-icon` renders via `mask-image` (`--navbar-toggler-icon`) tinted with `background-color: currentcolor` instead of an embedded `background-image` SVG. The markup stays an empty `<span class="navbar-toggler-icon">`, but the icon now inherits the current text color (including dark mode), so the separate light/dark toggler SVGs are no longer needed.
- **Rebuilt the carousel on CSS scroll snap.** The slide engine no longer uses `float` + `translateX` class juggling or a custom swipe handler—`.carousel-inner` is now a native horizontal scroll-snap container, so sliding, touch dragging, momentum, and keyboard scrolling come from the browser. The markup is unchanged (`.carousel` &rarr; `.carousel-inner` &rarr; `.carousel-item`), and the public JavaScript API (`next`, `prev`, `to`, `cycle`, `pause`) plus the `slide.bs.carousel` / `slid.bs.carousel` events are preserved.
- **New capabilities:** show multiple slides at once, reveal a “peek” of adjacent slides, gaps, center mode, and variable-width slides—all via CSS custom properties (`--carousel-items`, `--carousel-items-gap`, `--carousel-items-peek`) and the `.carousel-center` / `.carousel-auto` variants.
Expand Down