diff --git a/packages/api-generator/src/locale/en/VBreadcrumbs.json b/packages/api-generator/src/locale/en/VBreadcrumbs.json
index 6bc0747f73b..07630abb120 100644
--- a/packages/api-generator/src/locale/en/VBreadcrumbs.json
+++ b/packages/api-generator/src/locale/en/VBreadcrumbs.json
@@ -1,14 +1,18 @@
{
"props": {
+ "collapseInMenu": "When true, overflowing breadcrumb items are collapsed into a contextual menu rather than a static ellipsis.",
"divider": "Specifies the dividing character between items.",
+ "ellipsis": "Text displayed when breadcrumb items collapsed.",
"icons": "Specifies that the dividers between items are [v-icon](/components/icons)s.",
"justifyCenter": "Align the breadcrumbs center.",
"justifyEnd": "Align the breadcrumbs at the end.",
- "large": "Increase the font-size of the breadcrumb item text to 16px (14px default)."
+ "large": "Increase the font-size of the breadcrumb item text to 16px (14px default).",
+ "totalVisible": "Determines how many breadcrumb items can be shown before middle items are collapsed."
},
"slots": {
"divider": "The slot used for dividers.",
"prepend": "The slot used for prepend content.",
- "title": "The slot used to display the title of each breadcrumb."
+ "title": "The slot used to display the title of each breadcrumb.",
+ "listItem": "The slot used for customizing each item inside the contextual menu when breadcrumbs items are collapsed."
}
}
diff --git a/packages/docs/src/examples/v-breadcrumbs/slot-collapse-in-menu.vue b/packages/docs/src/examples/v-breadcrumbs/slot-collapse-in-menu.vue
new file mode 100644
index 00000000000..122915ebbd2
--- /dev/null
+++ b/packages/docs/src/examples/v-breadcrumbs/slot-collapse-in-menu.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/examples/v-breadcrumbs/slot-list-item.vue b/packages/docs/src/examples/v-breadcrumbs/slot-list-item.vue
new file mode 100644
index 00000000000..513522c2479
--- /dev/null
+++ b/packages/docs/src/examples/v-breadcrumbs/slot-list-item.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+ ๐ {{ item.title }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/examples/v-breadcrumbs/slot-total-visible.vue b/packages/docs/src/examples/v-breadcrumbs/slot-total-visible.vue
new file mode 100644
index 00000000000..466b628d3be
--- /dev/null
+++ b/packages/docs/src/examples/v-breadcrumbs/slot-total-visible.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
diff --git a/packages/docs/src/pages/en/components/breadcrumbs.md b/packages/docs/src/pages/en/components/breadcrumbs.md
index ad7ce1f360a..bf255240624 100644
--- a/packages/docs/src/pages/en/components/breadcrumbs.md
+++ b/packages/docs/src/pages/en/components/breadcrumbs.md
@@ -78,3 +78,21 @@ To customize the divider, use the `divider` slot.
You can use the `title` slot to customize each breadcrumb title.
+
+#### Collapsed breadcrumbs
+
+You can use the `totalVisible` prop to redefine the maximum number of breadcrumb items displayed before the component collapses.
+
+
+
+#### Collapsed with menu
+
+You can use the `collapseInMenu` prop to display the collapsed breadcrumb items inside a dropdown menu.
+
+
+
+#### Collapsed with custom menu
+
+You can use the `list-item` slot to customize how each breadcrumb item is rendered inside the collapsed menu when `collapseInMenu` is enabled.
+
+
diff --git a/packages/vuetify/playgrounds/Playground.breadcrumbs.vue b/packages/vuetify/playgrounds/Playground.breadcrumbs.vue
new file mode 100644
index 00000000000..1b99e70780c
--- /dev/null
+++ b/packages/vuetify/playgrounds/Playground.breadcrumbs.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+ ๐ {{ item.title }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbs.sass b/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbs.sass
index edae8df1829..9c42f581714 100644
--- a/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbs.sass
+++ b/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbs.sass
@@ -5,6 +5,7 @@
.v-breadcrumbs
display: flex
align-items: center
+ flex-wrap: wrap
line-height: $breadcrumbs-line-height
padding: $breadcrumbs-padding-y $breadcrumbs-padding-x
@@ -28,6 +29,9 @@
text-decoration: none
vertical-align: $breadcrumbs-vertical-align
+ &--ellipsis
+ cursor: pointer
+
&--disabled
opacity: $breadcrumbs-item-disabled-opacity
pointer-events: none
diff --git a/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbs.tsx b/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbs.tsx
index 27f7f73e6ec..e780d3ec6e7 100644
--- a/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbs.tsx
+++ b/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbs.tsx
@@ -4,8 +4,10 @@ import './VBreadcrumbs.sass'
// Components
import { VBreadcrumbsDivider } from './VBreadcrumbsDivider'
import { VBreadcrumbsItem } from './VBreadcrumbsItem'
+import { VIcon } from '../VIcon'
+import { VList, VListItem, VListItemTitle } from '../VList'
+import { VMenu } from '../VMenu'
import { VDefaultsProvider } from '@/components/VDefaultsProvider'
-import { VIcon } from '@/components/VIcon'
// Composables
import { useBackgroundColor } from '@/composables/color'
@@ -17,8 +19,8 @@ import { makeRoundedProps, useRounded } from '@/composables/rounded'
import { makeTagProps } from '@/composables/tag'
// Utilities
-import { computed, toRef } from 'vue'
-import { genericComponent, isObject, propsFactory, useRender } from '@/util'
+import { computed, ref, toRef, watch } from 'vue'
+import { genericComponent, isObject, noop, propsFactory, useRender } from '@/util'
// Types
import type { PropType } from 'vue'
@@ -36,23 +38,35 @@ export const makeVBreadcrumbsProps = propsFactory({
activeClass: String,
activeColor: String,
bgColor: String,
+ collapseInMenu: Boolean,
color: String,
disabled: Boolean,
divider: {
type: String,
default: '/',
},
+ ellipsis: {
+ type: String,
+ default: '...',
+ },
icon: IconValue,
items: {
type: Array as PropType,
default: () => ([]),
},
itemProps: Boolean,
+ listProps: {
+ type: Object as PropType,
+ },
+ menuProps: {
+ type: Object as PropType,
+ },
+ totalVisible: Number,
...makeComponentProps(),
...makeDensityProps(),
...makeRoundedProps(),
- ...makeTagProps({ tag: 'ul' }),
+ ...makeTagProps({ tag: 'nav' }),
}, 'VBreadcrumbs')
export const VBreadcrumbs = genericComponent(
@@ -64,6 +78,7 @@ export const VBreadcrumbs = genericComponent(
title: { item: InternalBreadcrumbItem, index: number }
divider: { item: T, index: number }
item: { item: InternalBreadcrumbItem, index: number }
+ 'list-item': { item: InternalBreadcrumbItem, index: number }
default: never
}
) => GenericProps>()({
@@ -91,12 +106,23 @@ export const VBreadcrumbs = genericComponent(
const items = computed(() => props.items.map(item => {
return typeof item === 'string' ? { item: { title: item }, raw: item } : { item, raw: item }
}))
+ const ellipsisEnabled = toRef(() => props.totalVisible ? items.value.length > props.totalVisible : false)
+ const hasEllipsis = ref(ellipsisEnabled.value)
+
+ const onClickEllipsis = () => {
+ hasEllipsis.value = false
+ }
+
+ watch(ellipsisEnabled, (value: boolean) => {
+ hasEllipsis.value = value
+ })
useRender(() => {
const hasPrepend = !!(slots.prepend || props.icon)
return (
(
props.style,
]}
>
+
{ hasPrepend && (
-
{ !slots.prepend ? (
@@ -133,7 +160,7 @@ export const VBreadcrumbs = genericComponent(
)}
- { items.value.map(({ item, raw }, index, array) => (
+ { !hasEllipsis.value && items.value.map(({ item, raw }, index, array) => (
<>
{ slots.item?.({ item, index }) ?? (
(
>
))}
+ { hasEllipsis.value && (
+ <>
+ { (() => {
+ const { item } = items.value[0]
+ return (
+ <>
+ { slots.item?.({ item, index: 0 }) ?? (
+
+ )}
+ >
+ )
+ })()}
+
+
+
+
+ { props.ellipsis }
+ { props.collapseInMenu ? (
+
+ {{
+ default: () => (
+
+ { items.value.slice(1, items.value.length - 1).map(({ item }, index) => {
+ if (slots['list-item']) {
+ return slots['list-item']({ item, index })
+ }
+ return (
+
+ { item.title }
+
+ )
+ })}
+
+ ),
+ }}
+
+ ) : null }
+
+
+
+
+ { (() => {
+ const lastIndex = items.value.length - 1
+ const { item } = items.value[lastIndex]
+ return (
+ <>
+ { slots.item?.({ item, index: lastIndex }) ?? (
+
+ )}
+ >
+ )
+ })()}
+ >
+ )}
+
{ slots.default?.() }
+
)
})
diff --git a/packages/vuetify/src/components/VBreadcrumbs/__tests__/VBreadcrumbs.spec.browser.tsx b/packages/vuetify/src/components/VBreadcrumbs/__tests__/VBreadcrumbs.spec.browser.tsx
index a606a365f60..427db8492cd 100644
--- a/packages/vuetify/src/components/VBreadcrumbs/__tests__/VBreadcrumbs.spec.browser.tsx
+++ b/packages/vuetify/src/components/VBreadcrumbs/__tests__/VBreadcrumbs.spec.browser.tsx
@@ -4,7 +4,7 @@ import { VBreadcrumbsDivider } from '../VBreadcrumbsDivider'
import { VBreadcrumbsItem } from '../VBreadcrumbsItem'
// Utilities
-import { render, screen } from '@test'
+import { render, screen, userEvent } from '@test'
describe('VBreadcrumbs', () => {
it('should use item slot', async () => {
@@ -110,4 +110,75 @@ describe('VBreadcrumbs', () => {
expect(screen.getByText('/')).toBeVisible()
expect(screen.getByText('-')).toBeVisible()
})
+
+ it('should collapse into ellipsis when items exceed totalVisible', async () => {
+ render(() => (
+
+ ))
+
+ const ellipsis = screen.getByText('...')
+ expect(ellipsis).toBeVisible()
+ expect(ellipsis).toHaveClass('v-breadcrumbs-item--ellipsis')
+ })
+
+ it('should expand all items when ellipsis is clicked and collapseInMenu = false', async () => {
+ render(() => (
+
+ ))
+
+ const ellipsis = screen.getByText('...')
+ expect(ellipsis).toBeVisible()
+
+ await userEvent.click(ellipsis)
+
+ const items = screen.getAllByCSS('.v-breadcrumbs-item')
+ expect(items).toHaveLength(8)
+ })
+
+ it('should show a VMenu when collapseInMenu = true', async () => {
+ render(() => (
+
+ ))
+
+ const ellipsis = screen.getByText('...')
+ expect(ellipsis).toBeVisible()
+
+ await userEvent.click(ellipsis)
+
+ // Le menu doit maintenant รชtre rendu
+ const listItems = screen.getAllByText(/b|c/)
+ expect(listItems).toHaveLength(2)
+ })
+
+ it('should not use VMenu when collapseInMenu = false', () => {
+ render(() => (
+
+ ))
+
+ const ellipsis = screen.getByText('...')
+ expect(ellipsis).toBeVisible()
+
+ const menu = screen.queryByCSS('.v-menu')
+ expect(menu).toBeNull()
+ })
})