Skip to content

Commit 117b4b3

Browse files
fix(CodeTree/Tree): improve accessibility (#4945)
1 parent 4809c43 commit 117b4b3

File tree

6 files changed

+295
-260
lines changed

6 files changed

+295
-260
lines changed

src/runtime/components/Tree.vue

Lines changed: 69 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -153,56 +153,84 @@ const defaultExpanded = computed(() =>
153153
<!-- eslint-disable vue/no-template-shadow -->
154154
<template>
155155
<DefineTreeTemplate v-slot="{ items, level }">
156-
<li
156+
<TreeItem
157157
v-for="(item, index) in items"
158158
:key="`${level}-${index}`"
159-
:class="level > 0 ? ui.itemWithChildren({ class: [props.ui?.itemWithChildren, item.ui?.itemWithChildren] }) : ui.item({ class: [props.ui?.item, item.ui?.item] })"
159+
v-slot="{ isExpanded, isSelected }"
160+
:level="level"
161+
:value="item"
162+
:class="level > 1 ? ui.itemWithChildren({ class: [props.ui?.itemWithChildren, item.ui?.itemWithChildren] }) : ui.item({ class: [props.ui?.item, item.ui?.item] })"
163+
@toggle="item.onToggle"
164+
@select="item.onSelect"
160165
>
161-
<TreeItem
162-
v-slot="{ isExpanded, isSelected }"
163-
as-child
164-
:level="level"
165-
:value="item"
166-
@toggle="item.onToggle"
167-
@select="item.onSelect"
166+
<slot
167+
:name="((item.slot ? `${item.slot}-wrapper` : 'item-wrapper') as keyof TreeSlots<T>)"
168+
v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }"
168169
>
169-
<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; }>)">
170-
<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 })">
171-
<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; }>)">
172-
<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; }>)">
170+
<button
171+
type="button"
172+
:disabled="item.disabled || disabled"
173+
:data-expanded="isExpanded"
174+
:class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], selected: isSelected, disabled: item.disabled || disabled })"
175+
>
176+
<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; }>)">
177+
<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; }>)">
178+
<UIcon
179+
v-if="item.icon"
180+
:name="item.icon"
181+
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
182+
/>
183+
<UIcon
184+
v-else-if="item.children?.length"
185+
:name="isExpanded ? (expandedIcon ?? appConfig.ui.icons.folderOpen) : (collapsedIcon ?? appConfig.ui.icons.folder)"
186+
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
187+
/>
188+
</slot>
189+
190+
<span
191+
v-if="getItemLabel(item) || !!slots[(item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>]"
192+
:class="ui.linkLabel({ class: [props.ui?.linkLabel, item.ui?.linkLabel] })"
193+
>
194+
<slot
195+
:name="((item.slot ? `${item.slot}-label`: 'item-label') as keyof TreeSlots<T>)"
196+
v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }"
197+
>
198+
{{ getItemLabel(item) }}
199+
</slot>
200+
</span>
201+
202+
<span
203+
v-if="item.trailingIcon || item.children?.length || !!slots[(item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>]"
204+
:class="ui.linkTrailing({ class: [props.ui?.linkTrailing, item.ui?.linkTrailing] })"
205+
>
206+
<slot
207+
:name="((item.slot ? `${item.slot}-trailing`: 'item-trailing') as keyof TreeSlots<T>)"
208+
v-bind="{ item, index, level, expanded: isExpanded, selected: isSelected }"
209+
>
173210
<UIcon
174-
v-if="item.icon"
175-
:name="item.icon"
176-
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
211+
v-if="item.trailingIcon"
212+
:name="item.trailingIcon"
213+
:class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })"
177214
/>
178215
<UIcon
179216
v-else-if="item.children?.length"
180-
:name="isExpanded ? (expandedIcon ?? appConfig.ui.icons.folderOpen) : (collapsedIcon ?? appConfig.ui.icons.folder)"
181-
:class="ui.linkLeadingIcon({ class: [props.ui?.linkLeadingIcon, item.ui?.linkLeadingIcon] })"
217+
:name="trailingIcon ?? appConfig.ui.icons.chevronDown"
218+
:class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })"
182219
/>
183220
</slot>
184-
185-
<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] })">
186-
<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; }>)">
187-
{{ getItemLabel(item) }}
188-
</slot>
189-
</span>
190-
191-
<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] })">
192-
<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; }>)">
193-
<UIcon v-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })" />
194-
<UIcon v-else-if="item.children?.length" :name="trailingIcon ?? appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon] })" />
195-
</slot>
196-
</span>
197-
</slot>
198-
</button>
199-
</slot>
200-
201-
<ul v-if="item.children?.length && isExpanded" :class="ui.listWithChildren({ class: [props.ui?.listWithChildren, item.ui?.listWithChildren] })">
202-
<ReuseTreeTemplate :items="item.children" :level="level + 1" />
203-
</ul>
204-
</TreeItem>
205-
</li>
221+
</span>
222+
</slot>
223+
</button>
224+
</slot>
225+
226+
<ul
227+
v-if="item.children?.length && isExpanded"
228+
role="group"
229+
:class="ui.listWithChildren({ class: [props.ui?.listWithChildren, item.ui?.listWithChildren] })"
230+
>
231+
<ReuseTreeTemplate :items="item.children" :level="level + 1" />
232+
</ul>
233+
</TreeItem>
206234
</DefineTreeTemplate>
207235

