Skip to content

Commit 411d937

Browse files
authored
feat(Tree): add item-wrapper slot (#4521)
1 parent 7fa3aca commit 411d937

File tree

5 files changed

+60
-27
lines changed

5 files changed

+60
-27
lines changed

docs/content/3.components/tree.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ Use the `slot` property to customize a specific item.
411411

412412
You will have access to the following slots:
413413

414+
- `#{{ item.slot }}-wrapper`{lang="ts-type"}
414415
- `#{{ item.slot }}`{lang="ts-type"}
415416
- `#{{ item.slot }}-leading`{lang="ts-type"}
416417
- `#{{ item.slot }}-label`{lang="ts-type"}

src/runtime/components/Tree.vue

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export type TreeSlots<
9090
A extends TreeItem[] = TreeItem[],
9191
T extends NestedItem<A> = NestedItem<A>
9292
> = {
93+
'item-wrapper': SlotProps<T>
9394
'item': SlotProps<T>
9495
'item-leading': SlotProps<T>
9596
'item-label': SlotProps<T>
@@ -163,35 +164,37 @@ const defaultExpanded = computed(() =>
163164
@toggle="item.onToggle"
164165
@select="item.onSelect"
165166
>
166-
<button type="button" :disabled="item.disabled || disabled" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], selected: isSelected, disabled: item.disabled || disabled })">
167-
<slot :name="((item.slot || 'item') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
168-
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
169-
<UIcon
170-
v-if="item.icon"
171-
:name="item.icon"
172-
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
173-
/>
174-
<UIcon
175-
v-else-if="item.children?.length"
176-
:name="isExpanded ? (expandedIcon ?? appConfig.ui.icons.folderOpen) : (collapsedIcon ?? appConfig.ui.icons.folder)"
177-
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
178-
/>
179-
</slot>
180-
181-
<span v-if="getItemLabel(item) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>]" :class="ui.linkLabel({ class: [props.ui?.linkLabel, item.ui?.linkLabel] })">
182-
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>)" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
183-
{{ getItemLabel(item) }}
167+
<slot :name="((item.slot ? `${item.slot}-wrapper` : 'item-wrapper') as keyof TreeSlots<T>)" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
168+
<button type="button" :disabled="item.disabled || disabled" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], selected: isSelected, disabled: item.disabled || disabled })">
169+
<slot :name="((item.slot || 'item') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
170+
<slot :name="((item.slot ? `${item.slot}-leading`: 'item-leading') as keyof TreeSlots<T>)" v-bind="{ index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
171+
<UIcon
172+
v-if="item.icon"
173+
:name="item.icon"
174+
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
175+
/>
176+
<UIcon
177+
v-else-if="item.children?.length"
178+
:name="isExpanded ? (expandedIcon ?? appConfig.ui.icons.folderOpen) : (collapsedIcon ?? appConfig.ui.icons.folder)"
179+
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
180+
/>
184181
</slot>
185-
</span>
186182

187-
<span v-if="item.trailingIcon || item.children?.length || !!slots[(item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>]" :class="ui.linkTrailing({ class: [props.ui?.linkTrailing, item.ui?.linkTrailing] })">
188-
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>)" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
189-
<UIcon v-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })" />
190-
<UIcon v-else-if="item.children?.length" :name="trailingIcon ?? appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })" />
191-
</slot>
192-
</span>
193-
</slot>
194-
</button>
183+
<span v-if="getItemLabel(item) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>]" :class="ui.linkLabel({ class: [props.ui?.linkLabel, item.ui?.linkLabel] })">
184+
<slot :name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>)" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
185+
{{ getItemLabel(item) }}
186+
</slot>
187+
</span>
188+
189+
<span v-if="item.trailingIcon || item.children?.length || !!slots[(item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>]" :class="ui.linkTrailing({ class: [props.ui?.linkTrailing, item.ui?.linkTrailing] })">
190+
<slot :name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>)" v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }" :item="(item as Extract<NestedItem<T>, { slot: string; }>)">
191+
<UIcon v-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })" />
192+
<UIcon v-else-if="item.children?.length" :name="trailingIcon ?? appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })" />
193+
</slot>
194+
</span>
195+
</slot>
196+
</button>
197+
</slot>
195198

196199
<ul v-if="item.children?.length && isExpanded" :class="ui.listWithChildren({ class: [props.ui?.listWithChildren, item.ui?.listWithChildren] })">
197200
<ReuseTreeTemplate :items="item.children" :level="level + 1" />

test/components/Tree.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe('Tree', () => {
6060
['with ui', { props: { ...props, ui: { link: 'font-bold' } } }],
6161
// Slots
6262
['with default slot', { props, slots: { default: () => 'default slot' } }],
63+
['with item-wrapper slot', { props, slots: { 'item-wrapper': () => 'wrapper slot' } }],
6364
['with item slot', { props, slots: { item: () => 'item slot' } }],
6465
['with item-leading slot', { props, slots: { 'item-leading': () => 'leading slot' } }],
6566
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'trailing slot' } }],

