diff --git a/packages/vuetify/src/components/VList/VList.tsx b/packages/vuetify/src/components/VList/VList.tsx index 567728ccd1c..00e020b2a94 100644 --- a/packages/vuetify/src/components/VList/VList.tsx +++ b/packages/vuetify/src/components/VList/VList.tsx @@ -22,7 +22,7 @@ import { makeVariantProps } from '@/composables/variant' // Utilities import { computed, ref, shallowRef, toRef } from 'vue' -import { focusChild, genericComponent, getPropertyFromItem, omit, propsFactory, useRender } from '@/util' +import { EventProp, focusChild, genericComponent, getPropertyFromItem, omit, propsFactory, useRender } from '@/util' // Types import type { PropType } from 'vue' @@ -95,6 +95,8 @@ export const makeVListProps = propsFactory({ slim: Boolean, nav: Boolean, + 'onClick:open': EventProp<[{ id: unknown, value: boolean, path: unknown[] }]>(), + 'onClick:select': EventProp<[{ id: unknown, value: boolean, path: unknown[] }]>(), ...makeNestedProps({ selectStrategy: 'single-leaf' as const, openStrategy: 'list' as const, @@ -130,6 +132,8 @@ export const VList = genericComponent> selected?: S 'onUpdate:selected'?: (value: S) => void + 'onClick:open'?: (value: { id: unknown, value: boolean, path: unknown[] }) => void + 'onClick:select'?: (value: { id: unknown, value: boolean, path: unknown[] }) => void opened?: O 'onUpdate:opened'?: (value: O) => void }, diff --git a/packages/vuetify/src/components/VList/VListGroup.tsx b/packages/vuetify/src/components/VList/VListGroup.tsx index a475a86d330..2d94ce78e31 100644 --- a/packages/vuetify/src/components/VList/VListGroup.tsx +++ b/packages/vuetify/src/components/VList/VListGroup.tsx @@ -66,6 +66,7 @@ export const VListGroup = genericComponent()({ const { isBooted } = useSsrBoot() function onClick (e: Event) { + e.stopPropagation() open(!isOpen.value, e) } diff --git a/packages/vuetify/src/components/VList/VListItem.sass b/packages/vuetify/src/components/VList/VListItem.sass index cfa039d1685..d1146c26a47 100644 --- a/packages/vuetify/src/components/VList/VListItem.sass +++ b/packages/vuetify/src/components/VList/VListItem.sass @@ -319,7 +319,7 @@ .v-list-group__items .v-list-item padding-inline-start: calc(#{$base-padding} + var(--indent-padding)) !important - .v-list-group__header.v-list-item--active + .v-list-group__header:not(.v-treeview-item--activetable-group-activator).v-list-item--active &:not(:focus-visible) .v-list-item__overlay opacity: 0 diff --git a/packages/vuetify/src/components/VList/VListItem.tsx b/packages/vuetify/src/components/VList/VListItem.tsx index ec51bda0e14..5033f7954e1 100644 --- a/packages/vuetify/src/components/VList/VListItem.tsx +++ b/packages/vuetify/src/components/VList/VListItem.tsx @@ -34,7 +34,7 @@ import { deprecate, EventProp, genericComponent, propsFactory, useRender } from import type { PropType } from 'vue' import type { RippleDirectiveBinding } from '@/directives/ripple' -type ListItemSlot = { +export type ListItemSlot = { isActive: boolean isSelected: boolean isIndeterminate: boolean @@ -359,6 +359,8 @@ export const VListItem = genericComponent()({ }) return { + activate, + isActivated, isGroupActivator, isSelected, list, diff --git a/packages/vuetify/src/labs/VTreeview/VTreeview.tsx b/packages/vuetify/src/labs/VTreeview/VTreeview.tsx index 693d45e41aa..daaa14820f2 100644 --- a/packages/vuetify/src/labs/VTreeview/VTreeview.tsx +++ b/packages/vuetify/src/labs/VTreeview/VTreeview.tsx @@ -34,7 +34,7 @@ export const makeVTreeviewProps = propsFactory({ ...omit(makeVListProps({ collapseIcon: '$treeviewCollapse', expandIcon: '$treeviewExpand', - selectStrategy: 'independent' as const, + selectStrategy: 'classic' as const, openStrategy: 'multiple' as const, slim: true, }), ['nav']), diff --git a/packages/vuetify/src/labs/VTreeview/VTreeviewChildren.tsx b/packages/vuetify/src/labs/VTreeview/VTreeviewChildren.tsx index 85bf5e3c4d1..17d40421270 100644 --- a/packages/vuetify/src/labs/VTreeview/VTreeviewChildren.tsx +++ b/packages/vuetify/src/labs/VTreeview/VTreeviewChildren.tsx @@ -4,13 +4,14 @@ import { VTreeviewItem } from './VTreeviewItem' import { VCheckboxBtn } from '@/components/VCheckbox' // Utilities -import { shallowRef } from 'vue' +import { shallowRef, withModifiers } from 'vue' import { genericComponent, propsFactory } from '@/util' // Types import type { PropType } from 'vue' import type { InternalListItem } from '@/components/VList/VList' import type { VListItemSlots } from '@/components/VList/VListItem' +import type { SelectStrategyProp } from '@/composables/nested/nested' import type { GenericProps } from '@/util' export type VTreeviewChildrenSlots = { @@ -28,6 +29,7 @@ export const makeVTreeviewChildrenProps = propsFactory({ }, items: Array as PropType, selectable: Boolean, + selectStrategy: [String, Function, Object] as PropType, }, 'VTreeviewChildren') export const VTreeviewChildren = genericComponent( @@ -60,29 +62,37 @@ export const VTreeviewChildren = genericComponent void, isSelected: boolean) { + if (props.selectable) { + select(!isSelected) + } } return () => slots.default?.() ?? props.items?.map(({ children, props: itemProps, raw: item }) => { const loading = isLoading.value === item.value const slotsWithItem = { - prepend: slots.prepend - ? slotProps => slots.prepend?.({ ...slotProps, item }) - : props.selectable - ? ({ isSelected, isIndeterminate }) => ( - onClick(e, item) } - /> - ) - : undefined, + prepend: slotProps => ( + <> + { props.selectable && (!children || (children && !['leaf', 'single-leaf'].includes(props.selectStrategy as string))) && ( +
+ selectItem(slotProps.select, slotProps.isSelected), ['stop']) } + onKeydown={ (e: KeyboardEvent) => { + if (!['Enter', 'Space'].includes(e.key)) return + e.stopPropagation() + selectItem(slotProps.select, slotProps.isSelected) + }} + /> +
+ )} + + { slots.prepend?.({ ...slotProps, item }) } + + ), append: slots.append ? slotProps => slots.append?.({ ...slotProps, item }) : undefined, title: slots.title ? slotProps => slots.title?.({ ...slotProps, item }) : undefined, } satisfies VTreeviewItem['$props']['$children'] @@ -96,15 +106,22 @@ export const VTreeviewChildren = genericComponent {{ - activator: ({ props: activatorProps }) => ( - onClick(e, item) } - /> - ), + activator: ({ props: activatorProps }) => { + const listItemProps = { + ...itemProps, + ...activatorProps, + value: itemProps?.value, + } + + return ( + checkChildren(item) } + /> + ) + }, default: () => ( ()({ setup (props, { attrs, slots, emit }) { const link = useLink(props, attrs) - const id = computed(() => props.value === undefined ? link.href.value : props.value) + const rawId = computed(() => props.value === undefined ? link.href.value : props.value) const vListItemRef = ref() + const { + activate, + isActivated, + select, + isSelected, + isIndeterminate, + isGroupActivator, + root, + id, + } = useNestedItem(rawId, false) + + const isActivatableGroupActivator = computed(() => + (root.activatable.value) && + isGroupActivator + ) + + const { densityClasses } = useDensity(props, 'v-list-item') + + const slotProps = computed(() => ({ + isActive: isActivated.value, + select, + isSelected: isSelected.value, + isIndeterminate: isIndeterminate.value, + } satisfies ListItemSlot)) + const isClickable = computed(() => !props.disabled && props.link !== false && (props.link || link.isClickable.value || (props.value != null && !!vListItemRef.value?.list)) ) - function onClick (e: MouseEvent | KeyboardEvent) { - if (!vListItemRef.value?.isGroupActivator || !isClickable.value) return - props.value != null && vListItemRef.value?.select(!vListItemRef.value?.isSelected, e) + function activateItem (e: MouseEvent | KeyboardEvent) { + if ( + !isClickable.value || + (!isActivatableGroupActivator.value && isGroupActivator) + ) return + + if (root.activatable.value) { + if (isActivatableGroupActivator.value) { + activate(!isActivated.value, e) + } else { + vListItemRef.value?.activate(!vListItemRef.value?.isActivated, e) + } + } } function onKeyDown (e: KeyboardEvent) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() - onClick(e as any as MouseEvent) + activateItem(e) } } const visibleIds = inject(VTreeviewSymbol, { visibleIds: ref() }).visibleIds useRender(() => { + const hasTitle = (slots.title || props.title != null) + const hasSubtitle = (slots.subtitle || props.subtitle != null) const listItemProps = VListItem.filterProps(props) const hasPrepend = slots.prepend || props.toggleIcon - return ( + return isActivatableGroupActivator.value + ? ( +
+ <> + { genOverlays(isActivated.value || isSelected.value, 'v-list-item') } + { props.toggleIcon && ( + + + {{ + loader () { + return ( + + ) + }, + }} + + + )} + + + +
+ { hasTitle && ( + + { slots.title?.({ title: props.title }) ?? props.title } + + )} + + { hasSubtitle && ( + + { slots.subtitle?.({ subtitle: props.subtitle }) ?? props.subtitle } + + )} + + { slots.default?.(slotProps.value) } +
+
+ ) : ( ()({ }, props.class, ]} - onClick={ onClick } + value={ id.value } + onClick={ activateItem } onKeydown={ isClickable.value && onKeyDown } > {{ @@ -108,7 +211,7 @@ export const VTreeviewItem = genericComponent()({ } : undefined, }} - ) + ) }) return {}