Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/content/3.components/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ Use the `slot` property to customize a specific item.

You will have access to the following slots:

- `#{{ item.slot }}-wrapper`{lang="ts-type"}
- `#{{ item.slot }}`{lang="ts-type"}
- `#{{ item.slot }}-leading`{lang="ts-type"}
- `#{{ item.slot }}-label`{lang="ts-type"}
Expand Down
57 changes: 30 additions & 27 deletions src/runtime/components/Tree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export type TreeSlots<
A extends TreeItem[] = TreeItem[],
T extends NestedItem<A> = NestedItem<A>
> = {
'item-wrapper': SlotProps<T>
'item': SlotProps<T>
'item-leading': SlotProps<T>
'item-label': SlotProps<T>
Expand Down Expand Up @@ -163,35 +164,37 @@ const defaultExpanded = computed(() =>
@toggle="item.onToggle"
@select="item.onSelect"
>
<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 })">
<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; }>)">
<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; }>)">
<UIcon
v-if="item.icon"
:name="item.icon"
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
/>
<UIcon
v-else-if="item.children?.length"
:name="isExpanded ? (expandedIcon ?? appConfig.ui.icons.folderOpen) : (collapsedIcon ?? appConfig.ui.icons.folder)"
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
/>
</slot>

<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] })">
<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; }>)">
{{ getItemLabel(item) }}
<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; }>)">
<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 })">
<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; }>)">
<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; }>)">
<UIcon
v-if="item.icon"
:name="item.icon"
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
/>
<UIcon
v-else-if="item.children?.length"
:name="isExpanded ? (expandedIcon ?? appConfig.ui.icons.folderOpen) : (collapsedIcon ?? appConfig.ui.icons.folder)"
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
/>
</slot>
</span>

<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] })">
<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; }>)">
<UIcon v-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })" />
<UIcon v-else-if="item.children?.length" :name="trailingIcon ?? appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })" />
</slot>
</span>
</slot>
</button>
<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] })">
<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; }>)">
{{ getItemLabel(item) }}
</slot>
</span>

<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] })">
<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; }>)">
<UIcon v-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })" />
<UIcon v-else-if="item.children?.length" :name="trailingIcon ?? appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })" />
</slot>
</span>
</slot>
</button>
</slot>

<ul v-if="item.children?.length && isExpanded" :class="ui.listWithChildren({ class: [props.ui?.listWithChildren, item.ui?.listWithChildren] })">
<ReuseTreeTemplate :items="item.children" :level="level + 1" />
Expand Down
1 change: 1 addition & 0 deletions test/components/Tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ describe('Tree', () => {
['with ui', { props: { ...props, ui: { link: 'font-bold' } } }],
// Slots
['with default slot', { props, slots: { default: () => 'default slot' } }],
['with item-wrapper slot', { props, slots: { 'item-wrapper': () => 'wrapper slot' } }],
['with item slot', { props, slots: { item: () => 'item slot' } }],
['with item-leading slot', { props, slots: { 'item-leading': () => 'leading slot' } }],
['with item-trailing slot', { props, slots: { 'item-trailing': () => 'trailing slot' } }],
Expand Down
14 changes: 14 additions & 0 deletions test/components/__snapshots__/Tree-vue.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,20 @@ exports[`Tree > renders with item-trailing slot correctly 1`] = `
</ul>"
`;

exports[`Tree > renders with item-wrapper slot correctly 1`] = `
"<ul class="relative isolate" tabindex="0" data-orientation="vertical" dir="ltr" style="outline-color: none; outline-style: none; outline-width: initial;" role="tree">
<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>
<!--v-if-->
</li>
<li class="">wrapper slot
<!--v-if-->
</li>
<li class="">wrapper slot
<!--v-if-->
</li>
</ul>"
`;

exports[`Tree > renders with items correctly 1`] = `
"<ul class="relative isolate" tabindex="0" data-orientation="vertical" dir="ltr" style="outline-color: none; outline-style: none; outline-width: initial;" role="tree">
<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>
Expand Down
14 changes: 14 additions & 0 deletions test/components/__snapshots__/Tree.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,20 @@ exports[`Tree > renders with item-trailing slot correctly 1`] = `
</ul>"
`;

exports[`Tree > renders with item-wrapper slot correctly 1`] = `
"<ul class="relative isolate" tabindex="0" data-orientation="vertical" dir="ltr" style="outline-color: none; outline-style: none; outline-width: initial;" role="tree">
<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>
<!--v-if-->
</li>
<li class="">wrapper slot
<!--v-if-->
</li>
<li class="">wrapper slot
<!--v-if-->
</li>
</ul>"
`;

exports[`Tree > renders with items correctly 1`] = `
"<ul class="relative isolate" tabindex="0" data-orientation="vertical" dir="ltr" style="outline-color: none; outline-style: none; outline-width: initial;" role="tree">
<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>
Expand Down