Skip to content

Commit

Permalink
fix(VTreeview): select & activate issues (#19795)
Browse files Browse the repository at this point in the history
fixes #19441
fixes #19402
fixes #19400
fixes #19533
fixes #19471

Co-authored-by: Kael <kaelwd@gmail.com>
Co-authored-by: John Leider <john@vuetifyjs.com>
  • Loading branch information
3 people authored May 29, 2024
1 parent f5f2fa4 commit 8d7beeb
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 42 deletions.
6 changes: 5 additions & 1 deletion packages/vuetify/src/components/VList/VList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -130,6 +132,8 @@ export const VList = genericComponent<new <
itemProps?: SelectItemKey<ItemType<T>>
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
},
Expand Down
1 change: 1 addition & 0 deletions packages/vuetify/src/components/VList/VListGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const VListGroup = genericComponent<VListGroupSlots>()({
const { isBooted } = useSsrBoot()

function onClick (e: Event) {
e.stopPropagation()
open(!isOpen.value, e)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/vuetify/src/components/VList/VListItem.sass
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/vuetify/src/components/VList/VListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -359,6 +359,8 @@ export const VListItem = genericComponent<VListItemSlots>()({
})

return {
activate,
isActivated,
isGroupActivator,
isSelected,
list,
Expand Down
2 changes: 1 addition & 1 deletion packages/vuetify/src/labs/VTreeview/VTreeview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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']),
Expand Down
73 changes: 45 additions & 28 deletions packages/vuetify/src/labs/VTreeview/VTreeviewChildren.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = {
Expand All @@ -28,6 +29,7 @@ export const makeVTreeviewChildrenProps = propsFactory({
},
items: Array as PropType<readonly InternalListItem[]>,
selectable: Boolean,
selectStrategy: [String, Function, Object] as PropType<SelectStrategyProp>,
}, 'VTreeviewChildren')

export const VTreeviewChildren = genericComponent<new <T extends InternalListItem>(
Expand Down Expand Up @@ -60,29 +62,37 @@ export const VTreeviewChildren = genericComponent<new <T extends InternalListIte
})
}

function onClick (e: MouseEvent | KeyboardEvent, item: any) {
e.stopPropagation()

checkChildren(item)
function selectItem (select: (value: boolean) => 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 }) => (
<VCheckboxBtn
key={ item.value }
tabindex="-1"
modelValue={ isSelected }
loading={ loading }
indeterminate={ isIndeterminate }
onClick={ (e: MouseEvent) => onClick(e, item) }
/>
)
: undefined,
prepend: slotProps => (
<>
{ props.selectable && (!children || (children && !['leaf', 'single-leaf'].includes(props.selectStrategy as string))) && (
<div>
<VCheckboxBtn
key={ item.value }
modelValue={ slotProps.isSelected }
loading={ loading }
indeterminate={ slotProps.isIndeterminate }
onClick={ withModifiers(() => selectItem(slotProps.select, slotProps.isSelected), ['stop']) }
onKeydown={ (e: KeyboardEvent) => {
if (!['Enter', 'Space'].includes(e.key)) return
e.stopPropagation()
selectItem(slotProps.select, slotProps.isSelected)
}}
/>
</div>
)}

{ 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']
Expand All @@ -96,15 +106,22 @@ export const VTreeviewChildren = genericComponent<new <T extends InternalListIte
{ ...treeviewGroupProps }
>
{{
activator: ({ props: activatorProps }) => (
<VTreeviewItem
{ ...itemProps }
{ ...activatorProps }
loading={ loading }
v-slots={ slotsWithItem }
onClick={ (e: MouseEvent | KeyboardEvent) => onClick(e, item) }
/>
),
activator: ({ props: activatorProps }) => {
const listItemProps = {
...itemProps,
...activatorProps,
value: itemProps?.value,
}

return (
<VTreeviewItem
{ ...listItemProps }
loading={ loading }
v-slots={ slotsWithItem }
onClick={ () => checkChildren(item) }
/>
)
},
default: () => (
<VTreeviewChildren
{ ...treeviewChildrenProps }
Expand Down
123 changes: 113 additions & 10 deletions packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@ import './VTreeviewItem.sass'

// Components
import { VBtn } from '@/components/VBtn'
import { VListItemAction } from '@/components/VList'
import { VListItemAction, VListItemSubtitle, VListItemTitle } from '@/components/VList'
import { makeVListItemProps, VListItem } from '@/components/VList/VListItem'
import { VProgressCircular } from '@/components/VProgressCircular'

// Composables
import { useDensity } from '@/composables/density'
import { IconValue } from '@/composables/icons'
import { useNestedItem } from '@/composables/nested/nested'
import { useLink } from '@/composables/router'
import { genOverlays } from '@/composables/variant'

// Utilities
import { computed, inject, ref } from 'vue'
import { genericComponent, propsFactory, useRender } from '@/util'

// Types
import { VTreeviewSymbol } from './shared'
import type { VListItemSlots } from '@/components/VList/VListItem'
import type { ListItemSlot, VListItemSlots } from '@/components/VList/VListItem'

export const makeVTreeviewItemProps = propsFactory({
loading: Boolean,
Expand All @@ -33,34 +36,133 @@ export const VTreeviewItem = genericComponent<VListItemSlots>()({

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<VListItem>()

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
? (
<div
class={[
'v-list-item',
'v-list-item--one-line',
'v-treeview-item',
'v-treeview-item--activetable-group-activator',
{
'v-list-item--active': isActivated.value || isSelected.value,
'v-treeview-item--filtered': visibleIds.value && !visibleIds.value.has(id.value),
},
densityClasses.value,
props.class,
]}
onClick={ activateItem }
v-ripple={ isClickable.value && props.ripple }
>
<>
{ genOverlays(isActivated.value || isSelected.value, 'v-list-item') }
{ props.toggleIcon && (
<VListItemAction start={ false }>
<VBtn
density="compact"
icon={ props.toggleIcon }
loading={ props.loading }
variant="text"
onClick={ props.onClick }
>
{{
loader () {
return (
<VProgressCircular
indeterminate="disable-shrink"
size="20"
width="2"
/>
)
},
}}
</VBtn>
</VListItemAction>
)}

</>

<div class="v-list-item__content" data-no-activator="">
{ hasTitle && (
<VListItemTitle key="title">
{ slots.title?.({ title: props.title }) ?? props.title }
</VListItemTitle>
)}

{ hasSubtitle && (
<VListItemSubtitle key="subtitle">
{ slots.subtitle?.({ subtitle: props.subtitle }) ?? props.subtitle }
</VListItemSubtitle>
)}

{ slots.default?.(slotProps.value) }
</div>
</div>
) : (
<VListItem
ref={ vListItemRef }
{ ...listItemProps }
Expand All @@ -71,7 +173,8 @@ export const VTreeviewItem = genericComponent<VListItemSlots>()({
},
props.class,
]}
onClick={ onClick }
value={ id.value }
onClick={ activateItem }
onKeydown={ isClickable.value && onKeyDown }
>
{{
Expand Down Expand Up @@ -108,7 +211,7 @@ export const VTreeviewItem = genericComponent<VListItemSlots>()({
} : undefined,
}}
</VListItem>
)
)
})

return {}
Expand Down

0 comments on commit 8d7beeb

Please sign in to comment.