Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(VTreeview): select & activate issues #19795

Merged
merged 26 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b49a70b
feat(VList): allow group activator to be activated without open children
yuwu9145 May 10, 2024
d13e68f
refactor: rollback groupActivatorActivatable & move activate group ac…
yuwu9145 May 11, 2024
cae4b0b
refactor: remove groupActivatorActivatable from VList
yuwu9145 May 11, 2024
a911942
chore: introduce open-on-click prop & improve logic
yuwu9145 May 11, 2024
bf1dd2b
chore: achieve selectable same as activable
yuwu9145 May 12, 2024
7f3811a
chore: fix lint error & rollback redundant changes
yuwu9145 May 12, 2024
52385b1
chore: fix known issue & improve
yuwu9145 May 12, 2024
b09290c
chore: remove selectable from the concern of this PR
yuwu9145 May 12, 2024
c7d296d
fix: selectable is initially working
yuwu9145 May 17, 2024
91a763f
fix: classic select strategy works
yuwu9145 May 17, 2024
673f6cb
fix: selectable is fully working
yuwu9145 May 17, 2024
1cda184
chore: remove open-on-click logic
yuwu9145 May 17, 2024
a94a4ad
Merge branch 'master' into fix-19441
yuwu9145 May 17, 2024
893db03
chore: load children when click group activator
yuwu9145 May 17, 2024
83b1643
chore(VTreeviewChildren): allow prepend slot to display infront of se…
yuwu9145 May 17, 2024
9316bcd
chore: improve naming & typing
yuwu9145 May 17, 2024
a8914f9
fix: move prepend slot after tree view item checkbox
yuwu9145 May 17, 2024
0592d49
fix: fix failed cy test
yuwu9145 May 17, 2024
d4c9845
chore: remove nestedId
yuwu9145 May 21, 2024
6dc6838
Merge branch 'master' into fix-19441
KaelWD May 21, 2024
827a74e
chore(VTreeviewChildren): code refactor
johnleider May 21, 2024
13a9141
chore(VTreeviewItem): change variable name
johnleider May 21, 2024
69f9fbc
fix: support onClick:open & onClick:select
yuwu9145 May 22, 2024
ff1e855
fix: keyboard navigation when using selectable group activator
yuwu9145 May 22, 2024
0709cb7
Merge branch 'master' into fix-19441
yuwu9145 May 23, 2024
ab96106
fix: prevent clicking open button activate the item
yuwu9145 May 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('VListGroup', () => {

<VList>
<VListGroup>
{{ activator: props => <VListItem { ...props } title="Group" /> }}
{{ activator: props => <VListItem isOpen={ props.isOpen } value={ props.nestedId } title="Group" /> }}
</VListGroup>
</VList>
</CenteredGrid>
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
69 changes: 41 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,33 @@ 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 }
tabindex="-1"
yuwu9145 marked this conversation as resolved.
Show resolved Hide resolved
modelValue={ slotProps.isSelected }
loading={ loading }
indeterminate={ slotProps.isIndeterminate }
onClick={ withModifiers(() => selectItem(slotProps.select, slotProps.isSelected), ['stop']) }
/>
</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 +102,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,12 +3,15 @@ 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'

// 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'
Expand All @@ -17,7 +20,7 @@ import { genericComponent, propsFactory, useRender } from '@/util'
// Types
import { VTreeviewSymbol } from './shared'
import { VProgressCircular } from '../allComponents'
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
Loading