GrBreadcrumbs

  • ядро
  • Навигация
  • API · 12 props
  • ⌨ клавиши

Берут, когда вложенность реальна.

import { GrBreadcrumbs } from '@feugene/granularity/components/GrBreadcrumbs'

Обзор

Когда брать

  • вложенность реальна — каталог, файловая система, разделы документации: путь показывает, где пользователь;
  • путь длинный — maxItems и autoCollapse сворачивают середину, оставляя начало и конец;
  • на любой уровень можно вернуться — каждый предок ссылка, последний пункт — текущая страница;
  • роутер уже подключён — ссылки рисует GrLink, поэтому способ навигации тот же, что везде.

Когда взять другое

НужноБерите
Шаги процесса, а не уровниGrTimeline
Разделы одного уровняGrTabs
Основная навигация приложенияGrSidebar / GrNavbar
Один переход назадGrButton / GrLink

Плоский сайт хлебных крошек не требует: путь из одного уровня ничего не сообщает, а место занимает. Они окупаются там, где пользователь может оказаться на четвёртом уровне и не помнить дороги.

Playground контролы выводятся из API, а не пишутся руками

Сцена

Загружается…

0 изменено· состояние — в адресе

Примеры 3 у каждого — живое демо и его код

  • Родители над заголовком в консоли приложения

    Страница называет себя в заголовке, поэтому в пути только её родители: current-index="-1" не отдаёт aria-current никому. Состоянием владеет роутер — у пунктов to, as рисует их компонентом ссылки приложения. Спуститесь на несколько уровней, и середина свернётся по max-items; многоточие с подписью из expand-label раскрывает её на месте. Организация с disabled остаётся в пути текстом, домик — iconOnly, а подпись при нём остаётся диктору, и «Environments» получает ariaLabel с именем проекта.

    Variables
    DATABASE_URL = ••••••••STRIPE_KEY = ••••••••LOG_LEVEL = info
  • Глубокий путь в узкой панели

    Число пунктов плохо предсказывает ширину: три коротких папки влезают, две длинных — нет. auto-collapse меряет: путь остаётся в одной строке, а середина сворачивается ровно настолько, насколько панели не хватает, и никогда — до последнего пункта. Слот ellipsis говорит, сколько папок он прячет; ссылки ведут в настоящее дерево исходников.

  • Текущая группа остаётся ссылкой

    link-current оставляет последний пункт кликабельным — здесь он открывает группу заново, без фильтра, — а aria-current="page" на нём сохраняется. Слот item ставит рядом с именем размер группы, а separator="›" читается как шаг вниз, а не как часть имени.

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
size"xs" | "sm" | "md" | "lg" | undefinedundefinedКегль пути и его ссылок — шкала общая с `GrLink`. По умолчанию `md`; берётся из `GrConfigProvider`.
ariaLabelstring | undefinedundefinedИмя лендмарка. Не задано — берётся из локали.
asGrBreadcrumbsLinkComponent | undefinedundefinedКомпонент ссылки для всех пунктов: `RouterLink`, `NuxtLink`, `Link` Inertia.
separatorstring | undefinedundefinedРазделитель между пунктами. Декоративен: диктору не читается.
maxItemsnumber | undefinedundefinedС какого числа пунктов схлопывать середину в «…». `0`/не задано — показывать путь целиком.
itemsBeforeCollapsenumber | undefined1Сколько пунктов оставить в начале при схлопывании.
itemsAfterCollapsenumber | undefined1Сколько пунктов оставить в конце при схлопывании.
linkCurrentboolean | undefinedfalseПоследний пункт остаётся ссылкой (текущая страница кликабельна).
currentIndexnumber | undefinedundefinedКакой пункт — текущая страница. Не задан — последний. `-1` означает, что текущей страницы в пути нет: её имя стоит в `h1`, а путь показывает только родителей. Тогда `aria-current` не получает никто, и последний пункт остаётся обычной ссылкой.
expandLabelstring | undefinedundefinedi18n-метка кнопки раскрытия схлопнутого пути.
autoCollapseboolean | undefinedfalseСхлопывать по доступной ширине, а не по числу пунктов. Путь становится однострочным, середина уходит под «…» ровно настолько, насколько не влезает. Без пропа поведение прежнее: список переносится на вторую строку, а порог задаёт `maxItems`. Вместе пропы совместимы — `maxItems` остаётся жёстким потолком, ширина ужимает дальше.
itemsобязательныйGrBreadcrumbItem[]—Путь от корня к текущей странице. Последний пункт — текущая страница.

