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
7 changes: 7 additions & 0 deletions .changeset/add-mt-breadcrumb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@shopware-ag/meteor-component-library": minor
---

Add `MtBreadcrumb` for hierarchy navigation, rendered from an `items` array, with sizes `xs` and `s` and automatic truncation and collapsing of crumbs on narrow containers. Collapsed crumbs stay reachable through an overflow menu.

`MtActionMenuItem` now also accepts a component for `as`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup lang="ts">
import MtBreadcrumb from "@shopware-ag/meteor-component-library/MtBreadcrumb";

const items = [
{ label: "Home", to: "#" },
{ label: "Products", to: "#" },
{ label: "Shoes" },
];
</script>

<template>
<mt-breadcrumb :items="items" />
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
import MtBreadcrumb from "@shopware-ag/meteor-component-library/MtBreadcrumb";

const items = [
{ label: "Home", to: "#" },
{ label: "Catalog", to: "#" },
{ label: "Clothing and accessories for every season", to: "#" },
{ label: "Shoes", to: "#" },
{ label: "Sneakers", to: "#" },
{ label: "Running shoes" },
];
</script>

<template>
<div
style="
width: 360px;
max-width: 100%;
resize: horizontal;
overflow: hidden;
padding: 8px;
border: 1px dashed var(--color-border-primary-default);
"
>
<mt-breadcrumb :items="items" />
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
import MtBreadcrumb from "@shopware-ag/meteor-component-library/MtBreadcrumb";

const items = [
{ label: "Home", to: "#" },
{ label: "Clothing and accessories for every season of the year", to: "#" },
{ label: "Shoes" },
];
</script>

<template>
<div
style="
width: 480px;
max-width: 100%;
resize: horizontal;
overflow: hidden;
padding: 8px;
border: 1px dashed var(--color-border-primary-default);
"
>
<mt-breadcrumb :items="items" />
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script setup lang="ts">
import { computed } from "vue";
import MtBreadcrumb from "@shopware-ag/meteor-component-library/MtBreadcrumb";

const route = useRoute();

const items = computed(() => {
const segments = route.path.split("/").filter(Boolean);

return segments.map((segment, index) => ({
label:
segment.charAt(0).toUpperCase() + segment.slice(1).replaceAll("-", " "),
to: `/${segments.slice(0, index + 1).join("/")}`,
}));
});
</script>

<template>
<mt-breadcrumb :items="items" />
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script setup lang="ts">
import MtBreadcrumb from "@shopware-ag/meteor-component-library/MtBreadcrumb";

const items = [
{ label: "Home", to: "#" },
{ label: "Products", to: "#" },
{ label: "Shoes" },
];
</script>

<template>
<div style="display: flex; flex-direction: column; gap: 16px">
<mt-breadcrumb :items="items" size="xs" />
<mt-breadcrumb :items="items" size="s" />
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
import MtBreadcrumb from "@shopware-ag/meteor-component-library/MtBreadcrumb";

const items = [
{ label: "Home", to: "#" },
{ label: "Catalog", to: "#" },
{ label: "Clothing and accessories for every season", to: "#" },
{ label: "Shoes", to: "#" },
{ label: "Sneakers", to: "#" },
{ label: "Running shoes" },
];
</script>

<template>
<div
style="
width: 360px;
max-width: 100%;
resize: horizontal;
overflow: hidden;
padding: 8px;
border: 1px dashed var(--color-border-primary-default);
"
>
<mt-breadcrumb :items="items" overflow="wrap" />
</div>
</template>
99 changes: 99 additions & 0 deletions apps/docs/content/2.components/breadcrumb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
title: Breadcrumb
description: A trail of links that shows where the current page sits in the hierarchy and lets users move back up to any level.
---

::component-example{name="breadcrumb-basic-example"}
::

## Usage

**Breadcrumb** shows the path from the root of a hierarchy to the current page and lets users jump back to any level along the way. Use it on nested pages such as folders, categories, or documents, where users need to understand where they are and move within that structure. It is a secondary aid that complements the main navigation rather than replacing it, and it always ends with the current page.

```ts
import { MtBreadcrumb, type BreadcrumbItem } from "@shopware-ag/meteor-component-library";
```

