Skip to content
This repository was archived by the owner on Mar 27, 2025. It is now read-only.

Commit e55a371

Browse files
committed
feat(BToggle): Reactive targets
Implements support for reactively updated targets, matching bootstrap-vue's behavior.
1 parent 5966dab commit e55a371

File tree

3 files changed

+36
-17
lines changed

3 files changed

+36
-17
lines changed

packages/bootstrap-vue-next/src/directives/BToggle.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,18 @@ const getTargets = (binding: DirectiveBinding<string>, el: HTMLElement) => {
2929
return targets.filter((t, index, arr) => t && arr.indexOf(t) === index)
3030
}
3131

32-
const toggle = (binding: DirectiveBinding<string>, el: HTMLElement) => {
33-
const targetIds = getTargets(binding, el)
32+
const toggle = (targetIds: string[], el: HTMLElement) => {
3433
targetIds.forEach((targetId) => {
3534
const target = document.getElementById(targetId)
3635

3736
if (target !== null) {
3837
target.dispatchEvent(new Event('bv-toggle'))
3938
}
4039
})
41-
setTimeout(() => checkVisibility(binding, el), 50)
40+
setTimeout(() => checkVisibility(targetIds, el), 50)
4241
}
4342

44-
const checkVisibility = (binding: DirectiveBinding<string>, el: HTMLElement) => {
45-
const targetIds = getTargets(binding, el)
43+
const checkVisibility = (targetIds: string[], el: HTMLElement) => {
4644
let visible = false
4745
targetIds.forEach((targetId) => {
4846
const target = document.getElementById(targetId)
@@ -59,17 +57,29 @@ const checkVisibility = (binding: DirectiveBinding<string>, el: HTMLElement) =>
5957
el.classList.add(visible ? 'not-collapsed' : 'collapsed')
6058
}
6159

60+
const handleUpdate = (el: WithToggle, binding: DirectiveBinding<string>) => {
61+
// Determine targets
62+
const targets = getTargets(binding, el)
63+
64+
// Set up click handler
65+
if (el.__toggle) {
66+
el.removeEventListener('click', el.__toggle)
67+
}
68+
el.__toggle = () => toggle(targets, el)
69+
el.addEventListener('click', el.__toggle)
70+
71+
// Update attributes
72+
el.setAttribute('aria-controls', targets.join(' '))
73+
checkVisibility(targets, el)
74+
}
75+
6276
export interface WithToggle extends HTMLElement {
6377
__toggle: () => void
6478
}
6579

6680
export default {
67-
mounted(el: WithToggle, binding: DirectiveBinding<string>): void {
68-
el.__toggle = () => toggle(binding, el)
69-
el.addEventListener('click', el.__toggle)
70-
checkVisibility(binding, el)
71-
el.setAttribute('aria-controls', getTargets(binding, el).join(' '))
72-
},
81+
mounted: handleUpdate,
82+
updated: handleUpdate,
7383
unmounted(el: WithToggle): void {
7484
el.removeEventListener('click', el.__toggle)
7585
el.removeAttribute('aria-controls')

packages/bootstrap-vue-next/src/directives/toggle.spec.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {enableAutoUnmount, flushPromises, mount} from '@vue/test-utils'
22
import {afterEach, describe, expect, it, vi} from 'vitest'
33
import {nextTick} from 'vue'
44
import VBToggle from './BToggle'
5+
import {asyncTimeout} from '../../tests/utils'
56

67
// Emitted control event for collapse (emitted to collapse)
78
const EVENT_TOGGLE = 'bv-toggle'
@@ -40,6 +41,7 @@ describe('toggle directive', () => {
4041
expect($button.classes()).not.toContain('not-collapsed')
4142

4243
await $button.trigger('click')
44+
await asyncTimeout(50)
4345
expect(spy).toHaveBeenCalledTimes(1)
4446

4547
// Since there is no target collapse to respond with the
@@ -81,6 +83,7 @@ describe('toggle directive', () => {
8183
expect($button.classes()).not.toContain('not-collapsed')
8284

8385
await $button.trigger('click')
86+
await asyncTimeout(50)
8487
expect(spy).toHaveBeenCalledTimes(1)
8588

8689
// Since there is no target collapse to respond with the
@@ -121,6 +124,7 @@ describe('toggle directive', () => {
121124
expect($button.classes()).not.toContain('not-collapsed')
122125

123126
await $button.trigger('click')
127+
await asyncTimeout(50)
124128
expect(spy).toHaveBeenCalledTimes(1)
125129

126130
// Since there is no target collapse to respond with the
@@ -162,6 +166,7 @@ describe('toggle directive', () => {
162166
expect($link.classes()).not.toContain('not-collapsed')
163167

164168
await $link.trigger('click')
169+
await asyncTimeout(50)
165170
expect(spy).toHaveBeenCalledTimes(1)
166171

167172
// Since there is no target collapse to respond with the
@@ -173,8 +178,7 @@ describe('toggle directive', () => {
173178
expect($link.classes()).not.toContain('not-collapsed')
174179
})
175180

176-
// Does not currently support dynamic updates to target list (bootstrap-vue does)
177-
it.skip('works with multiple targets, and updates when targets change', async () => {
181+
it('works with multiple targets, and updates when targets change', async () => {
178182
const spy1 = vi.fn()
179183
const spy2 = vi.fn()
180184
const App = {
@@ -188,12 +192,12 @@ describe('toggle directive', () => {
188192
},
189193
},
190194
mounted() {
191-
document.getElementById('test')?.addEventListener(EVENT_TOGGLE, spy1)
192-
document.getElementById('test')?.addEventListener(EVENT_TOGGLE, spy2)
195+
document.getElementById('test1')?.addEventListener(EVENT_TOGGLE, spy1)
196+
document.getElementById('test2')?.addEventListener(EVENT_TOGGLE, spy2)
193197
},
194198
destroy() {
195-
document.getElementById('test')?.removeEventListener(EVENT_TOGGLE, spy1)
196-
document.getElementById('test')?.removeEventListener(EVENT_TOGGLE, spy2)
199+
document.getElementById('test1')?.removeEventListener(EVENT_TOGGLE, spy1)
200+
document.getElementById('test2')?.removeEventListener(EVENT_TOGGLE, spy2)
197201
},
198202
template: `<button v-b-toggle="target">button</button><div id="test1"></div><div id="test2"></div>`,
199203
}
@@ -227,6 +231,7 @@ describe('toggle directive', () => {
227231
expect(spy2).not.toHaveBeenCalled()
228232

229233
await $button.trigger('click')
234+
await asyncTimeout(50)
230235
expect(spy1).toHaveBeenCalledTimes(1)
231236
expect(spy2).toHaveBeenCalledTimes(1)
232237

@@ -246,6 +251,7 @@ describe('toggle directive', () => {
246251
expect(spy2).toHaveBeenCalledTimes(1)
247252

248253
await $button.trigger('click')
254+
await asyncTimeout(50)
249255
expect(spy1).toHaveBeenCalledTimes(1)
250256
expect(spy2).toHaveBeenCalledTimes(2)
251257

@@ -296,6 +302,7 @@ describe('toggle directive', () => {
296302
expect($span.text()).toBe('span')
297303

298304
await $span.trigger('click')
305+
await asyncTimeout(50)
299306
expect(spy).toHaveBeenCalledTimes(1)
300307
expect($span.attributes('role')).toBe('button')
301308
expect($span.attributes('tabindex')).toBe('0')

packages/bootstrap-vue-next/tests/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ export const createContainer = (tag = 'div'): HTMLElement => {
55
}
66
export const waitRAF = (): Promise<number> =>
77
new Promise((resolve) => requestAnimationFrame(resolve))
8+
export const asyncTimeout = (timeout: number): Promise<void> =>
9+
new Promise((resolve) => setTimeout(resolve.bind(null), timeout))

0 commit comments

Comments
 (0)