Skip to content

Commit ffc8c40

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

File tree

8 files changed

+72
-74
lines changed

8 files changed

+72
-74
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
"props": {
33
"collapseInMenu": "When true, overflowing breadcrumb items are collapsed into a contextual menu rather than a static ellipsis.",
44
"divider": "Specifies the dividing character between items.",
5+
"ellipsis": "Text displayed when breadcrumb items collapsed.",
56
"icons": "Specifies that the dividers between items are [v-icon](/components/icons)s.",
67
"justifyCenter": "Align the breadcrumbs center.",
78
"justifyEnd": "Align the breadcrumbs at the end.",
89
"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."
10+
"totalVisible": "Determines how many breadcrumb items can be shown before middle items are collapsed."
1011
},
1112
"slots": {
1213
"divider": "The slot used for dividers.",

packages/docs/src/examples/v-breadcrumbs/slot-collapse-in-menu.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<v-breadcrumbs :items="items" :max-items="3" collapse-in-menu>
2+
<v-breadcrumbs :items="items" :total-visible="3" collapse-in-menu>
33
</v-breadcrumbs>
44
</template>
55

packages/docs/src/examples/v-breadcrumbs/slot-max-items.vue renamed to packages/docs/src/examples/v-breadcrumbs/slot-total-visible.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<v-breadcrumbs :items="items" :max-items="3">
2+
<v-breadcrumbs :items="items" :total-visible="3">
33
</v-breadcrumbs>
44
</template>
55

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ You can use the `title` slot to customize each breadcrumb title.
8181

8282
#### Collapsed breadcrumbs
8383

84-
You can use the `maxItems` prop to redefine the maximum number of breadcrumb items displayed before the component collapses.
84+
You can use the `totalVisible` prop to redefine the maximum number of breadcrumb items displayed before the component collapses.
8585

86-
<ExamplesExample file="v-breadcrumbs/slot-max-items" />
86+
<ExamplesExample file="v-breadcrumbs/slot-total-visible" />
8787

8888
#### Collapsed with menu
8989

packages/vuetify/playgrounds/Playground.breadcrumbs.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<template>
22
<v-app>
33
<v-container>
4-
<v-breadcrumbs :items="items" :max-items="3" />
5-
<v-breadcrumbs :items="items" :max-items="3" collapse-in-menu />
4+
<v-breadcrumbs :items="items" :total-visible="3" />
5+
<v-breadcrumbs :items="items" :total-visible="3" collapse-in-menu />
66
</v-container>
77
</v-app>
88
</template>

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
text-decoration: none
3030
vertical-align: $breadcrumbs-vertical-align
3131

32+
&--ellipsis
33+
cursor: pointer
34+
3235
&--disabled
3336
opacity: $breadcrumbs-item-disabled-opacity
3437
pointer-events: none

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

Lines changed: 42 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import './VBreadcrumbs.sass'
44
// Components
55
import { VBreadcrumbsDivider } from './VBreadcrumbsDivider'
66
import { VBreadcrumbsItem } from './VBreadcrumbsItem'
7-
import { VBtn } from '../VBtn'
87
import { VIcon } from '../VIcon'
98
import { VList, VListItem, VListItemTitle } from '../VList'
109
import { VMenu } from '../VMenu'
@@ -20,8 +19,8 @@ import { makeRoundedProps, useRounded } from '@/composables/rounded'
2019
import { makeTagProps } from '@/composables/tag'
2120

2221
// Utilities
23-
import { computed, ref, toRef } from 'vue'
24-
import { genericComponent, isObject, propsFactory, useRender } from '@/util'
22+
import { computed, ref, toRef, watch } from 'vue'
23+
import { genericComponent, isObject, noop, propsFactory, useRender } from '@/util'
2524

2625
// Types
2726
import type { PropType } from 'vue'
@@ -46,16 +45,17 @@ export const makeVBreadcrumbsProps = propsFactory({
4645
type: String,
4746
default: '/',
4847
},
48+
ellipsis: {
49+
type: String,
50+
default: '...',
51+
},
4952
icon: IconValue,
5053
items: {
5154
type: Array as PropType<readonly BreadcrumbItem[]>,
5255
default: () => ([]),
5356
},
5457
itemProps: Boolean,
55-
maxItems: {
56-
type: Number,
57-
default: 8,
58-
},
58+
totalVisible: Number,
5959

6060
...makeComponentProps(),
6161
...makeDensityProps(),
@@ -99,33 +99,36 @@ export const VBreadcrumbs = genericComponent<new <T extends BreadcrumbItem>(
9999
const items = computed(() => props.items.map(item => {
100100
return typeof item === 'string' ? { item: { title: item }, raw: item } : { item, raw: item }
101101
}))
102-
const showEllipsis = computed(() => items.value.length >= props.maxItems)
102+
const showEllipsis = toRef(() => props.totalVisible ? items.value.length > props.totalVisible : false)
103103
const enableEllipsis = ref(showEllipsis.value)
104104

105105
const onClickEllipsis = () => {
106106
enableEllipsis.value = false
107107
}
108108

109+
watch(showEllipsis, (value: boolean) => {
110+
enableEllipsis.value = value
111+
})
112+
109113
useRender(() => {
110114
const hasPrepend = !!(slots.prepend || props.icon)
111115

112116
return (
113117
<props.tag
114118
aria-label="breadcrumbs"
119+
class={[
120+
'v-breadcrumbs',
121+
backgroundColorClasses.value,
122+
densityClasses.value,
123+
roundedClasses.value,
124+
props.class,
125+
]}
126+
style={[
127+
backgroundColorStyles.value,
128+
props.style,
129+
]}
115130
>
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-
>
131+
<ol>
129132
{ hasPrepend && (
130133
<li key="prepend" class="v-breadcrumbs__prepend">
131134
{ !slots.prepend ? (
@@ -193,38 +196,26 @@ export const VBreadcrumbs = genericComponent<new <T extends BreadcrumbItem>(
193196
<VBreadcrumbsDivider />
194197

195198
<VBreadcrumbsItem
196-
disabled={ false }
197-
onClick={ onClickEllipsis }
199+
tabindex="0"
200+
onClick={ props.collapseInMenu ? noop : onClickEllipsis }
201+
class="v-breadcrumbs-item--ellipsis"
198202
>
203+
{ props.ellipsis }
199204
{ 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-
)}
205+
<VMenu activator="parent">
206+
{{
207+
default: () => (
208+
<VList>
209+
{ items.value.slice(1, items.value.length - 1).map(({ item }, index) => (
210+
<VListItem key={ index } value={ index } component="a" href={ 'href' in item ? item.href : undefined }>
211+
<VListItemTitle>{ item.title }</VListItemTitle>
212+
</VListItem>
213+
))}
214+
</VList>
215+
),
216+
}}
217+
</VMenu>
218+
) : null }
228219
</VBreadcrumbsItem>
229220

230221
<VBreadcrumbsDivider />

packages/vuetify/src/components/VBreadcrumbs/__tests__/VBreadcrumbs.spec.browser.tsx

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -111,30 +111,32 @@ describe('VBreadcrumbs', () => {
111111
expect(screen.getByText('-')).toBeVisible()
112112
})
113113

114-
it('should collapse into ellipsis when items exceed maxItems', async () => {
114+
it('should collapse into ellipsis when items exceed totalVisible', async () => {
115115
render(() => (
116116
<VBreadcrumbs
117117
items={['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']}
118-
maxItems={ 5 }
118+
totalVisible={ 5 }
119119
/>
120120
))
121121

122-
const ellipsisBtn = screen.getByCSS('button .mdi-dots-horizontal')
123-
expect(ellipsisBtn).toBeVisible()
122+
const ellipsis = screen.getByText('...')
123+
expect(ellipsis).toBeVisible()
124+
expect(ellipsis).toHaveClass('v-breadcrumbs-item--ellipsis')
124125
})
125126

126-
it('should expand all items when ellipsis is clicked', async () => {
127+
it('should expand all items when ellipsis is clicked and collapseInMenu = false', async () => {
127128
render(() => (
128129
<VBreadcrumbs
129130
items={['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']}
130-
maxItems={ 5 }
131+
totalVisible={ 5 }
132+
collapseInMenu={ false }
131133
/>
132134
))
133135

134-
const ellipsisBtn = screen.getByCSS('button .mdi-dots-horizontal')
135-
expect(ellipsisBtn).toBeVisible()
136+
const ellipsis = screen.getByText('...')
137+
expect(ellipsis).toBeVisible()
136138

137-
await ellipsisBtn.click()
139+
await ellipsis.click()
138140

139141
const items = screen.getAllByCSS('.v-breadcrumbs-item')
140142
expect(items).toHaveLength(8)
@@ -149,16 +151,17 @@ describe('VBreadcrumbs', () => {
149151
{ title: 'c', href: '/c' },
150152
{ title: 'd' },
151153
]}
152-
maxItems={ 3 }
154+
totalVisible={ 3 }
153155
collapseInMenu
154156
/>
155157
))
156158

157-
const activator = screen.getByCSS('button .mdi-dots-horizontal')
158-
expect(activator).toBeVisible()
159+
const ellipsis = screen.getByText('...')
160+
expect(ellipsis).toBeVisible()
159161

160-
await activator.click()
162+
await ellipsis.click()
161163

164+
// Le menu doit maintenant être rendu
162165
const listItems = screen.getAllByText(/b|c/)
163166
expect(listItems).toHaveLength(2)
164167
})
@@ -167,13 +170,13 @@ describe('VBreadcrumbs', () => {
167170
render(() => (
168171
<VBreadcrumbs
169172
items={['a', 'b', 'c', 'd']}
170-
maxItems={ 3 }
173+
totalVisible={ 3 }
171174
collapseInMenu={ false }
172175
/>
173176
))
174177

175-
const activator = screen.getByCSS('button .mdi-dots-horizontal')
176-
expect(activator).toBeVisible()
178+
const ellipsis = screen.getByText('...')
179+
expect(ellipsis).toBeVisible()
177180

178181
const menu = screen.queryByCSS('.v-menu')
179182
expect(menu).toBeNull()

0 commit comments

Comments
 (0)