Pass the trail as `items`, ordered from the root to the current page. Every item has a `label`. An item with a `to` renders as a link, and the last item always renders as the current page, so its `to` is ignored. Links render as a `router-link` by default. In an app without a router, set `link-as="a"` to render plain anchors instead. You can also pass any other element or component, and a single item can override it with its own `as`. Anything other than `router-link` receives the item's `to` value as its `href`, so it must be a URL string, not a route object.

## Examples

### Sizes

::component-example{name="breadcrumb-sizes-example"}
::

### Truncating long labels

Labels are shown in full as long as the trail fits. As the container gets narrower, long labels shrink with an ellipsis, down to 8 characters, before any crumb is hidden. Drag the corner of the box to try it.

::component-example{name="breadcrumb-long-label-example"}
::

### Collapsing on narrow containers

When the trail does not fit, the labels shrink first. Once they have reached their minimum width, the crumbs in the middle move into a menu behind the ellipsis button, beginning with the one next to the root. Drag the corner of the box to watch crumbs shrink, disappear, and come back, and open the menu to reach the hidden levels.

::component-example{name="breadcrumb-collapsed-example"}
::

### Wrapping instead of collapsing

Drag the corner of the box to see the crumbs flow onto additional lines.

::component-example{name="breadcrumb-wrap-example"}
::

### From the current route

Build the items from the route segments and mark nothing by hand: the last item is the current page automatically. `useRoute` comes from `vue-router`.

::component-example{name="breadcrumb-route-example"}
::

## API reference

:component-api

## Best practices

::do-dont{vertical}
#do

- Keep labels short and use the same names as in the navigation and page titles.
- End the items with the current page.
- Give every item except the last one a `to`, so users can move up to any level.

#dont

- Do not use **Breadcrumb** as the only way to reach a page, and do not use it as a replacement for the main navigation.
- Do not put the current page into the items twice, for example once as a link and once as the last item.

::

## Behavior

- **Breadcrumb** fills the available width of its container and keeps all crumbs on a single line by default. Inside a flex row, set `min-width: 0` or `flex: 1` on it so the row is allowed to shrink it.
- The last item is the current page. It is rendered as text, never as a link, and carries `aria-current="page"`.
- **Breadcrumb** observes its own width and reacts immediately when the container, the viewport, or the items change.
- As long as the trail fits, every label is shown in full.
- When the trail no longer fits, labels shrink and show an ellipsis. Long labels shrink first, and no label becomes narrower than 8 characters.
- If the trail still does not fit once every label has reached its minimum width, the crumbs in the middle are hidden behind an ellipsis button, beginning with the one next to the root. The root is hidden last, and the current page is never hidden.
- The ellipsis button opens a menu that lists the hidden levels from the root downwards. Each entry is a link to that level; levels without a destination are listed but disabled.
- Set `overflow="wrap"` to let the crumbs flow onto additional lines instead of collapsing. Only a label wider than the whole container is truncated in this mode.
- Hidden crumbs come back as soon as there is enough space again. An open menu closes when its button disappears.

## Accessibility

- The trail is a `nav` landmark that contains an ordered list, so screen readers announce it as breadcrumb navigation and read the levels in order. Use `aria-label` to change the name of the landmark.
- The current page carries `aria-current="page"`.
- Separators and the ellipsis are hidden from assistive technology.
- Links can be reached with the Tab key and show a visible focus ring. The current page is plain text and cannot be focused.
- The ellipsis button is in the tab order, is named "Show n hidden levels", and announces that it opens a menu. Enter or Space opens the menu and focuses its first entry, the arrow keys move between entries, and Escape closes the menu and returns focus to the button.
- The menu entries are links, so they can also be opened in a new tab.

## Related components

- [**Link**](/components/link): for a single inline link rather than a path.
- [**Tabs**](/components/tabs): for switching between peer sections of the same page.
- [**Pagination**](/components/pagination): for moving through pages of the same list.
1 change: 1 addition & 0 deletions apps/docs/content/2.components/link.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ import { MtLink } from "@shopware-ag/meteor-component-library";
## Related components

- [**Button**](/components/button): when the user should trigger an action on the current page.
- [**Breadcrumb**](/components/breadcrumb): when the user should see and move along the path to the current page.
4 changes: 4 additions & 0 deletions apps/docs/content/2.components/pagination.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ import { MtPagination } from "@shopware-ag/meteor-component-library";
- Navigation buttons include accessible labels for first, previous, next, and last page.
- Keep **Pagination** near the content it updates so the relationship stays clear.
- Make sure page changes also update the visible content in a predictable way for keyboard and screen-reader users.

