Skip to content
Merged
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
22 changes: 9 additions & 13 deletions packages/bootstrap-vue-next/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,30 @@

## [0.40.9](https://github.com/bootstrap-vue-next/bootstrap-vue-next/compare/bootstrapvuenext-v0.40.8...bootstrapvuenext-v0.40.9) (2025-11-28)


### Bug Fixes

* allow custom component props in orchestrator create methods with type safety ([#2922](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2922)) ([fdf2359](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/fdf2359c1cc4f154a40de8ae4252dbb0eb0235c9))
* **BModal:** prevent focus trap error when no tabbable elements exist ([#2864](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2864)) ([d5d85f2](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/d5d85f2c741c789a3ca94e442ff371af73b49cbe))
* **BTableLite:** Use primary key to persist row-details state when items change ([#2871](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2871)) ([a933f96](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/a933f96d1cf4bd1bc82645f18e1c9410e050ad76))
- allow custom component props in orchestrator create methods with type safety ([#2922](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2922)) ([fdf2359](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/fdf2359c1cc4f154a40de8ae4252dbb0eb0235c9))
- **BModal:** prevent focus trap error when no tabbable elements exist ([#2864](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2864)) ([d5d85f2](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/d5d85f2c741c789a3ca94e442ff371af73b49cbe))
- **BTableLite:** Use primary key to persist row-details state when items change ([#2871](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2871)) ([a933f96](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/a933f96d1cf4bd1bc82645f18e1c9410e050ad76))

## [0.40.8](https://github.com/bootstrap-vue-next/bootstrap-vue-next/compare/bootstrapvuenext-v0.40.7...bootstrapvuenext-v0.40.8) (2025-11-17)


### Features

* add name and form props to BFormRating for form submission ([#2895](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2895)) ([f14f049](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/f14f0495a03678a4f6c0d0ee87d3eabfc6def136))
* **BTable:** add an AbortSignal to the provider object parameter for cancelling in progress requests ([2a12859](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/2a12859404a4ee498e6ccc4aa5490dab9997d7c7))
* **BTable:** add configurable debouncing ([2a12859](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/2a12859404a4ee498e6ccc4aa5490dab9997d7c7))

- add name and form props to BFormRating for form submission ([#2895](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2895)) ([f14f049](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/f14f0495a03678a4f6c0d0ee87d3eabfc6def136))
- **BTable:** add an AbortSignal to the provider object parameter for cancelling in progress requests ([2a12859](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/2a12859404a4ee498e6ccc4aa5490dab9997d7c7))
- **BTable:** add configurable debouncing ([2a12859](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/2a12859404a4ee498e6ccc4aa5490dab9997d7c7))

### Bug Fixes

* **directives:** Robustness fixes for directives ([#2906](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2906)) ([7b39759](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/7b397599f76f50d10773cd8fb63fb6d2e72dc4c7))
* **typings:** Fix paths to `*.d.mts` files ([#2907](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2907)) ([4b0d55a](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/4b0d55a957f029131c89f740adc65ca7d9e79d58))
- **directives:** Robustness fixes for directives ([#2906](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2906)) ([7b39759](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/7b397599f76f50d10773cd8fb63fb6d2e72dc4c7))
- **typings:** Fix paths to `*.d.mts` files ([#2907](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2907)) ([4b0d55a](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/4b0d55a957f029131c89f740adc65ca7d9e79d58))

## [0.40.7](https://github.com/bootstrap-vue-next/bootstrap-vue-next/compare/bootstrapvuenext-v0.40.6...bootstrapvuenext-v0.40.7) (2025-10-22)


### Features

* add NumpadEnter support for BTable and BFormTags keyboard navigation (accessibility) ([#2884](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2884)) ([bdf6fee](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/bdf6fee56eaf708d1b14e89f0179c4d44b7bf063))
- add NumpadEnter support for BTable and BFormTags keyboard navigation (accessibility) ([#2884](https://github.com/bootstrap-vue-next/bootstrap-vue-next/issues/2884)) ([bdf6fee](https://github.com/bootstrap-vue-next/bootstrap-vue-next/commit/bdf6fee56eaf708d1b14e89f0179c4d44b7bf063))

## [0.40.6](https://github.com/bootstrap-vue-next/bootstrap-vue-next/compare/bootstrapvuenext-v0.40.5...bootstrapvuenext-v0.40.6) (2025-10-07)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@

<script setup lang="ts">
import type {BNavbarToggleProps} from '../../types/ComponentProps'
import {computed, inject, toValue} from 'vue'
import {computed, inject} from 'vue'
import {useDefaults} from '../../composables/useDefaults'
import {showHideRegistryKey} from '../../utils/keys'
import {getActiveShowHide, getShowHideValue} from '../../utils/registryAccess'
import type {BNavbarToggleEmits, BNavbarToggleSlots} from '../../types'

const _props = withDefaults(defineProps<BNavbarToggleProps>(), {
Expand All @@ -37,17 +38,20 @@ const showHideData = inject(showHideRegistryKey, undefined)

const collapseExpanded = computed(() => {
if (!props.target || !showHideData) return false
if (typeof props.target === 'string')
return toValue(toValue(showHideData.values.value.get(props.target))?.value) || false
return props.target.some((target) => toValue(showHideData.values.value.get(target)?.value))
if (typeof props.target === 'string') {
return getShowHideValue(showHideData.values, props.target)
}
return props.target.some((target) => getShowHideValue(showHideData.values, target))
})
const toggleExpand = () => {
if (!props.target || !showHideData) return
if (typeof props.target === 'string') {
toValue(showHideData.values.value.get(props.target))?.toggle()
getActiveShowHide(showHideData.values, props.target)?.toggle()
return
}
props.target.forEach((target) => toValue(showHideData.values.value.get(target))?.toggle())
props.target.forEach((target) => {
getActiveShowHide(showHideData.values, target)?.toggle()
})
}

const onClick = (e: Readonly<MouseEvent>): void => {
Expand Down
85 changes: 78 additions & 7 deletions packages/bootstrap-vue-next/src/composables/useRegistry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
breadcrumbRegistryKey,
modalManagerKey,
type RegisterShowHideFnInput,
type RegisterShowHideInstances,
type RegisterShowHideMapValue,
rtlRegistryKey,
showHideRegistryKey,
Expand Down Expand Up @@ -96,8 +97,12 @@ export const useRegistry = (rtl: BAppProps['rtl'] = false) => {
}
}

// Helper function to create getActive method for instance holders
const createGetActive = (instances: RegisterShowHideMapValue[]) => () =>
instances.length > 0 ? instances[instances.length - 1] : undefined

export const _newShowHideRegistry = () => {
const values: Ref<Map<string, RegisterShowHideMapValue>> = ref(new Map())
const values: Ref<Map<string, RegisterShowHideInstances>> = ref(new Map())

const register = ({
id,
Expand All @@ -109,7 +114,8 @@ export const _newShowHideRegistry = () => {
registerTrigger,
unregisterTrigger,
}: RegisterShowHideFnInput) => {
values.value.set(id, {
let currentId = id
const instanceValue: RegisterShowHideMapValue = {
id,
component,
value: readonly(value),
Expand All @@ -118,17 +124,82 @@ export const _newShowHideRegistry = () => {
hide,
registerTrigger,
unregisterTrigger,
})
}

// Get or create the instances array for this ID
let instancesHolder = values.value.get(currentId)
if (!instancesHolder) {
const instances: RegisterShowHideMapValue[] = []
instancesHolder = {
instances,
// Returns the last mounted instance (most recent)
getActive: createGetActive(instances),
}
values.value.set(currentId, instancesHolder)
}

// Append this instance to the array
instancesHolder.instances.push(instanceValue)

const componentUid = component.uid

return {
unregister() {
values.value.delete(id)
const holder = values.value.get(currentId)
if (!holder) return

// Remove only this component's instance by UID
const index = holder.instances.findIndex(
(inst: RegisterShowHideMapValue) => inst.component.uid === componentUid
)
if (index !== -1) {
holder.instances.splice(index, 1)
}

// Clean up the map entry if no instances remain
if (holder.instances.length === 0) {
values.value.delete(currentId)
}
},
updateId(newId: string, oldId: string) {
const existingValue = values.value.get(oldId)
if (existingValue) {
values.value.set(newId, {...existingValue, id: newId})
const holder = values.value.get(oldId)
if (!holder) return

// Find this component's instance in the array
const instance = holder.instances.find(
(inst: RegisterShowHideMapValue) => inst.component.uid === componentUid
)
if (!instance) return

// Update the ID in the instance
instance.id = newId

// Get or create holder for new ID
let newHolder = values.value.get(newId)
if (!newHolder) {
const instances: RegisterShowHideMapValue[] = []
newHolder = {
instances,
getActive: createGetActive(instances),
}
values.value.set(newId, newHolder)
}

// Move this instance to the new ID's array
const index = holder.instances.findIndex(
(inst: RegisterShowHideMapValue) => inst.component.uid === componentUid
)
if (index !== -1) {
holder.instances.splice(index, 1)
newHolder.instances.push(instance)
}

// Clean up old ID if no instances remain
if (holder.instances.length === 0) {
values.value.delete(oldId)
}
// Keep local id in sync so unregister() uses the latest key
currentId = newId
},
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from 'vue'

import {showHideRegistryKey} from '../../utils/keys'
import {getActiveShowHide} from '../../utils/registryAccess'

export const useToggle = (id: MaybeRefOrGetter<string | undefined> = undefined) => {
const instance = getCurrentInstance()
Expand Down Expand Up @@ -40,18 +41,17 @@ export const useToggle = (id: MaybeRefOrGetter<string | undefined> = undefined)
const myComponent = computed(() => {
const resolvedId = toValue(id)

if (!registry) return null
if (resolvedId) {
const value = registry.value.get(resolvedId)
return toValue(value) || null
return getActiveShowHide(registry, resolvedId)
}

if (!instance) {
return null
}

const component = findComponent(instance)
return toValue(registry.value.get(toValue(component?.exposed?.id))) || null
const componentId = toValue(component?.exposed?.id)
return getActiveShowHide(registry, componentId)
})

return {
Expand Down
25 changes: 13 additions & 12 deletions packages/bootstrap-vue-next/src/directives/BToggle/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {RX_HASH, RX_HASH_ID, RX_SPACE_SPLIT} from '../../utils/constants'
import {type Directive, type DirectiveBinding, toValue, type VNode} from 'vue'
import {type Directive, type DirectiveBinding, type VNode} from 'vue'
import {findProvides} from '../utils'
import {type RegisterShowHideValue, showHideRegistryKey} from '../../utils/keys'
import {getActiveShowHide} from '../../utils/registryAccess'

const getTargets = (
binding: DirectiveBinding<string | readonly string[] | undefined>,
Expand Down Expand Up @@ -43,18 +44,18 @@ const handleUpdate = (
if (targets.length === 0) return

const provides = findProvides(binding, vnode)
const showHideMap = (provides as Record<symbol, RegisterShowHideValue>)[showHideRegistryKey]
?.values
const showHideMap =
(provides as Record<symbol, RegisterShowHideValue>)[showHideRegistryKey]?.values ?? null
if ((el as HTMLElement).dataset.bvtoggle) {
const oldTargets = ((el as HTMLElement).dataset.bvtoggle || '').split(' ')
if (oldTargets.length === 0) return
for (const targetId of oldTargets) {
const showHide = showHideMap?.value.get(targetId)
const showHide = getActiveShowHide(showHideMap, targetId)
if (!showHide) {
continue
}
if (!targets.includes(targetId)) {
toValue(showHide).unregisterTrigger('click', el, false)
showHide.unregisterTrigger('click', el, false)
}
}
}
Expand All @@ -73,7 +74,7 @@ const handleUpdate = (
return
}

const showHide = showHideMap?.value.get(targetId)
const showHide = getActiveShowHide(showHideMap, targetId)
if (!showHide) {
count++
if (count < maxAttempts) {
Expand All @@ -94,8 +95,8 @@ const handleUpdate = (
if (!(el as HTMLElement).dataset.bvtoggle) return

// Register the trigger element
toValue(showHide).unregisterTrigger('click', el, false)
toValue(showHide).registerTrigger('click', el)
showHide.unregisterTrigger('click', el, false)
showHide.registerTrigger('click', el)
break
}
})
Expand All @@ -111,16 +112,16 @@ const handleUnmount = (
const targets = getTargets(binding, el)
if (targets.length === 0) return
const provides = findProvides(binding, vnode)
const showHideMap = (provides as Record<symbol, RegisterShowHideValue>)[showHideRegistryKey]
?.values
const showHideMap =
(provides as Record<symbol, RegisterShowHideValue>)[showHideRegistryKey]?.values ?? null

targets.forEach((targetId) => {
const showHide = showHideMap?.value.get(targetId)
const showHide = getActiveShowHide(showHideMap, targetId)
if (!showHide) {
return
}
// Pass clean=true to let the composable handle cleanup of aria-expanded and classes
toValue(showHide).unregisterTrigger('click', el, true)
showHide.unregisterTrigger('click', el, true)
})

// Only remove what the directive manages (aria-controls)
Expand Down
12 changes: 11 additions & 1 deletion packages/bootstrap-vue-next/src/utils/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,22 @@ export interface RegisterShowHideMapValue {
registerTrigger: (trigger: string, el: Element) => void
unregisterTrigger: (trigger: string, el: Element, clean: boolean) => void
}

/**
* Represents an array of component instances with the same ID
* Used to handle race conditions where multiple instances mount/unmount out of order
*/
export interface RegisterShowHideInstances {
instances: RegisterShowHideMapValue[]
getActive: () => RegisterShowHideMapValue | undefined
}

export interface RegisterShowHideValue {
register: (input: RegisterShowHideFnInput) => {
unregister: () => void
updateId: (newId: string, oldId: string) => void
}
values: Ref<Map<string, RegisterShowHideMapValue>>
values: Ref<Map<string, RegisterShowHideInstances>>
}
export const showHideRegistryKey: InjectionKey<RegisterShowHideValue> =
createBvnRegistryInjectionKey('showHide')
Expand Down
35 changes: 35 additions & 0 deletions packages/bootstrap-vue-next/src/utils/registryAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type {Ref} from 'vue'
import type {RegisterShowHideInstances, RegisterShowHideMapValue} from './keys'

/**
* Gets the active show/hide instance for a given ID.
* Uses getActive() which returns the most recent instance to handle UID disambiguation.
*
* @param registry - The show/hide registry ref (or null)
* @param id - The component ID to look up (or falsy value)
* @returns The active show/hide instance, undefined if not found, or null if id is falsy
*/
export const getActiveShowHide = (
registry: Ref<Map<string, RegisterShowHideInstances>> | null,
id: string | undefined
): RegisterShowHideMapValue | undefined | null => {
if (!id) return null
if (!registry) return undefined
const holder = registry.value.get(id)
return holder?.getActive()
}

/**
* Gets the boolean visibility state of a show/hide component.
*
* @param registry - The show/hide registry ref (or null)
* @param id - The component ID to look up
* @returns The visibility state (true if shown), or false if not found
*/
export const getShowHideValue = (
registry: Ref<Map<string, RegisterShowHideInstances>> | null,
id: string
): boolean => {
const instance = getActiveShowHide(registry, id)
return instance?.value.value ?? false
}
Loading
Loading