Skip to content

Commit a7b13a4

Browse files
Jonathan de FlaugerguesJonathan de Flaugergues
authored andcommitted
feat(VBreadcrumbs): improve accessibility and add menu when collapsed
1 parent ee213c5 commit a7b13a4

File tree

8 files changed

+342
-16
lines changed

8 files changed

+342
-16
lines changed

packages/api-generator/src/locale/en/VBreadcrumbs.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
{
22
"props": {
3+
"collapseInMenu": "When true, overflowing breadcrumb items are collapsed into a contextual menu rather than a static ellipsis.",
34
"divider": "Specifies the dividing character between items.",
45
"icons": "Specifies that the dividers between items are [v-icon](/components/icons)s.",
56
"justifyCenter": "Align the breadcrumbs center.",
67
"justifyEnd": "Align the breadcrumbs at the end.",
7-
"large": "Increase the font-size of the breadcrumb item text to 16px (14px default)."
8+
"large": "Increase the font-size of the breadcrumb item text to 16px (14px default).",
9+
"maxItems": "Determines how many breadcrumb items can be shown before middle items are collapsed."
810
},
911
"slots": {
1012
"divider": "The slot used for dividers.",
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<template>
2+
<v-breadcrumbs :items="items" :max-items="3" collapse-in-menu>
3+
</v-breadcrumbs>
4+
</template>
5+
6+
<script setup>
7+
const items = [
8+
{
9+
title: 'Dashboard',
10+
disabled: false,
11+
href: 'breadcrumbs_dashboard',
12+
},
13+
{
14+
title: 'Link 1',
15+
disabled: false,
16+
href: 'breadcrumbs_link_1',
17+
},
18+
{
19+
title: 'Link 2',
20+
disabled: false,
21+
href: 'breadcrumbs_link_2',
22+
},
23+
{
24+
title: 'Link 3',
25+
disabled: false,
26+
href: 'breadcrumbs_link_3',
27+
},
28+
{
29+
title: 'Link 4',
30+
disabled: true,
31+
href: 'breadcrumbs_link_4',
32+
},
33+
]
34+
</script>
35+
36+
<script>
37+
export default {
38+
data: () => ({
39+
items: [
40+
{
41+
title: 'Dashboard',
42+
disabled: false,
43+
href: 'breadcrumbs_dashboard',
44+
},
45+
{
46+
title: 'Link 1',
47+
disabled: false,
48+
href: 'breadcrumbs_link_1',
49+
},
50+
{
51+
title: 'Link 2',
52+
disabled: false,
53+
href: 'breadcrumbs_link_2',
54+
},
55+
{
56+
title: 'Link 3',
57+
disabled: false,
58+
href: 'breadcrumbs_link_3',
59+
},
60+
{
61+
title: 'Link 4',
62+
disabled: true,
63+
href: 'breadcrumbs_link_4',
64+
},
65+
],
66+
}),
67+
}
68+
</script>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<template>
2+
<v-breadcrumbs :items="items" :max-items="3">
3+
</v-breadcrumbs>
4+
</template>
5+
6+
<script setup>
7+
const items = [
8+
{
9+
title: 'Dashboard',
10+
disabled: false,
11+
href: 'breadcrumbs_dashboard',
12+
},
13+
{
14+
title: 'Link 1',
15+
disabled: false,
16+
href: 'breadcrumbs_link_1',
17+
},
18+
{
19+
title: 'Link 2',
20+
disabled: false,
21+
href: 'breadcrumbs_link_2',
22+
},
23+
{
24+
title: 'Link 3',
25+
disabled: false,
26+
href: 'breadcrumbs_link_3',
27+
},
28+
{
29+
title: 'Link 4',
30+
disabled: true,
31+
href: 'breadcrumbs_link_4',
32+
},
33+
]
34+
</script>
35+
36+
<script>
37+
export default {
38+
data: () => ({
39+
items: [
40+
{
41+
title: 'Dashboard',
42+
disabled: false,
43+
href: 'breadcrumbs_dashboard',
44+
},
45+
{
46+
title: 'Link 1',
47+
disabled: false,
48+
href: 'breadcrumbs_link_1',
49+
},
50+
{
51+
title: 'Link 2',
52+
disabled: false,
53+
href: 'breadcrumbs_link_2',
54+
},
55+
{
56+
title: 'Link 3',
57+
disabled: false,
58+
href: 'breadcrumbs_link_3',
59+
},
60+
{
61+
title: 'Link 4',
62+
disabled: true,
63+
href: 'breadcrumbs_link_4',
64+
},
65+
],
66+
}),
67+
}
68+
</script>

packages/docs/src/pages/en/components/breadcrumbs.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,15 @@ To customize the divider, use the `divider` slot.
7878
You can use the `title` slot to customize each breadcrumb title.
7979

8080
<ExamplesExample file="v-breadcrumbs/slot-title" />
81+
82+
#### Collapsed breadcrumbs
83+
84+
You can use the `maxItems` prop to redefine the maximum number of breadcrumb items displayed before the component collapses.
85+
86+
<ExamplesExample file="v-breadcrumbs/slot-max-items" />
87+
88+
#### Collapsed with menu
89+
90+
You can use the `collapseInMenu` prop to display the collapsed breadcrumb items inside a dropdown menu.
91+
92+
<ExamplesExample file="v-breadcrumbs/slot-collapse-in-menu" />
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<template>
2+
<v-app>
3+
<v-container>
4+
<v-breadcrumbs :items="items" :max-items="3" />
5+
<v-breadcrumbs :items="items" :max-items="3" collapse-in-menu />
6+
</v-container>
7+
</v-app>
8+
</template>
9+
10+
<script>
11+
export default {
12+
name: 'Playground',
13+
data: () => ({
14+
items: Array.from({ length: 4 }, (k, v) => ({ title: `Link ${v + 1}`, href: `breadcrumbs_link_${v + 1}` })),
15+
}),
16+
}
17+
</script>

packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbs.sass

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
.v-breadcrumbs
66
display: flex
77
align-items: center
8+
flex-wrap: wrap
89
line-height: $breadcrumbs-line-height
910
padding: $breadcrumbs-padding-y $breadcrumbs-padding-x
1011

packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbs.tsx

Lines changed: 105 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import './VBreadcrumbs.sass'
44
// Components
55
import { VBreadcrumbsDivider } from './VBreadcrumbsDivider'
66
import { VBreadcrumbsItem } from './VBreadcrumbsItem'
7+
import { VBtn } from '../VBtn'
8+
import { VIcon } from '../VIcon'
9+
import { VList, VListItem, VListItemTitle } from '../VList'
10+
import { VMenu } from '../VMenu'
711
import { VDefaultsProvider } from '@/components/VDefaultsProvider'
8-
import { VIcon } from '@/components/VIcon'
912

1013
// Composables
1114
import { useBackgroundColor } from '@/composables/color'
@@ -17,7 +20,7 @@ import { makeRoundedProps, useRounded } from '@/composables/rounded'
1720
import { makeTagProps } from '@/composables/tag'
1821

1922
// Utilities
20-
import { computed, toRef } from 'vue'
23+
import { computed, ref, toRef } from 'vue'
2124
import { genericComponent, isObject, propsFactory, useRender } from '@/util'
2225

2326
// Types
@@ -36,6 +39,7 @@ export const makeVBreadcrumbsProps = propsFactory({
3639
activeClass: String,
3740
activeColor: String,
3841
bgColor: String,
42+
collapseInMenu: Boolean,
3943
color: String,
4044
disabled: Boolean,
4145
divider: {
@@ -48,11 +52,15 @@ export const makeVBreadcrumbsProps = propsFactory({
4852
default: () => ([]),
4953
},
5054
itemProps: Boolean,
55+
maxItems: {
56+
type: Number,
57+
default: 8,
58+
},
5159

5260
...makeComponentProps(),
5361
...makeDensityProps(),
5462
...makeRoundedProps(),
55-
...makeTagProps({ tag: 'ul' }),
63+
...makeTagProps({ tag: 'nav' }),
5664
}, 'VBreadcrumbs')
5765

5866
export const VBreadcrumbs = genericComponent<new <T extends BreadcrumbItem>(
@@ -91,24 +99,33 @@ export const VBreadcrumbs = genericComponent<new <T extends BreadcrumbItem>(
9199
const items = computed(() => props.items.map(item => {
92100
return typeof item === 'string' ? { item: { title: item }, raw: item } : { item, raw: item }
93101
}))
102+
const showEllipsis = computed(() => items.value.length >= props.maxItems)
103+
const enableEllipsis = ref(showEllipsis.value)
104+
105+
const onClickEllipsis = () => {
106+
enableEllipsis.value = false
107+
}
94108

95109
useRender(() => {
96110
const hasPrepend = !!(slots.prepend || props.icon)
97111

98112
return (
99113
<props.tag
100-
class={[
101-
'v-breadcrumbs',
102-
backgroundColorClasses.value,
103-
densityClasses.value,
104-
roundedClasses.value,
105-
props.class,
106-
]}
107-
style={[
108-
backgroundColorStyles.value,
109-
props.style,
110-
]}
114+
aria-label="breadcrumbs"
111115
>
116+
<ol
117+
class={[
118+
'v-breadcrumbs',
119+
backgroundColorClasses.value,
120+
densityClasses.value,
121+
roundedClasses.value,
122+
props.class,
123+
]}
124+
style={[
125+
backgroundColorStyles.value,
126+
props.style,
127+
]}
128+
>
112129
{ hasPrepend && (
113130
<li key="prepend" class="v-breadcrumbs__prepend">
114131
{ !slots.prepend ? (
@@ -133,7 +150,7 @@ export const VBreadcrumbs = genericComponent<new <T extends BreadcrumbItem>(
133150
</li>
134151
)}
135152

136-
{ items.value.map(({ item, raw }, index, array) => (
153+
{ !enableEllipsis.value && items.value.map(({ item, raw }, index, array) => (
137154
<>
138155
{ slots.item?.({ item, index }) ?? (
139156
<VBreadcrumbsItem
@@ -157,7 +174,80 @@ export const VBreadcrumbs = genericComponent<new <T extends BreadcrumbItem>(
157174
</>
158175
))}
159176

177+
{ enableEllipsis.value && (
178+
<>
179+
{ (() => {
180+
const { item } = items.value[0]
181+
return (
182+
<>
183+
{ slots.item?.({ item, index: 0 }) ?? (
184+
<VBreadcrumbsItem
185+
disabled={ false }
186+
{ ...(typeof item === 'string' ? { title: item } : item) }
187+
/>
188+
)}
189+
</>
190+
)
191+
})()}
192+
193+
<VBreadcrumbsDivider />
194+
195+
<VBreadcrumbsItem
196+
disabled={ false }
197+
onClick={ onClickEllipsis }
198+
>
199+
{ props.collapseInMenu ? (
200+
<VMenu>
201+
{{
202+
activator: ({ props: activatorProps }) => (
203+
<VBtn
204+
icon="mdi-dots-horizontal"
205+
variant="text"
206+
size="x-small"
207+
{ ...activatorProps }
208+
/>
209+
),
210+
default: () => (
211+
<VList>
212+
{ items.value.slice(1, items.value.length - 1).map(({ item }, index) => (
213+
<VListItem key={ index } value={ index } component="a" href={ 'href' in item ? item.href : undefined }>
214+
<VListItemTitle>{ item.title }</VListItemTitle>
215+
</VListItem>
216+
))}
217+
</VList>
218+
),
219+
}}
220+
</VMenu>
221+
) : (
222+
<VBtn
223+
icon="mdi-dots-horizontal"
224+
variant="text"
225+
size="x-small"
226+
/>
227+
)}
228+
</VBreadcrumbsItem>
229+
230+
<VBreadcrumbsDivider />
231+
232+
{ (() => {
233+
const lastIndex = items.value.length - 1
234+
const { item } = items.value[lastIndex]
235+
return (
236+
<>
237+
{ slots.item?.({ item, index: lastIndex }) ?? (
238+
<VBreadcrumbsItem
239+
disabled
240+
{ ...(typeof item === 'string' ? { title: item } : item) }
241+
/>
242+
)}
243+
</>
244+
)
245+
})()}
246+
</>
247+
)}
248+
160249
{ slots.default?.() }
250+
</ol>
161251
</props.tag>
162252
)
163253
})

0 commit comments

Comments
 (0)