Skip to content

Commit 7ef555b

Browse files
authored
feat(Tree): add disabled for TreeItem (#2529)
1 parent a8314fe commit 7ef555b

4 files changed

Lines changed: 106 additions & 2 deletions

File tree

docs/content/meta/TreeItem.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
'type': 'boolean',
1616
'required': false
1717
},
18+
{
19+
'name': 'disabled',
20+
'description': '<p>When <code>true</code>, prevents the user from interacting with the item.</p>\n',
21+
'type': 'boolean',
22+
'required': false
23+
},
1824
{
1925
'name': 'level',
2026
'description': '<p>Level of depth</p>\n',
@@ -58,6 +64,11 @@
5864
'description': '',
5965
'type': 'boolean | undefined'
6066
},
67+
{
68+
'name': 'isDisabled',
69+
'description': '',
70+
'type': 'boolean'
71+
},
6172
{
6273
'name': 'handleToggle',
6374
'description': '',

packages/core/src/Tree/Tree.test.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { DOMWrapper, VueWrapper } from '@vue/test-utils'
22
import { mount } from '@vue/test-utils'
33
import { beforeEach, describe, expect, it } from 'vitest'
44
import { axe } from 'vitest-axe'
5-
import { nextTick } from 'vue'
5+
import { defineComponent, h, nextTick } from 'vue'
66
import { useKbd } from '@/shared'
7+
import { TreeItem, TreeRoot } from '.'
78
import Tree from './story/_Tree.vue'
89

910
const kbd = useKbd()
@@ -333,3 +334,75 @@ describe('given a Tree with bubbleSelect and propagateSelect', () => {
333334
expect(items.map(i => i.attributes('aria-selected'))).toStrictEqual(['false', 'false', 'false', 'false'])
334335
})
335336
})
337+
338+
describe('given a Tree with disabled items', () => {
339+
const treeItems = [
340+
{ title: 'apple' },
341+
{ title: 'banana' },
342+
{ title: 'cherry' },
343+
{ title: 'fruits', children: [{ title: 'grape' }, { title: 'kiwi' }] },
344+
]
345+
346+
function mountTree(disabledKeys: string[] = ['banana'], rootDisabled = false) {
347+
const wrapper = mount(defineComponent({
348+
setup() {
349+
return () => h(TreeRoot as any, {
350+
items: treeItems,
351+
getKey: (item: any) => item.title,
352+
selectionBehavior: 'toggle',
353+
disabled: rootDisabled,
354+
expanded: ['fruits'],
355+
}, {
356+
default: ({ flattenItems }: any) => flattenItems.map((item: any) =>
357+
h(TreeItem as any, {
358+
key: item._id,
359+
...item.bind,
360+
disabled: disabledKeys.includes(item._id),
361+
}, { default: () => item.value.title }),
362+
),
363+
})
364+
},
365+
}), { attachTo: document.body })
366+
return { wrapper, items: () => wrapper.findAll('[role=treeitem]') }
367+
}
368+
369+
beforeEach(() => {
370+
document.body.innerHTML = ''
371+
})
372+
373+
it('should set aria-disabled and data-disabled attributes', () => {
374+
const { items } = mountTree()
375+
expect(items()[1].attributes('aria-disabled')).toBe('true')
376+
expect(items()[1].attributes('data-disabled')).toBe('')
377+
expect(items()[0].attributes('aria-disabled')).toBeUndefined()
378+
expect(items()[0].attributes('data-disabled')).toBeUndefined()
379+
})
380+
381+
it('should not select a disabled item on click or keydown', async () => {
382+
const { items } = mountTree()
383+
await items()[1].trigger('click')
384+
expect(items()[1].attributes('aria-selected')).toBe('false')
385+
await items()[1].trigger('keydown', { key: kbd.ENTER })
386+
expect(items()[1].attributes('aria-selected')).toBe('false')
387+
})
388+
389+
it('should not toggle a disabled item', async () => {
390+
const { items } = mountTree(['fruits'])
391+
expect(items().length).toBe(6)
392+
393+
await items()[3].trigger('click')
394+
expect(items().length).toBe(6)
395+
396+
await items()[3].trigger('keydown', { key: kbd.ARROW_LEFT })
397+
expect(items().length).toBe(6)
398+
})
399+
400+
it('should disable all items when root is disabled', async () => {
401+
const { items } = mountTree([], true)
402+
for (const item of items()) {
403+
expect(item.attributes('aria-disabled')).toBe('true')
404+
}
405+
await items()[0].trigger('click')
406+
expect(items()[0].attributes('aria-selected')).toBe('false')
407+
})
408+
})

