From ba81e21906df6450d4a742e36ffd172cb7fbaf4a Mon Sep 17 00:00:00 2001 From: MATILLAT Quentin Date: Tue, 27 Dec 2022 16:26:17 +0100 Subject: [PATCH] feat(VList): add lazy mode to VListGroup Add a `lazy` props to skip the rendering of children if the group has never been opened This allow to render instantly a list with ~1k elements (see example in docs) --- .../examples/v-list/misc-lazy-rendering.vue | 68 ++++++++++++++ .../docs/src/pages/en/components/lists.md | 26 ++---- .../src/components/VList/VListGroup.tsx | 88 +++++++++++-------- .../VList/__tests__/VListGroup.spec.cy.tsx | 58 ++++++++---- 4 files changed, 168 insertions(+), 72 deletions(-) create mode 100644 packages/docs/src/examples/v-list/misc-lazy-rendering.vue diff --git a/packages/docs/src/examples/v-list/misc-lazy-rendering.vue b/packages/docs/src/examples/v-list/misc-lazy-rendering.vue new file mode 100644 index 00000000000..35fcb074e1a --- /dev/null +++ b/packages/docs/src/examples/v-list/misc-lazy-rendering.vue @@ -0,0 +1,68 @@ + + + diff --git a/packages/docs/src/pages/en/components/lists.md b/packages/docs/src/pages/en/components/lists.md index e660967f2f6..bda8dbd131d 100644 --- a/packages/docs/src/pages/en/components/lists.md +++ b/packages/docs/src/pages/en/components/lists.md @@ -6,37 +6,23 @@ meta: keywords: lists, vuetify list component, vue list component related: - /components/item-groups/ - - /components/avatars/ - - /components/sheets/ --- # Lists The `v-list` component is used to display information. It can contain an avatar, content, actions, subheaders and much more. Lists present content in a way that makes it easy to identify a specific item in a collection. They provide a consistent styling for organizing groups of text and images. + + ## Usage Lists come in three main variations. **single-line** (default), **two-line** and **three-line**. The line declaration specifies the minimum height of the item and can also be controlled from `v-list` with the same prop. - - ## API -| Component | Description | -| - | - | -| [v-list](/api/v-list/) | Primary Component | -| [v-list-group](/api/v-list-group/) | Sub-component used to display or hide groups of items | -| [v-list-subheader](/api/v-list-subheader/) | Sub-component used to separate groups of items | -| [v-list-item](/api/v-list-item/) | Sub-component used to display a single item or modify the `v-list` state | -| [v-list-item-title](/api/v-list-item-title/) | Sub-component used to display the title of a list item. Wraps the `#title` slot | -| [v-list-item-subtitle](/api/v-list-item-subtitle/) | Sub-component used to display the subtitle of a list item. Wraps the `#subtitle` slot | -| [v-list-item-action](/api/v-list-item-action/) | Sub-component used to display [v-checkbox](/components/checkboxes/) or [v-switch](/components/switches/) | -| [v-list-img](/api/v-list-img/) | Sub-component that is used to wrap a the [v-img](/components/images/) component | -| [v-list-item-media](/api/v-list-item-media/) | Sub-component that is used to wrap a the [v-img](/components/images/) component | - - + ## Examples @@ -125,3 +111,9 @@ Lists can contain subheaders, dividers, and can contain 1 or more lines. The sub A **three-line** list with actions. Utilizing **v-list-group**, easily connect actions to your tiles. + +#### Lazy rendering + +A big list rendered instantly using *lazy* option. + + diff --git a/packages/vuetify/src/components/VList/VListGroup.tsx b/packages/vuetify/src/components/VList/VListGroup.tsx index 2651e3491cb..f80934d778b 100644 --- a/packages/vuetify/src/components/VList/VListGroup.tsx +++ b/packages/vuetify/src/components/VList/VListGroup.tsx @@ -3,21 +3,26 @@ import { VDefaultsProvider } from '@/components/VDefaultsProvider' import { VExpandTransition } from '@/components/transitions' // Composables +import { useList } from './list' import { IconValue } from '@/composables/icons' -import { makeComponentProps } from '@/composables/component' +import { makeLazyProps, useLazy } from '@/composables/lazy' import { makeTagProps } from '@/composables/tag' -import { MaybeTransition } from '@/composables/transition' -import { useList } from './list' import { useNestedGroupActivator, useNestedItem } from '@/composables/nested/nested' -import { useSsrBoot } from '@/composables/ssrBoot' // Utilities import { computed, toRef } from 'vue' -import { defineComponent, genericComponent, propsFactory, useRender } from '@/util' - -export type VListGroupSlots = { - default: [] - activator: [{ isOpen: boolean, props: Record }] +import { defineComponent, genericComponent, pick, propsFactory, useRender } from '@/util' + +// Types +import type { InternalListItem } from './VList' +import type { SlotsToProps } from '@/util' +import type { ExtractPropTypes, Ref } from 'vue' + +export type ListGroupActivatorSlot = { + props: { + onClick: (e: Event) => void + class: string + } } const VListGroupActivator = defineComponent({ @@ -31,9 +36,7 @@ const VListGroupActivator = defineComponent({ }) export const makeVListGroupProps = propsFactory({ - /* @deprecated */ activeColor: String, - baseColor: String, color: String, collapseIcon: { type: IconValue, @@ -47,47 +50,46 @@ export const makeVListGroupProps = propsFactory({ appendIcon: IconValue, fluid: Boolean, subgroup: Boolean, - title: String, value: null, - ...makeComponentProps(), + ...makeLazyProps(), ...makeTagProps(), }, 'v-list-group') -export const VListGroup = genericComponent()({ +export const VListGroup = genericComponent() => { + $props: { + items?: T[] + } & SlotsToProps<{ + activator: [ListGroupActivatorSlot] + default: [] + }> +}>()({ name: 'VListGroup', - props: makeVListGroupProps(), + props: { + title: String, + + ...makeVListGroupProps(), + }, setup (props, { slots }) { const { isOpen, open, id: _id } = useNestedItem(toRef(props, 'value'), true) const id = computed(() => `v-list-group--id-${String(_id.value)}`) const list = useList() - const { isBooted } = useSsrBoot() + + const { hasContent, onAfterLeave } = useLazy(props, isOpen) function onClick (e: Event) { open(!isOpen.value, e) } - const activatorProps = computed(() => ({ + const activatorProps: Ref = computed(() => ({ onClick, class: 'v-list-group__header', id: id.value, })) const toggleIcon = computed(() => isOpen.value ? props.collapseIcon : props.expandIcon) - const activatorDefaults = computed(() => ({ - VListItem: { - active: isOpen.value, - activeColor: props.activeColor, - baseColor: props.baseColor, - color: props.color, - prependIcon: props.prependIcon || (props.subgroup && toggleIcon.value), - appendIcon: props.appendIcon || (!props.subgroup && toggleIcon.value), - title: props.title, - value: props.value, - }, - })) useRender(() => ( ()({ 'v-list-group--subgroup': props.subgroup, 'v-list-group--open': isOpen.value, }, - props.class, ]} - style={ props.style } > { slots.activator && ( - + - { slots.activator({ props: activatorProps.value, isOpen: isOpen.value }) } + { slots.activator({ props: activatorProps.value, isOpen }) } )} - +
- { slots.default?.() } + { hasContent.value && slots.default?.() }
-
+
)) @@ -124,3 +136,7 @@ export const VListGroup = genericComponent()({ }) export type VListGroup = InstanceType + +export function filterListGroupProps (props: ExtractPropTypes>) { + return pick(props, Object.keys(VListGroup.props) as any) +} diff --git a/packages/vuetify/src/components/VList/__tests__/VListGroup.spec.cy.tsx b/packages/vuetify/src/components/VList/__tests__/VListGroup.spec.cy.tsx index 89535931f7a..0cfcde97c87 100644 --- a/packages/vuetify/src/components/VList/__tests__/VListGroup.spec.cy.tsx +++ b/packages/vuetify/src/components/VList/__tests__/VListGroup.spec.cy.tsx @@ -5,6 +5,28 @@ import { VListGroup } from '../VListGroup' import { VListItem } from '../VListItem' import { VList } from '../VList' +function ListGroup (lazy: boolean, open: boolean): JSX.Element { + return ( + +

ListGroup

+ + + + {{ + activator: ({ props }) => , + default: () => ( + <> + + + + ), + }} + + +
+ ) +} + describe('VListGroup', () => { function mountFunction (content: JSX.Element) { return cy.mount(() => content) @@ -27,26 +49,24 @@ describe('VListGroup', () => { }) it('supports children', () => { - const wrapper = mountFunction(( - -

ListGroup

+ const wrapper = mountFunction(ListGroup(false, true)) + wrapper.get('.v-list-item-title').contains('Group') + }) - - - {{ - activator: props => , - default: () => ( - <> - - - - ), - }} - - -
- )) + describe('lazy', () => { + it('open on click', () => { + const wrapper = mountFunction(ListGroup(true, false)) - wrapper.get('.v-list-item-title').contains('Group') + const group = wrapper.get('.v-list-group') + group.get('.v-list-group__items .v-list-item').should('have.length', 0) + group.get('.v-list-item').click() + group.get('.v-list-group__items .v-list-item').should('have.length', 2) + }) + + it('already opened children', () => { + const wrapper = mountFunction(ListGroup(true, true)) + + wrapper.get('.v-list-group__items .v-list-item').should('have.length', 2) + }) }) })