test/components/__snapshots__/Tree-vue.spec.ts.snap

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,20 @@ exports[`Tree > renders with item-trailing slot correctly 1`] = `
250250
</ul>"
251251
`;
252252

253+
exports[`Tree > renders with item-wrapper slot correctly 1`] = `
254+
"<ul class="relative isolate" tabindex="0" data-orientation="vertical" dir="ltr" style="outline-color: none; outline-style: none; outline-width: initial;" role="tree">
255+
<li class=""><button data-reka-collection-item="" tabindex="-1" data-orientation="vertical" role="treeitem" aria-selected="false" aria-expanded="false" aria-level="0" data-indent="0" type="button" class="relative group w-full flex items-center before:absolute before:inset-y-px before:inset-x-0 before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary px-2.5 py-1.5 text-sm gap-1.5 hover:not-disabled:text-highlighted hover:not-disabled:before:bg-elevated/50 transition-colors before:transition-colors"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" class="shrink-0 size-5"></svg><span class="truncate">app</span><span class="ms-auto inline-flex gap-1.5 items-center"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" class="shrink-0 transform transition-transform duration-200 group-data-expanded:rotate-180 size-5"></svg></span></button>
256+
<!--v-if-->
257+
</li>
258+
<li class="">wrapper slot
259+
<!--v-if-->
260+
</li>
261+
<li class="">wrapper slot
262+
<!--v-if-->
263+
</li>
264+
</ul>"
265+
`;
266+
253267
exports[`Tree > renders with items correctly 1`] = `
254268
"<ul class="relative isolate" tabindex="0" data-orientation="vertical" dir="ltr" style="outline-color: none; outline-style: none; outline-width: initial;" role="tree">
255269
<li class=""><button data-reka-collection-item="" tabindex="-1" data-orientation="vertical" role="treeitem" aria-selected="false" aria-expanded="false" aria-level="0" data-indent="0" type="button" class="relative group w-full flex items-center before:absolute before:inset-y-px before:inset-x-0 before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary px-2.5 py-1.5 text-sm gap-1.5 hover:not-disabled:text-highlighted hover:not-disabled:before:bg-elevated/50 transition-colors before:transition-colors"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" class="shrink-0 size-5"></svg><span class="truncate">app</span><span class="ms-auto inline-flex gap-1.5 items-center"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" class="shrink-0 transform transition-transform duration-200 group-data-expanded:rotate-180 size-5"></svg></span></button>

test/components/__snapshots__/Tree.spec.ts.snap

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,20 @@ exports[`Tree > renders with item-trailing slot correctly 1`] = `
250250
</ul>"
251251
`;
252252

253+
exports[`Tree > renders with item-wrapper slot correctly 1`] = `
254+
"<ul class="relative isolate" tabindex="0" data-orientation="vertical" dir="ltr" style="outline-color: none; outline-style: none; outline-width: initial;" role="tree">
255+
<li class=""><button data-reka-collection-item="" tabindex="-1" data-orientation="vertical" role="treeitem" aria-selected="false" aria-expanded="false" aria-level="0" data-indent="0" type="button" class="relative group w-full flex items-center before:absolute before:inset-y-px before:inset-x-0 before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary px-2.5 py-1.5 text-sm gap-1.5 hover:not-disabled:text-highlighted hover:not-disabled:before:bg-elevated/50 transition-colors before:transition-colors"><span class="iconify i-lucide:folder shrink-0 size-5" aria-hidden="true"></span><span class="truncate">app</span><span class="ms-auto inline-flex gap-1.5 items-center"><span class="iconify i-lucide:chevron-down shrink-0 transform transition-transform duration-200 group-data-expanded:rotate-180 size-5" aria-hidden="true"></span></span></button>
256+
<!--v-if-->
257+
</li>
258+
<li class="">wrapper slot
259+
<!--v-if-->
260+
</li>
261+
<li class="">wrapper slot
262+
<!--v-if-->
263+
</li>
264+
</ul>"
265+
`;
266+
253267
exports[`Tree > renders with items correctly 1`] = `
254268
"<ul class="relative isolate" tabindex="0" data-orientation="vertical" dir="ltr" style="outline-color: none; outline-style: none; outline-width: initial;" role="tree">
255269
<li class=""><button data-reka-collection-item="" tabindex="-1" data-orientation="vertical" role="treeitem" aria-selected="false" aria-expanded="false" aria-level="0" data-indent="0" type="button" class="relative group w-full flex items-center before:absolute before:inset-y-px before:inset-x-0 before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-primary px-2.5 py-1.5 text-sm gap-1.5 hover:not-disabled:text-highlighted hover:not-disabled:before:bg-elevated/50 transition-colors before:transition-colors"><span class="iconify i-lucide:folder shrink-0 size-5" aria-hidden="true"></span><span class="truncate">app</span><span class="ms-auto inline-flex gap-1.5 items-center"><span class="iconify i-lucide:chevron-down shrink-0 transform transition-transform duration-200 group-data-expanded:rotate-180 size-5" aria-hidden="true"></span></span></button>

0 commit comments

Comments
 (0)