GrCollapse
Берут, когда содержимого много, а нужно не всё сразу.
Обзор
Когда брать
- содержимого много, а нужно не всё сразу — вопросы и ответы, настройки, детали записи;
- открыта одна секция за раз —
accordionзакрывает предыдущую; - секции нужны незрячему —
headingLevelставит заголовок нужного уровня, и обход по заголовкам работает; - открытие надо перехватить —
beforeChangeоткладывает раскрытие до загрузки или подтверждения.
Когда взять другое
| Нужно | Берите |
|---|---|
| Разделы переключаются, а не раскрываются | GrTabs |
| Разделы формы идут подряд | GrFormSection |
| Данные вложены | GrTree |
| Содержимое открывается поверх страницы | GrDialog / GrDrawer |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrCollapse emptyText="No sections yet" v-model="value">
<GrCollapseItem name="billing" title="Billing"><span class="text-[var(--gr-muted-fg)]">Invoices go out on the 1st; the card ending 4242 is charged the same day.</span></GrCollapseItem>
<GrCollapseItem name="tax" title="Tax details"><span class="text-[var(--gr-muted-fg)]">VAT DE 293 847 561 · reverse charge applies.</span></GrCollapseItem>
<GrCollapseItem name="contacts" title="Billing contacts"><span class="text-[var(--gr-muted-fg)]">[email protected], [email protected]</span></GrCollapseItem>
</GrCollapse>Примеры 4 у каждого — живое демо и его код
Вопросы о счетах, открытых несколько
Модель — массив имён, поэтому сравниваемые ответы остаются открытыми вместе. Заголовок страницы —
h2, аheading-levelделает вопросыh3: скринридер обходит их по структуре, а триггер остаётся кнопкой внутри заголовка, как требует шаблон APG.Billing questions
You pay for the new seats only for the days left in the billing period. The amount appears as a separate line on the next invoice, called «Proration».
We charge VAT when the company has no valid EU VAT number on file. Add the number in Settings → Billing, and the next invoice will be issued with reverse charge.
Visa, Mastercard and American Express, SEPA Direct Debit for euro accounts, and bank transfer on the yearly plan.
Monthly plans are not refunded for the current month. Yearly plans are refunded pro rata within the first 30 days.
Заказ по одной секции
accordionдержит в модели одно имя, поэтому открытие «Оплаты» сворачивает «Товары», и итог остаётся на виду.borderlessубирает вторую рамку внутри карточки,dividedрисует разделители, а слотextraставит бейдж рядом с триггером, а не внутри.Order #4815€312.40 3- Halden desk, oak€249.00
- Desk lamp, black€39.90
- Cable tray, 80 cm€23.50
Tomorrow12 Harbour Street, 20457 Hamburg · DHL, before 12:00PaidVisa ending 4242 · 6 Oct, 09:14Панель фильтров, которая считает включённое
Каждая группа показывает в
extra, сколько её фильтров включено, поэтому свёрнутая секция всё равно говорит правду.expand-icon-position="start"ставит шеврон туда, где начинается список, аdisabled-секция объясняет себя заголовком.Секция, которая не свернётся с несохранённым
before-changeполучает секцию и направление. Измените название компании и попробуйте свернуть секцию — guard откажет и скажет почему. Откройте «Историю счетов» в первый раз — guard дождётся данных, прежде чем секция раскроется, а повторный клик тем временем игнорируется.Exports all invoices, then deletes the workspace after 30 days.
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
disabled | boolean | undefined | false | Блокирует все секции: заголовки не переключаются. |
modelValuev-model | GrCollapseValue | GrCollapseValue[] | undefined | undefined | Открытые секции (`v-model`): в `accordion` — одно значение или `undefined`, иначе массив. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Размер секций. Не задан — берётся из `GrConfigProvider`, иначе `md`. |
headingLevel | 2 | 3 | 4 | 5 | 6 | undefined | undefined | Уровень заголовков секций (`h2`…`h6`) — под структуру страницы. |
accordion | boolean | undefined | false | Одновременно открыта только одна секция: открытие новой закрывает прежнюю. |
divided | boolean | undefined | undefined | Разделители между секциями. По умолчанию `true`; глобально — `componentDefaults`. |
borderless | boolean | undefined | undefined | Без обёртки в `GrCard`: аккордеон внутри карточки или сайдбара иначе получает вторую рамку и вторую тень. По умолчанию `false`. |
expandIconPosition | "end" | "start" | undefined | undefined | Сторона шеврона относительно заголовка. |
beforeChange | GrCollapseBeforeChange | undefined | undefined | Async-guard переключения: получает секцию и направление, `false` отменяет. Пока он не ответил, повторный клик по той же секции игнорируется. |
empty | boolean | undefined | undefined | Пусто ли. Не задан — считается по содержимому: аккордеон без секций показывает заглушку вместо пустой рамки. `false` подавляет автоопределение — например, когда секции приезжают асинхронно и мигать текстом не надо. |
emptyText | string | undefined | undefined | Текст пустого состояния. Слот `#empty` сильнее. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | any | Секции аккордеона (`GrCollapseItem`). |
empty | any | Содержимое пустого состояния вместо текста по умолчанию. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: GrCollapseModelValue): void | — |
change | (event: "change", value: GrCollapseModelValue): void | — |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrCollapseProps, GrCollapseEmits, GrCollapseItemProps } from '@feugene/granularity/components/GrCollapse'Файлdist/types/components/GrCollapse/GrCollapse.vue.d.ts
GrCollapse — контейнер секций GrCollapseItem.
interface GrCollapseProps {
/** Открытые секции (`v-model`): в `accordion` — одно значение или `undefined`, иначе массив. */
modelValue?: GrCollapseModelValue;
/** Одновременно открыта только одна секция: открытие новой закрывает прежнюю. */
accordion?: boolean;
/** Блокирует все секции: заголовки не переключаются. */
disabled?: boolean;
/** Разделители между секциями. По умолчанию `true`; глобально — `componentDefaults`. */
divided?: boolean;
/**
* Без обёртки в `GrCard`: аккордеон внутри карточки или сайдбара иначе получает вторую рамку и
* вторую тень. По умолчанию `false`.
*/
borderless?: boolean;
/** Уровень заголовков секций (`h2`…`h6`) — под структуру страницы. */
headingLevel?: GrCollapseHeadingLevel;
/** Сторона шеврона относительно заголовка. */
expandIconPosition?: GrCollapseIconPosition;
/**
* Async-guard переключения: получает секцию и направление, `false` отменяет. Пока он не
* ответил, повторный клик по той же секции игнорируется.
*/
beforeChange?: GrCollapseBeforeChange;
/** Размер секций. Не задан — берётся из `GrConfigProvider`, иначе `md`. */
size?: GrComponentSize;
/**
* Пусто ли. Не задан — считается по содержимому: аккордеон без секций
* показывает заглушку вместо пустой рамки. `false` подавляет автоопределение —
* например, когда секции приезжают асинхронно и мигать текстом не надо.
*/
empty?: boolean;
/** Текст пустого состояния. Слот `#empty` сильнее. */
emptyText?: string;
}interface GrCollapseEmits {
/** Новые открытые секции (`v-model`): значение или `undefined` в `accordion`, иначе массив. */
(e: 'update:modelValue', value: GrCollapseModelValue): void;
/** Пользователь открыл или закрыл секцию — уже после `beforeChange`. */
(e: 'change', value: GrCollapseModelValue): void;
}Файлdist/types/components/GrCollapse/GrCollapseItem.vue.d.ts
GrCollapseItem — одна секция GrCollapse. Должна использоваться строго внутри GrCollapse.
@prop name — уникальный идентификатор секции (для v-model у GrCollapse); по умолчанию генерируется из uid.
@prop title — текст заголовка; можно заменить слотом #title.
@prop disabled — запрещает раскрытие/сворачивание секции.
Слот #extra (счётчик, бейдж, кнопка) рендерится **рядом с** триггером, а не
внутри него: <button> внутри <button> — невалидная разметка, а axe ловит
это как nested-interactive.
interface GrCollapseItemProps {
name?: GrCollapseValue;
title?: string;
disabled?: boolean;
}Файлdist/types/components/GrCollapse/config.d.ts
const grCollapseConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCollapse">;Файлdist/types/components/GrCollapse/defaults.d.ts
Пропы GrCollapse, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrCollapse: { … } }">.
Только оформление — см. GrButton/defaults.ts.
interface GrCollapseConfigurableProps {
size: GrComponentSize;
divided: boolean;
borderless: boolean;
expandIconPosition: GrCollapseIconPosition;
headingLevel: GrCollapseHeadingLevel;
}Файлdist/types/components/GrCollapse/safelist.d.ts
const grCollapseSafelist: string[];Доступность
Паттерн APG
accordion
Клавиши
Space / Enter↓ / ↑Home / End
| Клавиша | Действие |
|---|---|
| Space / Enter | раскрыть-свернуть |
| ↓ / ↑ | к соседнему заголовку (по кругу) |
| Home / End | к первому/последнему. Стрелки ходят только по заголовкам своего аккордеона: вложенный GrCollapse в них не попадает. Содержимое свёрнутой секции inert: Tab в него не заходит |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Уровень заголовка
Заголовок секции рендерится тегом h3, а headingLevel подгоняет его под
структуру страницы: в разделе <h4> аккордеон обязан начинаться с h5, иначе
навигация по заголовкам получает разрыв уровней. Кнопка остаётся внутри
заголовка — этого требует APG для accordion.
Поверхность
borderless убирает обёртку в GrCard: аккордеон внутри карточки, сайдбара или
панели фильтров иначе получает вторую рамку и вторую тень. divided управляет
разделителями между секциями.
<GrCollapse v-model="open" borderless :heading-level="5" expand-icon-position="start">
<GrCollapseItem name="filters" title="Фильтры">
<template #extra>
<GrBadge size="sm">3</GrBadge>
</template>
…
</GrCollapseItem>
</GrCollapse>Слот #icon заменяет шеврон, expandIconPosition переставляет его перед
заголовком. Слот #extra (счётчик, бейдж, кнопка) рендерится рядом с
триггером, а не внутри: <button> в <button> — невалидная разметка, и axe
ловит её как nested-interactive.
Guard на переключение
beforeChange(name, expanding) отменяет переключение, вернув false. Второй
аргумент — куда идёт секция, чтобы «сохранить изменения?» спрашивалось только на
сворачивании. Пока guard не ответил, повторный клик по тому же заголовку
игнорируется: иначе два подтверждения подряд вернули бы состояние к исходному.
async function beforeChange(name: GrCollapseValue, expanding: boolean): Promise<boolean> {
if (expanding)
return true
return confirmDiscardChanges(name)
}Клавиатура и вложенность
Стрелки ↑/↓ (по кругу) и Home/End ходят только по заголовкам своего
аккордеона: вложенный GrCollapse внутри раскрытой панели в обход не попадает.
Свёрнутая панель помечена inert — ни Tab, ни скринридер в неё не заходят,
при этом (в отличие от hidden) анимация раскрытия сохраняется.
Пустое состояние
<GrCollapse>
<GrCollapseItem v-for="item in filtered" :key="item.name" v-bind="item" />
</GrCollapse>Фильтр ничего не нашёл — аккордеон сам покажет текст вместо пустой рамки: рамка без содержимого читается как поломка, а не как «пока пусто».
Пустоту считает содержимое слота, а не длина ваших данных, поэтому работает и
v-for по пустому массиву, и v-if, ничего не отрисовавший. Комментарии,
которые оставляет после себя v-if, и переносы строк из шаблона содержимым не
считаются — иначе заглушка не появилась бы никогда.
Текст берётся из локали (gr.collapse.empty). Перебить его можно двумя
способами, от простого к общему:
<GrCollapse empty-text="Нет разделов" />
<GrCollapse>
<template #empty>
<GrEmptyState title="Ничего не найдено" description="Смягчите фильтр" />
</template>
</GrCollapse>:empty="false" подавляет автоопределение — это нужно, когда секции приезжают
асинхронно и мигать заглушкой в первом кадре не надо. :empty="true" показывает
её принудительно.
Пропы этого примера