Слоты

SlotTypeОписание
item{ item: GrBreadcrumbItem; index: number; isCurrent: boolean; }Содержимое пункта целиком (иконка и подпись).
separatoranyРазделитель между пунктами.
ellipsis{ hiddenCount: number; expand: () => void; }Кнопка раскрытия схлопнутой середины.

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { GrBreadcrumbsConfigurableProps, GrBreadcrumbsSize, GrBreadcrumbItem } from '@feugene/granularity/components/GrBreadcrumbs'

Файлdist/types/components/GrBreadcrumbs/config.d.ts

grBreadcrumbsConfigconst
const grBreadcrumbsConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrBreadcrumbs">;

Файлdist/types/components/GrBreadcrumbs/defaults.d.ts

GrBreadcrumbsConfigurablePropsinterface

Пропы GrBreadcrumbs, настраиваемые глобально через <GrConfigProvider :component-defaults="{ GrBreadcrumbs: { … } }">. Только оформление — см. GrButton/defaults.ts.

interface GrBreadcrumbsConfigurableProps {
    size: GrBreadcrumbsSize;
    separator: string;
}

Файлdist/types/components/GrBreadcrumbs/grBreadcrumbsStyles.d.ts

GrBreadcrumbsSizetype
type GrBreadcrumbsSize = GrComponentSize;
GrBreadcrumbItemtype

Пункт пути. Кроме 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;
};
GrBreadcrumbsLayoutEntrytype

Раскладка пути: какие пункты видны и где стоит кнопка раскрытия. Чистая функция, потому что вся её сложность — арифметика границ, и проверять её монтированием компонента дороже, чем таблицей входов.

type GrBreadcrumbsLayoutEntry = {
    kind: 'item';
    item: GrBreadcrumbItem;
    index: number;
} | {
    kind: 'ellipsis';
    hiddenCount: number;
};
resolveBreadcrumbsFitfunction

Сколько пунктов хвоста влезает в доступную ширину. Порог по числу пунктов (maxItems) на узком экране предсказывает плохо: три коротких пункта влезают, два длинных — нет. Здесь решение принимается по измеренным ширинам. Голова не ужимается: itemsBeforeCollapse — обычно один корневой пункт, и он дёшев. Хвост ужимается до упора, но **не меньше одного**: последний пункт отвечает на вопрос «где я сейчас», и прятать его бессмысленно — путь превратился бы в «Главная / …». Возврат — число пунктов хвоста; items.length означает «влезло всё».

function resolveBreadcrumbsFit(options: {
    itemWidths: number[];
    separatorWidth: number;
    ellipsisWidth: number;
    available: number;
    itemsBeforeCollapse: number;
    /** Зазор списка (`gap`). Стоит между каждой парой соседей, включая разделители. */
    gapWidth?: number;
}): number;
resolveBreadcrumbsLayoutfunction
function resolveBreadcrumbsLayout(options: {
    items: GrBreadcrumbItem[];
    maxItems?: number;
    itemsBeforeCollapse: number;
    itemsAfterCollapse: number;
    expanded: boolean;
}): GrBreadcrumbsLayoutEntry[];

Файлdist/types/components/GrBreadcrumbs/GrBreadcrumbs.vue.d.ts

GrBreadcrumbsPropsinterface

