GrBreadcrumbs
Берут, когда вложенность реальна.
Обзор
Когда брать
- вложенность реальна — каталог, файловая система, разделы документации: путь показывает, где пользователь;
- путь длинный —
maxItemsиautoCollapseсворачивают середину, оставляя начало и конец; - на любой уровень можно вернуться — каждый предок ссылка, последний пункт — текущая страница;
- роутер уже подключён — ссылки рисует
GrLink, поэтому способ навигации тот же, что везде.
Когда взять другое
| Нужно | Берите |
|---|---|
| Шаги процесса, а не уровни | GrTimeline |
| Разделы одного уровня | GrTabs |
| Основная навигация приложения | GrSidebar / GrNavbar |
| Один переход назад | GrButton / GrLink |
Плоский сайт хлебных крошек не требует: путь из одного уровня ничего не сообщает, а место занимает. Они окупаются там, где пользователь может оказаться на четвёртом уровне и не помнить дороги.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrBreadcrumbs :items="items" />Примеры 3 у каждого — живое демо и его код
Родители над заголовком в консоли приложения
Страница называет себя в заголовке, поэтому в пути только её родители:
current-index="-1"не отдаётaria-currentникому. Состоянием владеет роутер — у пунктовto,asрисует их компонентом ссылки приложения. Спуститесь на несколько уровней, и середина свернётся поmax-items; многоточие с подписью изexpand-labelраскрывает её на месте. Организация сdisabledостаётся в пути текстом, домик —iconOnly, а подпись при нём остаётся диктору, и «Environments» получаетariaLabelс именем проекта.VariablesDATABASE_URL = ••••••••STRIPE_KEY = ••••••••LOG_LEVEL = infoГлубокий путь в узкой панели
Число пунктов плохо предсказывает ширину: три коротких папки влезают, две длинных — нет.
auto-collapseмеряет: путь остаётся в одной строке, а середина сворачивается ровно настолько, насколько панели не хватает, и никогда — до последнего пункта. Слотellipsisговорит, сколько папок он прячет; ссылки ведут в настоящее дерево исходников.Текущая группа остаётся ссылкой
link-currentоставляет последний пункт кликабельным — здесь он открывает группу заново, без фильтра, — аaria-current="page"на нём сохраняется. Слотitemставит рядом с именем размер группы, аseparator="›"читается как шаг вниз, а не как часть имени.
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Кегль пути и его ссылок — шкала общая с `GrLink`. По умолчанию `md`; берётся из `GrConfigProvider`. |
ariaLabel | string | undefined | undefined | Имя лендмарка. Не задано — берётся из локали. |
as | GrBreadcrumbsLinkComponent | undefined | undefined | Компонент ссылки для всех пунктов: `RouterLink`, `NuxtLink`, `Link` Inertia. |
separator | string | undefined | undefined | Разделитель между пунктами. Декоративен: диктору не читается. |
maxItems | number | undefined | undefined | С какого числа пунктов схлопывать середину в «…». `0`/не задано — показывать путь целиком. |
itemsBeforeCollapse | number | undefined | 1 | Сколько пунктов оставить в начале при схлопывании. |
itemsAfterCollapse | number | undefined | 1 | Сколько пунктов оставить в конце при схлопывании. |
linkCurrent | boolean | undefined | false | Последний пункт остаётся ссылкой (текущая страница кликабельна). |
currentIndex | number | undefined | undefined | Какой пункт — текущая страница. Не задан — последний. `-1` означает, что текущей страницы в пути нет: её имя стоит в `h1`, а путь показывает только родителей. Тогда `aria-current` не получает никто, и последний пункт остаётся обычной ссылкой. |
expandLabel | string | undefined | undefined | i18n-метка кнопки раскрытия схлопнутого пути. |
autoCollapse | boolean | undefined | false | Схлопывать по доступной ширине, а не по числу пунктов. Путь становится однострочным, середина уходит под «…» ровно настолько, насколько не влезает. Без пропа поведение прежнее: список переносится на вторую строку, а порог задаёт `maxItems`. Вместе пропы совместимы — `maxItems` остаётся жёстким потолком, ширина ужимает дальше. |
itemsобязательный | GrBreadcrumbItem[] | — | Путь от корня к текущей странице. Последний пункт — текущая страница. |
Слоты
| Slot | Type | Описание |
|---|---|---|
item | { item: GrBreadcrumbItem; index: number; isCurrent: boolean; } | Содержимое пункта целиком (иконка и подпись). |
separator | any | Разделитель между пунктами. |
ellipsis | { hiddenCount: number; expand: () => void; } | Кнопка раскрытия схлопнутой середины. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrBreadcrumbsConfigurableProps, GrBreadcrumbsSize, GrBreadcrumbItem } from '@feugene/granularity/components/GrBreadcrumbs'Файлdist/types/components/GrBreadcrumbs/config.d.ts
const grBreadcrumbsConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrBreadcrumbs">;Файлdist/types/components/GrBreadcrumbs/defaults.d.ts
Пропы GrBreadcrumbs, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrBreadcrumbs: { … } }">.
Только оформление — см. GrButton/defaults.ts.
interface GrBreadcrumbsConfigurableProps {
size: GrBreadcrumbsSize;
separator: string;
}Файлdist/types/components/GrBreadcrumbs/grBreadcrumbsStyles.d.ts
type GrBreadcrumbsSize = GrComponentSize;Пункт пути. Кроме label всё опционально: последний пункт обычно без ссылки.
type GrBreadcrumbItem = {
label: string;
href?: string;
/** Цель роутерной ссылки: уезжает в `GrLink` через `attrs` вместе с `as`. */
to?: unknown;
/**
* Иконка перед подписью — декоративна. Vue-компонент либо класс иконки
* (`i-lucide-*` — правило для класса заводит приложение: фабрике движка или
* провайдером, см. `docs/installation.md#иконки`).
*/
icon?: string | Component;
/**
* Показывать только иконку: подпись прячется визуально, но остаётся в
* разметке и в дереве доступности. Классический «домик» в начале пути.
*
* Подпись не выбрасывается и не подменяется на `aria-label` намеренно: путь
* читают и поиском, и диктором, и пункт без имени для незрячего пустой.
* Без `icon` поле игнорируется — прятать подпись, ничего не показав взамен,
* значит стереть пункт.
*/
iconOnly?: boolean;
disabled?: boolean;
/** Доступное имя пункта, когда подписи недостаточно («Настройки» → «Настройки проекта»). */
ariaLabel?: string;
};Раскладка пути: какие пункты видны и где стоит кнопка раскрытия. Чистая функция, потому что вся её сложность — арифметика границ, и проверять её монтированием компонента дороже, чем таблицей входов.
type GrBreadcrumbsLayoutEntry = {
kind: 'item';
item: GrBreadcrumbItem;
index: number;
} | {
kind: 'ellipsis';
hiddenCount: number;
};Сколько пунктов хвоста влезает в доступную ширину.
Порог по числу пунктов (maxItems) на узком экране предсказывает плохо: три
коротких пункта влезают, два длинных — нет. Здесь решение принимается по
измеренным ширинам.
Голова не ужимается: itemsBeforeCollapse — обычно один корневой пункт, и он
дёшев. Хвост ужимается до упора, но **не меньше одного**: последний пункт
отвечает на вопрос «где я сейчас», и прятать его бессмысленно — путь
превратился бы в «Главная / …».
Возврат — число пунктов хвоста; items.length означает «влезло всё».
function resolveBreadcrumbsFit(options: {
itemWidths: number[];
separatorWidth: number;
ellipsisWidth: number;
available: number;
itemsBeforeCollapse: number;
/** Зазор списка (`gap`). Стоит между каждой парой соседей, включая разделители. */
gapWidth?: number;
}): number;function resolveBreadcrumbsLayout(options: {
items: GrBreadcrumbItem[];
maxItems?: number;
itemsBeforeCollapse: number;
itemsAfterCollapse: number;
expanded: boolean;
}): GrBreadcrumbsLayoutEntry[];Файлdist/types/components/GrBreadcrumbs/GrBreadcrumbs.vue.d.ts
Путь до текущей страницы (WAI-ARIA breadcrumb): <nav> с именем, внутри
упорядоченный список ссылок, последний пункт — текущая страница.
Ссылки рисует GrLink, поэтому роутер подключается тем же способом, что и
везде в пакете: as + to.
interface GrBreadcrumbsProps {
/** Путь от корня к текущей странице. Последний пункт — текущая страница. */
items: GrBreadcrumbItem[];
/** Компонент ссылки для всех пунктов: `RouterLink`, `NuxtLink`, `Link` Inertia. */
as?: GrBreadcrumbsLinkComponent;
/** Разделитель между пунктами. Декоративен: диктору не читается. */
separator?: string;
/** Кегль пути и его ссылок — шкала общая с `GrLink`. По умолчанию `md`; берётся из `GrConfigProvider`. */
size?: GrBreadcrumbsSize;
/**
* С какого числа пунктов схлопывать середину в «…». `0`/не задано — показывать
* путь целиком.
*/
maxItems?: number;
/** Сколько пунктов оставить в начале при схлопывании. */
itemsBeforeCollapse?: number;
/** Сколько пунктов оставить в конце при схлопывании. */
itemsAfterCollapse?: number;
/** Имя лендмарка. Не задано — берётся из локали. */
ariaLabel?: string;
/** Последний пункт остаётся ссылкой (текущая страница кликабельна). */
linkCurrent?: boolean;
/**
* Какой пункт — текущая страница. Не задан — последний.
*
* `-1` означает, что текущей страницы в пути нет: её имя стоит в `h1`, а
* путь показывает только родителей. Тогда `aria-current` не получает никто,
* и последний пункт остаётся обычной ссылкой.
*/
currentIndex?: number;
/** i18n-метка кнопки раскрытия схлопнутого пути. */
expandLabel?: string;
/**
* Схлопывать по доступной ширине, а не по числу пунктов. Путь становится
* однострочным, середина уходит под «…» ровно настолько, насколько не влезает.
*
* Без пропа поведение прежнее: список переносится на вторую строку, а порог
* задаёт `maxItems`. Вместе пропы совместимы — `maxItems` остаётся жёстким
* потолком, ширина ужимает дальше.
*/
autoCollapse?: boolean;
}Файлdist/types/components/GrBreadcrumbs/safelist.d.ts
const grBreadcrumbsSafelist: string[];Доступность
Паттерн APG
breadcrumb
| Клавиша | Действие |
|---|---|
| — | Tab по ссылкам пути; кнопка «…» — обычная кнопка (Enter/Space), после раскрытия фокус переезжает на первый раскрытый пункт |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Роутер
<GrBreadcrumbs :as="RouterLink" :items="items" />const items = [
{ label: 'Проекты', to: '/projects' },
{ label: 'Гранулярность', to: '/projects/granularity' },
{ label: 'Настройки' },
]as задаёт компонент ссылки на все пункты сразу, to уезжает в него пропом
через attrs GrLink. Без as пункт со href рендерится обычным <a>.
Текущая страница
Последний пункт — не ссылка и объявлен aria-current="page": именно этот
атрибут отвечает диктору на вопрос «где я». Если текущая страница должна
оставаться кликабельной (перезагрузка раздела, якорь), включите linkCurrent —
aria-current при этом сохраняется.
Промежуточный пункт можно выключить (disabled: true) — он станет текстом, но
aria-current не получит. Акцента текущей страницы у него тоже нет: он
приглушён и обычного веса, как соседние ссылки, — иначе выделенный предок в
начале пути читался бы как «вы здесь». Так же выглядит промежуточный пункт без
href и to.
Длинный путь
<GrBreadcrumbs :items="path" :max-items="4" :items-before-collapse="1" :items-after-collapse="2" />Середина сворачивается в кнопку «…», по краям остаётся столько уровней, сколько
просят itemsBeforeCollapse/itemsAfterCollapse. Хвост важнее головы: «где я
сейчас» читается справа, поэтому при нехватке места жертвуют серединой.
Прятать один пункт компонент не станет — кнопка заняла бы столько же места.
Клик по многоточию раскрывает путь на месте, без выпадающего меню. Кнопка
при этом исчезает, поэтому фокус переводится на первый раскрытый пункт: иначе он
уехал бы на <body>. Смена items (новая страница) снова схлопывает путь.
Раскладку можно посчитать и снаружи — resolveBreadcrumbsLayout экспортируется
как чистая функция.
Схлопывание по доступному месту
<GrBreadcrumbs :items="path" auto-collapse />maxItems считает пункты, а на узком экране число — плохой предиктор: три
коротких уровня влезают, два длинных — нет. autoCollapse меряет: путь
становится однострочным, и середина уходит под «…» ровно настолько,
насколько не влезает.
Что меняется вместе с режимом:
| без пропа | autoCollapse | |
|---|---|---|
| длинный путь | переносится на вторую строку | остаётся в одной строке |
| порог | maxItems (число пунктов) | ширина контейнера |
| хвост | itemsAfterCollapse | столько, сколько влезло, но не меньше одного |
Голова не ужимается: itemsBeforeCollapse — обычно корневой пункт, и он дёшев.
Хвост не ужимается ниже одного пункта: последний отвечает на вопрос «где я
сейчас», и путь «Главная / …» бесполезен.
Когда не влезает даже «голова, …, последний», свою ширину держат голова, разделители и «…», а подпись последнего пункта обрезается многоточием. Ужимать всех поровну нельзя: ссылка головы не обрезается и вылезла бы из своего пункта на зазор перед разделителем.
Пропы совместимы: maxItems остаётся жёстким потолком, ширина ужимает дальше.
Первый кадр в этом режиме — полный путь: ширины подписей снимаются с него, а
взять их до рендера неоткуда. Дальше пересчёт идёт по ResizeObserver и стоит
одну арифметическую операцию — подписи от ширины контейнера не зависят.
Арифметика вынесена отдельно: resolveBreadcrumbsFit экспортируется как чистая
функция, если решение нужно принять снаружи.
Разделитель и размер
separator (по умолчанию /) и size читаются из GrConfigProvider:
<GrConfigProvider :component-defaults="{ GrBreadcrumbs: { separator: '›' } }">Разделитель декоративен — он в отдельном элементе списка с aria-hidden:
структуру пути диктору сообщает сам список, а «/» он бы читал вслух.
Иконка пункта
Поле icon декоративно (aria-hidden) и принимает Vue-компонент либо класс
иконки (i-lucide-* — правило для него заводит приложение: фабрике движка или
провайдером, иначе — Vue-компонент; см. «Иконки»).
iconOnly показывает только иконку — классический «домик» в начале пути:
<GrBreadcrumbs :items="[{ label: 'Главная', href: '/', icon: 'i-lucide-house', iconOnly: true }, …]" />Подпись при этом не выбрасывается и не переезжает в aria-label, а прячется
sr-only. Причина в том, кто читает путь: поисковик и диктор читают его текстом,
и пункт без имени для незрячего пустой — он слышит «ссылка», не зная куда.
Без icon поле игнорируется, а в разработке компонент предупреждает: спрятать
подпись, не показав ничего взамен, значит стереть пункт — в разметке при этом всё
на месте, и пропажу видно только глазами.
Слоты
| Слот | Что заменяет |
|---|---|
item | содержимое пункта (item, index, isCurrent) |
separator | разделитель |
ellipsis | кнопку раскрытия (hiddenCount, expand) |
Пропы этого примера