208236
<TreeRoot
@@ -212,6 +240,6 @@ const defaultExpanded = computed(() =>
212240
:default-expanded="defaultExpanded"
213241
:selection-behavior="selectionBehavior"
214242
>
215-
<ReuseTreeTemplate :items="items" :level="0" />
243+
<ReuseTreeTemplate :items="items" :level="1" />
216244
</TreeRoot>
217245
</template>

src/runtime/components/prose/CodeTree.vue

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -146,43 +146,50 @@ onBeforeUpdate(() => rerenderCount.value++)
146146
<!-- eslint-disable vue/no-template-shadow -->
147147
<template>
148148
<DefineTreeTemplate v-slot="{ items, level }">
149-
<li
149+
<TreeItem
150150
v-for="(item, index) in items"
151151
:key="`${level}-${index}`"
152-
:class="level > 0 ? ui.itemWithChildren({ class: props.ui?.itemWithChildren }) : ui.item({ class: props.ui?.item })"
152+
v-slot="{ isExpanded, isSelected }"
153+
:level="level"
154+
:value="item"
155+
:class="level > 1 ? ui.itemWithChildren({ class: props.ui?.itemWithChildren }) : ui.item({ class: props.ui?.item })"
153156
>
154-
<TreeItem
155-
v-slot="{ isExpanded, isSelected }"
156-
as-child
157-
:level="level"
158-
:value="item"
157+
<button
158+
type="button"
159+
:data-expanded="isExpanded"
160+
:class="ui.link({ class: props.ui?.link, active: isSelected })"
159161
>
160-
<button :class="ui.link({ class: props.ui?.link, active: isSelected })">
162+
<UIcon
163+
v-if="item.children?.length"
164+
:name="isExpanded ? appConfig.ui.icons.folderOpen : appConfig.ui.icons.folder"
165+
:class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon })"
166+
/>
167+
<UCodeIcon
168+
v-else
169+
:filename="item.label"
170+
:class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon })"
171+
/>
172+
173+
<span :class="ui.linkLabel({ class: props.ui?.linkLabel })">
174+
{{ item.label }}
175+
</span>
176+
177+
<span v-if="item.children?.length" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
161178
<UIcon
162-
v-if="item.children?.length"
163-
:name="isExpanded ? appConfig.ui.icons.folderOpen : appConfig.ui.icons.folder"
164-
:class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon })"
179+
:name="appConfig.ui.icons.chevronDown"
180+
:class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon })"
165181
/>
166-
<UCodeIcon
167-
v-else
168-
:filename="item.label"
169-
:class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon })"
170-
/>
171-
172-
<span :class="ui.linkLabel({ class: props.ui?.linkLabel })">
173-
{{ item.label }}
174-
</span>
182+
</span>
183+
</button>
175184

176-
<span v-if="item.children?.length" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
177-
<UIcon :name="appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon })" />
178-
</span>
179-
</button>
180-
181-
<ul v-if="item.children?.length && isExpanded" :class="ui.listWithChildren({ class: props.ui?.listWithChildren })">
182-
<ReuseTreeTemplate :items="item.children" :level="level + 1" />
183-
</ul>
184-
</TreeItem>
185-
</li>
185+
<ul
186+
v-if="item.children?.length && isExpanded"
187+
role="group"
188+
:class="ui.listWithChildren({ class: props.ui?.listWithChildren })"
189+
>
190+
<ReuseTreeTemplate :items="item.children" :level="level + 1" />
191+
</ul>
192+
</TreeItem>
186193
</DefineTreeTemplate>
187194

188195
<div v-bind="$attrs" :class="ui.root({ class: [props.ui?.root, props.class] })">
@@ -193,7 +200,7 @@ onBeforeUpdate(() => rerenderCount.value++)
193200
:get-key="(item) => item.path"
194201
:default-expanded="expanded"
195202
>
196-
<ReuseTreeTemplate :items="items" :level="0" />
203+
<ReuseTreeTemplate :items="items" :level="1" />
197204
</TreeRoot>
198205

199206
<div :class="ui.content({ class: props.ui?.content })">

src/theme/prose/code-tree.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default (options: Required<NuxtOptions['ui']>) => ({
1111
linkLeadingIcon: 'size-4 shrink-0',
1212
linkLabel: 'truncate',
1313
linkTrailing: 'ms-auto inline-flex gap-1.5 items-center',
14-
linkTrailingIcon: 'size-5 transform transition-transform duration-200 shrink-0 group-data-expanded:rotate-180',
14+
linkTrailingIcon: 'size-5 transform transition-transform duration-200 shrink-0 group-data-[expanded=true]:rotate-180',
1515
content: 'overflow-hidden lg:col-span-2 flex flex-col [&>div]:my-0 [&>div]:flex-1 [&>div]:flex [&>div]:flex-col [&>div>div]:border-0 [&>div>pre]:border-b-0 [&>div>pre]:border-s-0 [&>div>pre]:border-e-0 [&>div>pre]:rounded-l-none [&>div>pre]:flex-1 [&>div]:overflow-y-auto'
1616
},
1717
variants: {

src/theme/tree.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default (options: Required<ModuleOptions>) => ({
1010
linkLeadingIcon: 'shrink-0',
1111
linkLabel: 'truncate',
1212
linkTrailing: 'ms-auto inline-flex gap-1.5 items-center',
13-
linkTrailingIcon: 'shrink-0 transform transition-transform duration-200 group-data-expanded:rotate-180'
13+
linkTrailingIcon: 'shrink-0 transform transition-transform duration-200 group-data-[expanded=true]:rotate-180'
1414
},
1515
variants: {
1616
color: {

0 commit comments

Comments
 (0)