Путь до текущей страницы (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

grBreadcrumbsSafelistconst
const grBreadcrumbsSafelist: string[];

Доступность

Паттерн APG

breadcrumb

КлавишаДействие
—Tab по ссылкам пути; кнопка «…» — обычная кнопка (Enter/Space), после раскрытия фокус переезжает на первый раскрытый пункт

Использование из прозы библиотеки

Роутер
vue
<GrBreadcrumbs :as="RouterLink" :items="items" />
ts
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.

Длинный путь
vue
<GrBreadcrumbs :items="path" :max-items="4" :items-before-collapse="1" :items-after-collapse="2" />

Середина сворачивается в кнопку «…», по краям остаётся столько уровней, сколько просят itemsBeforeCollapse/itemsAfterCollapse. Хвост важнее головы: «где я сейчас» читается справа, поэтому при нехватке места жертвуют серединой.

Прятать один пункт компонент не станет — кнопка заняла бы столько же места.

Клик по многоточию раскрывает путь на месте, без выпадающего меню. Кнопка при этом исчезает, поэтому фокус переводится на первый раскрытый пункт: иначе он уехал бы на <body>. Смена items (новая страница) снова схлопывает путь.

Раскладку можно посчитать и снаружи — resolveBreadcrumbsLayout экспортируется как чистая функция.

Схлопывание по доступному месту
vue
<GrBreadcrumbs :items="path" auto-collapse />

maxItems считает пункты, а на узком экране число — плохой предиктор: три коротких уровня влезают, два длинных — нет. autoCollapse меряет: путь становится однострочным, и середина уходит под «…» ровно настолько, насколько не влезает.

Что меняется вместе с режимом:

без пропаautoCollapse
длинный путьпереносится на вторую строкуостаётся в одной строке
порогmaxItems (число пунктов)ширина контейнера
хвостitemsAfterCollapseстолько, сколько влезло, но не меньше одного

Голова не ужимается: itemsBeforeCollapse — обычно корневой пункт, и он дёшев. Хвост не ужимается ниже одного пункта: последний отвечает на вопрос «где я сейчас», и путь «Главная / …» бесполезен.

Когда не влезает даже «голова, …, последний», свою ширину держат голова, разделители и «…», а подпись последнего пункта обрезается многоточием. Ужимать всех поровну нельзя: ссылка головы не обрезается и вылезла бы из своего пункта на зазор перед разделителем.

Пропы совместимы: maxItems остаётся жёстким потолком, ширина ужимает дальше.

Первый кадр в этом режиме — полный путь: ширины подписей снимаются с него, а взять их до рендера неоткуда. Дальше пересчёт идёт по ResizeObserver и стоит одну арифметическую операцию — подписи от ширины контейнера не зависят.

Арифметика вынесена отдельно: resolveBreadcrumbsFit экспортируется как чистая функция, если решение нужно принять снаружи.

Разделитель и размер

separator (по умолчанию /) и size читаются из GrConfigProvider:

vue
<GrConfigProvider :component-defaults="{ GrBreadcrumbs: { separator: '›' } }">

Разделитель декоративен — он в отдельном элементе списка с aria-hidden: структуру пути диктору сообщает сам список, а «/» он бы читал вслух.

Иконка пункта

Поле icon декоративно (aria-hidden) и принимает Vue-компонент либо класс иконки (i-lucide-* — правило для него заводит приложение: фабрике движка или провайдером, иначе — Vue-компонент; см. «Иконки»).

iconOnly показывает только иконку — классический «домик» в начале пути:

vue
<GrBreadcrumbs :items="[{ label: 'Главная', href: '/', icon: 'i-lucide-house', iconOnly: true }, …]" />

Подпись при этом не выбрасывается и не переезжает в aria-label, а прячется sr-only. Причина в том, кто читает путь: поисковик и диктор читают его текстом, и пункт без имени для незрячего пустой — он слышит «ссылка», не зная куда.

Без icon поле игнорируется, а в разработке компонент предупреждает: спрятать подпись, не показав ничего взамен, значит стереть пункт — в разметке при этом всё на месте, и пропажу видно только глазами.

Слоты
СлотЧто заменяет
itemсодержимое пункта (item, index, isCurrent)
separatorразделитель
ellipsisкнопку раскрытия (hiddenCount, expand)