diff --git a/packages/api-generator/src/locale/en/VList.json b/packages/api-generator/src/locale/en/VList.json index e32f07fd764..39410cc7105 100644 --- a/packages/api-generator/src/locale/en/VList.json +++ b/packages/api-generator/src/locale/en/VList.json @@ -6,6 +6,7 @@ "disabled": "Puts all children inputs into a disabled state.", "filterable": "**FOR INTERNAL USE ONLY** Prevents list item selection using [space] key and pass it back to the text input. Used internally for VAutocomplete and VCombobox.", "inactive": "If set, the list tile will not be rendered as a link even if it has to/href prop or @click handler.", + "itemsRegistration": "When set to 'props', skips rendering collapsed items/nodes (for significant performance gains).", "lines": "Designates a **minimum-height** for all children `v-list-item` components. This prop uses [line-clamp](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp) and is not supported in all browsers.", "link": "Applies `v-list-item` hover styles. Useful when using the item is an _activator_.", "nav": "An alternative styling that reduces `v-list-item` width and rounds the corners. Typically used with **[v-navigation-drawer](/components/navigation-drawers)**.", diff --git a/packages/docs/src/data/new-in.json b/packages/docs/src/data/new-in.json index 4c45716ba46..23683bf0003 100644 --- a/packages/docs/src/data/new-in.json +++ b/packages/docs/src/data/new-in.json @@ -206,8 +206,9 @@ }, "VList": { "props": { - "prependGap": "3.11.0", - "indent": "3.11.0" + "indent": "3.11.0", + "itemsRegistration": "3.11.0", + "prependGap": "3.11.0" } }, "VListItem": { @@ -344,7 +345,8 @@ "hideNoData": "3.10.0", "noDataText": "3.10.0", "separateRoots": "3.9.0", - "indentLines": "3.9.0" + "indentLines": "3.9.0", + "itemsRegistration": "3.11.0" }, "slots": { "header": "3.10.0", diff --git a/packages/vuetify/src/components/VList/VList.tsx b/packages/vuetify/src/components/VList/VList.tsx index 671bdf31815..662232432d7 100644 --- a/packages/vuetify/src/components/VList/VList.tsx +++ b/packages/vuetify/src/components/VList/VList.tsx @@ -172,7 +172,8 @@ export const VList = genericComponent props.returnObject) + const lineClasses = toRef(() => props.lines ? `v-list--${props.lines}-line` : undefined) const activeColor = toRef(() => props.activeColor) const baseColor = toRef(() => props.baseColor) diff --git a/packages/vuetify/src/components/VList/VListGroup.tsx b/packages/vuetify/src/components/VList/VListGroup.tsx index 4b1295bc4bd..5aee9ea25b8 100644 --- a/packages/vuetify/src/components/VList/VListGroup.tsx +++ b/packages/vuetify/src/components/VList/VListGroup.tsx @@ -6,13 +6,13 @@ import { VDefaultsProvider } from '@/components/VDefaultsProvider' import { useList } from './list' import { makeComponentProps } from '@/composables/component' import { IconValue } from '@/composables/icons' -import { useNestedGroupActivator, useNestedItem } from '@/composables/nested/nested' +import { useNestedGroupActivator, useNestedItem, VNestedSymbol } from '@/composables/nested/nested' import { useSsrBoot } from '@/composables/ssrBoot' import { makeTagProps } from '@/composables/tag' import { MaybeTransition } from '@/composables/transition' // Utilities -import { computed } from 'vue' +import { computed, inject, toRef } from 'vue' import { defineComponent, genericComponent, propsFactory, useRender } from '@/util' export type VListGroupSlots = { @@ -67,6 +67,9 @@ export const VListGroup = genericComponent()({ const list = useList() const { isBooted } = useSsrBoot() + const parent = inject(VNestedSymbol) + const renderWhenClosed = toRef(() => parent?.root?.itemsRegistration.value === 'render') + function onClick (e: Event) { if (['INPUT', 'TEXTAREA'].includes((e.target as Element)?.tagName)) return open(!isOpen.value, e) @@ -114,9 +117,16 @@ export const VListGroup = genericComponent()({ )} -
- { slots.default?.() } -
+ { renderWhenClosed.value + ? ( +
+ { slots.default?.() } +
+ ) : isOpen.value && ( +
+ { slots.default?.() } +
+ )}
)) diff --git a/packages/vuetify/src/components/VTreeview/__tests__/VTreeview.spec.browser.tsx b/packages/vuetify/src/components/VTreeview/__tests__/VTreeview.spec.browser.tsx index 42b4ed2c500..d3c18534e34 100644 --- a/packages/vuetify/src/components/VTreeview/__tests__/VTreeview.spec.browser.tsx +++ b/packages/vuetify/src/components/VTreeview/__tests__/VTreeview.spec.browser.tsx @@ -60,9 +60,11 @@ const items = [ ] describe.each([ - ['plain', items], - ['reactive', reactive(items)], -])('VTreeview with %s items', (_, items) => { + ['plain', 'render', items], + ['reactive', 'render', reactive(items)], + ['plain', 'props', items], + ['reactive', 'props', reactive(items)], +] as const)('VTreeview with %s items and %s registration', (_, itemsRegistration, items) => { describe('activate', () => { it('single-leaf strategy', async () => { const activated = ref([]) @@ -74,6 +76,7 @@ describe.each([ itemValue="id" activatable activeStrategy="single-leaf" + itemsRegistration={ itemsRegistration } /> )) @@ -96,6 +99,7 @@ describe.each([ itemValue="id" activatable activeStrategy="leaf" + itemsRegistration={ itemsRegistration } /> )) @@ -118,6 +122,7 @@ describe.each([ itemValue="id" activatable activeStrategy="independent" + itemsRegistration={ itemsRegistration } /> )) @@ -143,6 +148,7 @@ describe.each([ itemValue="id" activatable activeStrategy="single-independent" + itemsRegistration={ itemsRegistration } /> )) @@ -169,6 +175,7 @@ describe.each([ activatable activeStrategy="independent" onUpdate:activated={ onActivated } + itemsRegistration={ itemsRegistration } /> )) @@ -191,6 +198,7 @@ describe.each([ itemValue="id" selectable selectStrategy="single-leaf" + itemsRegistration={ itemsRegistration } /> )) @@ -212,6 +220,7 @@ describe.each([ itemValue="id" selectable selectStrategy="leaf" + itemsRegistration={ itemsRegistration } /> )) @@ -233,6 +242,7 @@ describe.each([ itemValue="id" selectable selectStrategy="independent" + itemsRegistration={ itemsRegistration } /> )) @@ -257,6 +267,7 @@ describe.each([ itemValue="id" selectable selectStrategy="single-independent" + itemsRegistration={ itemsRegistration } /> )) @@ -279,6 +290,7 @@ describe.each([ itemValue="id" selectable selectStrategy="classic" + itemsRegistration={ itemsRegistration } /> )) @@ -301,13 +313,14 @@ describe.each([ items={ items } itemValue="id" returnObject + itemsRegistration={ itemsRegistration } /> )) await userEvent.click(screen.getByText(/Vuetify/).parentElement!.previousElementSibling!) - await expect.element(screen.getByText(/Core/)).toBeVisible() + await expect.element(screen.getByText(/Core/)).toBeDisplayed() await userEvent.click(screen.getByText(/Vuetify/).parentElement!.previousElementSibling!) - await expect.element(screen.getByText(/Core/)).not.toBeVisible() + await expect.poll(() => screen.queryByText(/Core/)).not.toBeDisplayed() }) it('open-all should work', async () => { @@ -317,6 +330,7 @@ describe.each([ items={ items } itemValue="id" returnObject + itemsRegistration={ itemsRegistration } /> )) @@ -335,6 +349,7 @@ describe.each([ items={ items } itemValue="id" returnObject + itemsRegistration={ itemsRegistration } /> )) @@ -389,6 +404,7 @@ describe.each([ activatable activeStrategy="leaf" returnObject + itemsRegistration={ itemsRegistration } /> )) @@ -416,6 +432,7 @@ describe.each([ activatable activeStrategy="independent" returnObject + itemsRegistration={ itemsRegistration } /> )) @@ -453,6 +470,7 @@ describe.each([ activatable activeStrategy="single-independent" returnObject + itemsRegistration={ itemsRegistration } /> )) @@ -487,6 +505,7 @@ describe.each([ returnObject selectable selectStrategy="single-leaf" + itemsRegistration={ itemsRegistration } /> )) @@ -513,6 +532,7 @@ describe.each([ returnObject selectable selectStrategy="leaf" + itemsRegistration={ itemsRegistration } /> )) @@ -542,6 +562,7 @@ describe.each([ returnObject selectable selectStrategy="independent" + itemsRegistration={ itemsRegistration } /> )) @@ -580,6 +601,7 @@ describe.each([ returnObject selectable selectStrategy="single-independent" + itemsRegistration={ itemsRegistration } /> )) @@ -608,6 +630,7 @@ describe.each([ selectable returnObject selectStrategy="classic" + itemsRegistration={ itemsRegistration } /> )) @@ -645,6 +668,7 @@ describe.each([ itemValue="id" openAll returnObject + itemsRegistration={ itemsRegistration } /> )) @@ -667,6 +691,7 @@ describe.each([ openAll items={ items } itemValue="id" + itemsRegistration={ itemsRegistration } /> )) @@ -685,6 +710,7 @@ describe.each([ itemValue="id" openOnClick returnObject + itemsRegistration={ itemsRegistration } > {{ prepend: ({ isOpen }) => ({ `${isOpen}` }), @@ -692,10 +718,9 @@ describe.each([ )) - const itemsPrepend = screen.getAllByCSS('.v-treeview-item .v-list-item__prepend .prepend-is-open') - await userEvent.click(screen.getByText(/Vuetify Human Resources/)) await waitIdle() + const itemsPrepend = screen.getAllByCSS('.v-treeview-item .v-list-item__prepend .prepend-is-open') expect(itemsPrepend[0]).toHaveTextContent(/^true$/) expect(itemsPrepend[1]).toHaveTextContent(/^false$/) @@ -712,7 +737,6 @@ describe.each([ await userEvent.click(screen.getByText(/Vuetify Human Resources/)) await waitIdle() expect(itemsPrepend[0]).toHaveTextContent(/^false$/) - expect(itemsPrepend[1]).toHaveTextContent(/^false$/) }) }) diff --git a/packages/vuetify/src/composables/nested/nested.ts b/packages/vuetify/src/composables/nested/nested.ts index 9633ed57da9..f813b8147a0 100644 --- a/packages/vuetify/src/composables/nested/nested.ts +++ b/packages/vuetify/src/composables/nested/nested.ts @@ -38,6 +38,7 @@ import type { InjectionKey, MaybeRefOrGetter, PropType, Ref } from 'vue' import type { ActiveStrategy } from './activeStrategies' import type { OpenStrategy } from './openStrategies' import type { SelectStrategy } from './selectStrategies' +import type { ListItem } from '@/composables/list-items' import type { EventProp } from '@/util' export type ActiveStrategyProp = @@ -57,6 +58,7 @@ export type SelectStrategyProp = | SelectStrategy | ((mandatory: boolean) => SelectStrategy) export type OpenStrategyProp = 'single' | 'multiple' | 'list' | OpenStrategy +export type ItemsRegistrationType = 'props' | 'render' export interface NestedProps { activatable: boolean @@ -68,6 +70,7 @@ export interface NestedProps { selected: any opened: any mandatory: boolean + itemsRegistration: ItemsRegistrationType 'onUpdate:activated': EventProp<[any]> | undefined 'onUpdate:selected': EventProp<[any]> | undefined 'onUpdate:opened': EventProp<[any]> | undefined @@ -86,6 +89,7 @@ type NestedProvide = { activated: Ref> selected: Ref> selectedValues: Ref + itemsRegistration: Ref register: (id: unknown, parentId: unknown, isDisabled: boolean, isGroup?: boolean) => void unregister: (id: unknown) => void open: (id: unknown, value: boolean, event?: Event) => void @@ -101,6 +105,7 @@ export const VNestedSymbol: InjectionKey = Symbol.for('vuetify:ne export const emptyNested: NestedProvide = { id: shallowRef(), root: { + itemsRegistration: ref('render'), register: () => null, unregister: () => null, children: ref(new Map()), @@ -130,9 +135,13 @@ export const makeNestedProps = propsFactory({ activated: null, selected: null, mandatory: Boolean, + itemsRegistration: { + type: String as PropType, + default: 'render', + }, }, 'nested') -export const useNested = (props: NestedProps) => { +export const useNested = (props: NestedProps, items: Ref, returnObject: MaybeRefOrGetter) => { let isUnmounted = false const children = shallowRef(new Map()) const parents = shallowRef(new Map()) @@ -227,6 +236,48 @@ export const useNested = (props: NestedProps) => { }) }, 100) + watch(() => [items.value, toValue(returnObject)], () => { + if (props.itemsRegistration === 'props') { + updateInternalMaps() + } + }, { immediate: true }) + + function updateInternalMaps () { + const _parents = new Map() + const _children = new Map() + const _disabled = new Set() + + const getValue = toValue(returnObject) + ? (item: ListItem) => toRaw(item.raw) + : (item: ListItem) => item.value + + const stack = [...items.value] + let i = 0 + while (i < stack.length) { + const item = stack[i++] + const itemValue = getValue(item) + + if (item.children) { + const childValues = [] + for (const child of item.children) { + const childValue = getValue(child) + _parents.set(childValue, itemValue) + childValues.push(childValue) + stack.push(child) + } + _children.set(itemValue, childValues) + } + + if (item.props.disabled) { + _disabled.add(itemValue) + } + } + + children.value = _children + parents.value = _parents + disabled.value = _disabled + } + const nested: NestedProvide = { id: shallowRef(), root: { @@ -244,6 +295,7 @@ export const useNested = (props: NestedProps) => { return arr }), + itemsRegistration: toRef(() => props.itemsRegistration), register: (id, parentId, isDisabled, isGroup) => { if (nodeIds.has(id)) { const path = getPath(id).map(String).join(' -> ') @@ -391,26 +443,23 @@ export const useNestedItem = (id: MaybeRefOrGetter, isDisabled: MaybeRe } onBeforeMount(() => { - if (!parent.isGroupActivator) { - nextTick(() => { - parent.root.register(computedId.value, parent.id.value, toValue(isDisabled), isGroup) - }) - } + if (parent.isGroupActivator || parent.root.itemsRegistration.value === 'props') return + nextTick(() => { + parent.root.register(computedId.value, parent.id.value, toValue(isDisabled), isGroup) + }) }) onBeforeUnmount(() => { - if (!parent.isGroupActivator) { - parent.root.unregister(computedId.value) - } + if (parent.isGroupActivator || parent.root.itemsRegistration.value === 'props') return + parent.root.unregister(computedId.value) }) watch(computedId, (val, oldVal) => { - if (!parent.isGroupActivator) { - parent.root.unregister(oldVal) - nextTick(() => { - parent.root.register(val, parent.id.value, toValue(isDisabled), isGroup) - }) - } + if (parent.isGroupActivator || parent.root.itemsRegistration.value === 'props') return + parent.root.unregister(oldVal) + nextTick(() => { + parent.root.register(val, parent.id.value, toValue(isDisabled), isGroup) + }) }) isGroup && provide(VNestedSymbol, item) diff --git a/packages/vuetify/test/setup/browser-setup.ts b/packages/vuetify/test/setup/browser-setup.ts index 6d9b4db6c38..66153fdf387 100644 --- a/packages/vuetify/test/setup/browser-setup.ts +++ b/packages/vuetify/test/setup/browser-setup.ts @@ -19,7 +19,9 @@ beforeEach(async () => { expect.extend({ /** .toBeVisible but using wdio's isDisplayed */ async toBeDisplayed (received: Element) { - const isDisplayed = await commands.isDisplayed(page.elementLocator(received).selector) + const isDisplayed = received != null && ( + await commands.isDisplayed(page.elementLocator(received).selector) + ) return { pass: isDisplayed,