packages/core/src/Tree/TreeItem.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export interface TreeItemProps<T> extends PrimitiveProps {
44
value: T
55
/** Level of depth */
66
level: number
7+
/** When `true`, prevents the user from interacting with the item. */
8+
disabled?: boolean
79
}
810
911
export type SelectEvent<T> = CustomEvent<{ originalEvent: PointerEvent | KeyboardEvent, value?: T, isExpanded: boolean, isSelected: boolean }>
@@ -45,6 +47,7 @@ defineSlots<{
4547
isExpanded: boolean
4648
isSelected: boolean
4749
isIndeterminate: boolean | undefined
50+
isDisabled: boolean
4851
handleToggle: () => void
4952
handleSelect: () => void
5053
}) => any
@@ -81,7 +84,11 @@ const isIndeterminate = computed(() => {
8184
}
8285
})
8386
87+
const isDisabled = computed(() => rootContext.disabled.value || props.disabled)
88+
8489
function handleKeydownRight(ev: KeyboardEvent) {
90+
if (isDisabled.value)
91+
return
8592
if (!hasChildren.value)
8693
return
8794
@@ -103,6 +110,8 @@ function handleKeydownRight(ev: KeyboardEvent) {
103110
}
104111
105112
function handleKeydownLeft(ev: KeyboardEvent) {
113+
if (isDisabled.value)
114+
return
106115
if (isExpanded.value) {
107116
// close expanded
108117
handleToggleCustomEvent(ev)
@@ -121,13 +130,17 @@ function handleKeydownLeft(ev: KeyboardEvent) {
121130
}
122131
123132
async function handleSelect(ev: SelectEvent<T>) {
133+
if (isDisabled.value)
134+
return
124135
emits('select', ev)
125136
if (ev?.defaultPrevented)
126137
return
127138
128139
rootContext.onSelect(props.value)
129140
}
130141
async function handleToggle(ev: ToggleEvent<T>) {
142+
if (isDisabled.value)
143+
return
131144
emits('toggle', ev)
132145
if (ev?.defaultPrevented)
133146
return
@@ -155,6 +168,7 @@ defineExpose({
155168
isExpanded,
156169
isSelected,
157170
isIndeterminate,
171+
isDisabled,
158172
handleToggle: () => rootContext.onToggle(props.value),
159173
handleSelect: () => rootContext.onSelect(props.value),
160174
})
@@ -165,6 +179,7 @@ defineExpose({
165179
as-child
166180
:value="value"
167181
allow-shift-key
182+
:focusable="!isDisabled"
168183
>
169184
<Primitive
170185
v-bind="$attrs"
@@ -174,9 +189,11 @@ defineExpose({
174189
:aria-selected="isSelected"
175190
:aria-expanded="hasChildren ? isExpanded : undefined"
176191
:aria-level="level"
192+
:aria-disabled="isDisabled ? true : undefined"
177193
:data-indent="level"
178194
:data-selected="isSelected ? '' : undefined"
179195
:data-expanded="isExpanded ? '' : undefined"
196+
:data-disabled="isDisabled ? '' : undefined"
180197
@keydown.enter.space.self.prevent="handleSelectCustomEvent"
181198
@keydown.right.prevent="(ev: KeyboardEvent) => rootContext.dir.value === 'ltr' ? handleKeydownRight(ev) : handleKeydownLeft(ev)"
182199
@keydown.left.prevent="(ev: KeyboardEvent) => rootContext.dir.value === 'ltr' ? handleKeydownLeft(ev) : handleKeydownRight(ev)"
@@ -189,6 +206,7 @@ defineExpose({
189206
:is-expanded="isExpanded"
190207
:is-selected="isSelected"
191208
:is-indeterminate="isIndeterminate"
209+
:is-disabled="isDisabled"
192210
:handle-select="() => rootContext.onSelect(value)"
193211
:handle-toggle="() => rootContext.onToggle(value)"
194212
/>

packages/core/src/Tree/TreeVirtualizer.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ rootContext.virtualKeydownHook.on((event) => {
133133
const index = intent === 'first' ? 0 : rootContext.expandedItems.value.length - 1
134134
virtualizer.value.scrollToIndex(index)
135135
requestAnimationFrame(() => {
136-
const items = getItems()
136+
const items = getItems().filter(i => i.ref.dataset.disabled !== '')
137+
if (!items.length)
138+
return
137139
const item = intent === 'first' ? items[0] : items.at(-1)!
138140
item.ref.focus()
139141
})

0 commit comments

Comments
 (0)