## Related components

- [**Breadcrumb**](/components/breadcrumb): when the user moves up a hierarchy instead of through pages of the same list.
1 change: 1 addition & 0 deletions apps/docs/content/2.components/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@ Lay the tabs out as a vertical list for side navigation within a section.

- Segmented Control: when its current experimental grouped-action behavior is needed, not as a general replacement for tab navigation.
- [**Button**](/components/button): when the user should trigger an action instead of navigating between peer sections.
- [**Breadcrumb**](/components/breadcrumb): when the user should move up a hierarchy instead of switching between peer sections.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<script setup lang="ts">
import { DropdownMenuItem, DropdownMenuSubTrigger } from "reka-ui";
import MtIcon from "../mt-icon/mt-icon.vue";
import { computed, inject, onMounted } from "vue";
import { computed, inject, onMounted, type Component } from "vue";
import { useI18n } from "vue-i18n";

type ModifierKey = "mod" | "ctrl" | "alt" | "shift" | "meta";
Expand Down Expand Up @@ -168,7 +168,7 @@ const props = withDefaults(
disabled?: boolean;
shortcut?: ShortcutDefinition;
isSubTrigger?: boolean;
as?: string;
as?: string | Component;
link?: string;
}>(),
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { computeCollapsed, type BreadcrumbChild } from "./mt-breadcrumb-collapse";

const ELLIPSIS = 0;
const HOME = 1;
const SEPARATOR_1 = 2;
const PRODUCTS = 3;
const SEPARATOR_2 = 4;
const SHOES = 5;
const SEPARATOR_3 = 6;
const SNEAKERS = 7;

const GAP = 8;

/**
* Ellipsis 20px, crumbs 100px, separators 10px, gaps 8px:
* - everything without the ellipsis: 478px
* - Home / … / Shoes / Sneakers: 398px
* - Home / … / Sneakers: 272px
* - … / Sneakers: 146px
*/
function trail(): BreadcrumbChild[] {
return [
{ kind: "ellipsis", width: 20 },
{ kind: "item", width: 100 },
{ kind: "separator", width: 10 },
{ kind: "item", width: 100 },
{ kind: "separator", width: 10 },
{ kind: "item", width: 100 },
{ kind: "separator", width: 10 },
{ kind: "item", width: 100 },
];
}

describe("computeCollapsed", () => {
it("hides only the ellipsis when every crumb fits", () => {
// ACT
const hidden = computeCollapsed(trail(), 478, GAP);

// ASSERT
expect(hidden).toEqual([ELLIPSIS]);
});

it("hides the crumb next to the root first and keeps the separators around the ellipsis", () => {
// ACT
const hidden = computeCollapsed(trail(), 400, GAP);

// ASSERT
expect(hidden).toEqual([PRODUCTS]);
});

it("keeps hiding middle crumbs from the root side until the trail fits", () => {
// ACT
const hidden = computeCollapsed(trail(), 300, GAP);

// ASSERT
expect(hidden.sort()).toEqual([PRODUCTS, SEPARATOR_2, SHOES].sort());
});

it("hides the root only after every middle crumb is hidden", () => {
// ACT
const hidden = computeCollapsed(trail(), 150, GAP);

// ASSERT
expect(hidden.sort()).toEqual([HOME, SEPARATOR_1, PRODUCTS, SEPARATOR_2, SHOES].sort());
});

it("never hides the last crumb, its separator, or the ellipsis, even when nothing fits", () => {
// ACT
const hidden = computeCollapsed(trail(), 10, GAP);

// ASSERT
expect(hidden).not.toContain(SNEAKERS);
expect(hidden).not.toContain(SEPARATOR_3);
expect(hidden).not.toContain(ELLIPSIS);
});

it("does not collapse a trail with a single crumb", () => {
// ARRANGE
const children: BreadcrumbChild[] = [
{ kind: "ellipsis", width: 20 },
{ kind: "item", width: 500 },
];

// ACT
const hidden = computeCollapsed(children, 100, GAP);

// ASSERT
expect(hidden).toEqual([ELLIPSIS]);
});
});
Loading
Loading