Skip to content

Commit

Permalink
fix(nested): Prevent infinite loops when resolving path (#20390)
Browse files Browse the repository at this point in the history
closes #20389

Co-authored-by: jsek <[email protected]>
Co-authored-by: Kael <[email protected]>
  • Loading branch information
3 people authored Aug 31, 2024
1 parent f95a405 commit 970f827
Show file tree
Hide file tree
Showing 5 changed files with 32 additions and 36 deletions.
3 changes: 2 additions & 1 deletion packages/vuetify/src/components/VList/VList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export const VList = genericComponent<new <
const { dimensionStyles } = useDimension(props)
const { elevationClasses } = useElevation(props)
const { roundedClasses } = useRounded(props)
const { children, open, parents, select } = useNested(props)
const { children, open, parents, select, getPath } = useNested(props)
const lineClasses = computed(() => props.lines ? `v-list--${props.lines}-line` : undefined)
const activeColor = toRef(props, 'activeColor')
const baseColor = toRef(props, 'baseColor')
Expand Down Expand Up @@ -288,6 +288,7 @@ export const VList = genericComponent<new <
focus,
children,
parents,
getPath,
}
},
})
Expand Down
3 changes: 3 additions & 0 deletions packages/vuetify/src/components/VList/VListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export const VListItem = genericComponent<VListItemSlots>()({
root,
parent,
openOnSelect,
id: uid,
} = useNestedItem(id, false)
const list = useList()
const isActive = computed(() =>
Expand Down Expand Up @@ -368,6 +369,8 @@ export const VListItem = genericComponent<VListItemSlots>()({
isSelected,
list,
select,
root,
id: uid,
}
},
})
Expand Down
17 changes: 16 additions & 1 deletion packages/vuetify/src/composables/nested/nested.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
leafSelectStrategy,
leafSingleSelectStrategy,
} from './selectStrategies'
import { getCurrentInstance, getUid, propsFactory } from '@/util'
import { consoleError, getCurrentInstance, getUid, propsFactory } from '@/util'

// Types
import type { InjectionKey, PropType, Ref } from 'vue'
Expand Down Expand Up @@ -76,6 +76,7 @@ type NestedProvide = {
activate: (id: unknown, value: boolean, event?: Event) => void
select: (id: unknown, value: boolean, event?: Event) => void
openOnSelect: (id: unknown, value: boolean, event?: Event) => void
getPath: (id: unknown) => unknown[]
}
}

Expand All @@ -98,6 +99,7 @@ export const emptyNested: NestedProvide = {
activated: ref(new Set()),
selected: ref(new Map()),
selectedValues: ref([]),
getPath: () => [],
},
}

Expand Down Expand Up @@ -191,6 +193,8 @@ export const useNested = (props: NestedProps) => {

const vm = getCurrentInstance('nested')

const nodeIds = new Set<unknown>()

const nested: NestedProvide = {
id: shallowRef(),
root: {
Expand All @@ -209,6 +213,15 @@ export const useNested = (props: NestedProps) => {
return arr
}),
register: (id, parentId, isGroup) => {
if (nodeIds.has(id)) {
const path = getPath(id).join(' -> ')
const newPath = getPath(parentId).concat(id).join(' -> ')
consoleError(`Multiple nodes with the same ID\n\t${path}\n\t${newPath}`)
return
} else {
nodeIds.add(id)
}

parentId && id !== parentId && parents.value.set(id, parentId)

isGroup && children.value.set(id, [])
Expand All @@ -220,6 +233,7 @@ export const useNested = (props: NestedProps) => {
unregister: id => {
if (isUnmounted) return

nodeIds.delete(id)
children.value.delete(id)
const parent = parents.value.get(id)
if (parent) {
Expand Down Expand Up @@ -289,6 +303,7 @@ export const useNested = (props: NestedProps) => {
},
children,
parents,
getPath,
},
}

Expand Down
16 changes: 3 additions & 13 deletions packages/vuetify/src/labs/VTreeview/VTreeview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,24 +86,14 @@ export const VTreeview = genericComponent<new <T>(
const search = toRef(props, 'search')
const { filteredItems } = useFilter(props, flatItems, search)
const visibleIds = computed(() => {
if (!search.value) {
return null
}
if (!search.value) return null
const getPath = vListRef.value?.getPath
if (!getPath) return null
return new Set(filteredItems.value.flatMap(item => {
return [...getPath(item.props.value), ...getChildren(item.props.value)]
}))
})

function getPath (id: unknown) {
const path: unknown[] = []
let parent: unknown = id
while (parent != null) {
path.unshift(parent)
parent = vListRef.value?.parents.get(parent)
}
return path
}

function getChildren (id: unknown) {
const arr: unknown[] = []
const queue = ((vListRef.value?.children.get(id) ?? []).slice())
Expand Down
29 changes: 8 additions & 21 deletions packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { VProgressCircular } from '@/components/VProgressCircular'

// Composables
import { IconValue } from '@/composables/icons'
import { useNestedItem } from '@/composables/nested/nested'
import { useLink } from '@/composables/router'

// Utilities
Expand All @@ -35,20 +34,11 @@ export const VTreeviewItem = genericComponent<VListItemSlots>()({

setup (props, { attrs, slots, emit }) {
const link = useLink(props, attrs)
const rawId = computed(() => props.value === undefined ? link.href.value : props.value)
const vListItemRef = ref<VListItem>()

const {
activate,
isActivated,
isGroupActivator,
root,
id,
} = useNestedItem(rawId, false)

const isActivatableGroupActivator = computed(() =>
(root.activatable.value) &&
isGroupActivator
(vListItemRef.value?.root.activatable.value) &&
vListItemRef.value?.isGroupActivator
)

const isClickable = computed(() =>
Expand All @@ -60,15 +50,11 @@ export const VTreeviewItem = genericComponent<VListItemSlots>()({
function activateItem (e: MouseEvent | KeyboardEvent) {
if (
!isClickable.value ||
(!isActivatableGroupActivator.value && isGroupActivator)
(!isActivatableGroupActivator.value && vListItemRef.value?.isGroupActivator)
) return

if (root.activatable.value) {
if (isActivatableGroupActivator.value) {
activate(!isActivated.value, e)
} else {
vListItemRef.value?.activate(!vListItemRef.value?.isActivated, e)
}
if (vListItemRef.value?.root.activatable.value) {
vListItemRef.value?.activate(!vListItemRef.value?.isActivated, e)
}
}

Expand All @@ -80,13 +66,14 @@ export const VTreeviewItem = genericComponent<VListItemSlots>()({

return (
<VListItem
ref={ vListItemRef }
{ ...listItemProps }
active={ isActivated.value }
active={ vListItemRef.value?.isActivated }
class={[
'v-treeview-item',
{
'v-treeview-item--activatable-group-activator': isActivatableGroupActivator.value,
'v-treeview-item--filtered': visibleIds.value && !visibleIds.value.has(id.value),
'v-treeview-item--filtered': visibleIds.value && !visibleIds.value.has(vListItemRef.value?.id),
},
props.class,
]}
Expand Down

0 comments on commit 970f827

Please sign in to comment.