Skip to content

Commit 6f899fc

Browse files
authored
fix(collapse/toggle): persist toggle state on element and prevent multiple state emits (closes #2923) (#2924)
1 parent f0b1710 commit 6f899fc

File tree

2 files changed

+35
-8
lines changed

2 files changed

+35
-8
lines changed

src/components/collapse/collapse.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,9 @@ export default {
9292
this.setWindowEvents(true)
9393
}
9494
},
95-
updated() {
96-
this.$root.$emit(EVENT_STATE, this.id, this.show)
97-
},
9895
beforeDestroy() /* istanbul ignore next */ {
96+
// Trigger state emit if needed
97+
this.show = false
9998
if (this.isNav && inBrowser) {
10099
this.setWindowEvents(false)
101100
}

src/directives/toggle/toggle.js

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,38 @@
11
import target from '../../utils/target'
2-
import { setAttr, addClass, removeClass } from '../../utils/dom'
3-
4-
// Are we client side?
5-
const inBrowser = typeof window !== 'undefined'
2+
import { setAttr, removeAttr, addClass, removeClass } from '../../utils/dom'
3+
import { inBrowser } from '../../utils/env'
64

75
// target listen types
86
const listenTypes = { click: true }
97

108
// Property key for handler storage
119
const BVT = '__BV_toggle__'
10+
const BVT_STATE = '__BV_toggle_STATE__'
11+
const BVT_CONTROLS = '__BV_toggle_CONTROLS__'
1212

1313
// Emitted Control Event for collapse (emitted to collapse)
1414
const EVENT_TOGGLE = 'bv::toggle::collapse'
1515

1616
// Listen to Event for toggle state update (Emited by collapse)
1717
const EVENT_STATE = 'bv::collapse::state'
1818

19+
/* istanbul ignore next */
20+
const handleUpdate = (el, binding, vnode) => {
21+
// Ensure the collapse class and aria-* attributes persist
22+
// after element is updated (eitehr by parent re-rendering
23+
// or changes to this element or it's contents.
24+
if (inBrowser) {
25+
if (el[BVT_STATE] === true) {
26+
addClass(el, 'collapsed')
27+
setAttr(el, 'aria-expanded', 'true')
28+
} else if (el[BVT_STATE] === false) {
29+
removeClass(el, 'collapsed')
30+
setAttr(el, 'aria-expanded', 'false')
31+
}
32+
setAttr(el, 'aria-controls', el[BVT_CONTROLS])
33+
}
34+
}
35+
1936
export default {
2037
bind(el, binding, vnode) {
2138
const targets = target(vnode, binding, listenTypes, ({ targets, vnode }) => {
@@ -26,7 +43,10 @@ export default {
2643

2744
if (inBrowser && vnode.context && targets.length > 0) {
2845
// Add aria attributes to element
29-
setAttr(el, 'aria-controls', targets.join(' '))
46+
el[BVT_CONTROLS] = targets.join(' ')
47+
// state is initialy collapsed until we receive a state event
48+
el[BVT_STATE] = false
49+
setAttr(el, 'aria-controls', el[BVT_CONTROLS])
3050
setAttr(el, 'aria-expanded', 'false')
3151
if (el.tagName !== 'BUTTON') {
3252
// If element is not a button, we add `role="button"` for accessibility
@@ -39,6 +59,7 @@ export default {
3959
// Set aria-expanded state
4060
setAttr(el, 'aria-expanded', state ? 'true' : 'false')
4161
// Set/Clear 'collapsed' class state
62+
el[BVT_STATE] = state
4263
if (state) {
4364
removeClass(el, 'collapsed')
4465
} else {
@@ -51,11 +72,18 @@ export default {
5172
vnode.context.$root.$on(EVENT_STATE, el[BVT])
5273
}
5374
},
75+
componentUpdated: handleUpdate,
76+
updated: handleUpdate,
5477
unbind(el, binding, vnode) /* istanbul ignore next */ {
5578
if (el[BVT]) {
5679
// Remove our $root listener
5780
vnode.context.$root.$off(EVENT_STATE, el[BVT])
5881
el[BVT] = null
82+
el[BVT_STATE] = null
83+
el[BVT_CONTROLS] = null
84+
removeClass(el, 'collapsed')
85+
removeAttr(el, 'aria-expanded')
86+
removeAttr(el, 'aria-controls')
5987
}
6088
}
6189
}

0 commit comments

Comments
 (0)