GrCollapse

  • ядро
  • Слои
  • API · 11 props
  • ⌨ клавиши

Берут, когда содержимого много, а нужно не всё сразу.

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

Обзор

Когда брать

  • содержимого много, а нужно не всё сразу — вопросы и ответы, настройки, детали записи;
  • открыта одна секция за раз — accordion закрывает предыдущую;
  • секции нужны незрячему — headingLevel ставит заголовок нужного уровня, и обход по заголовкам работает;
  • открытие надо перехватить — beforeChange откладывает раскрытие до загрузки или подтверждения.

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

НужноБерите
Разделы переключаются, а не раскрываютсяGrTabs
Разделы формы идут подрядGrFormSection
Данные вложеныGrTree
Содержимое открывается поверх страницыGrDialog / GrDrawer

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

Сцена

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

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

Примеры 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

    Tomorrow
    12 Harbour Street, 20457 Hamburg · DHL, before 12:00

    Paid
    Visa 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

PropTypeпо умолчаниюОписание
disabledboolean | undefinedfalseБлокирует все секции: заголовки не переключаются.
modelValuev-modelGrCollapseValue | GrCollapseValue[] | undefinedundefinedОткрытые секции (`v-model`): в `accordion` — одно значение или `undefined`, иначе массив.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedРазмер секций. Не задан — берётся из `GrConfigProvider`, иначе `md`.
headingLevel2 | 3 | 4 | 5 | 6 | undefinedundefinedУровень заголовков секций (`h2`…`h6`) — под структуру страницы.
accordionboolean | undefinedfalseОдновременно открыта только одна секция: открытие новой закрывает прежнюю.
dividedboolean | undefinedundefinedРазделители между секциями. По умолчанию `true`; глобально — `componentDefaults`.
borderlessboolean | undefinedundefinedБез обёртки в `GrCard`: аккордеон внутри карточки или сайдбара иначе получает вторую рамку и вторую тень. По умолчанию `false`.
expandIconPosition"end" | "start" | undefinedundefinedСторона шеврона относительно заголовка.
beforeChangeGrCollapseBeforeChange | undefinedundefinedAsync-guard переключения: получает секцию и направление, `false` отменяет. Пока он не ответил, повторный клик по той же секции игнорируется.
emptyboolean | undefinedundefinedПусто ли. Не задан — считается по содержимому: аккордеон без секций показывает заглушку вместо пустой рамки. `false` подавляет автоопределение — например, когда секции приезжают асинхронно и мигать текстом не надо.
emptyTextstring | undefinedundefinedТекст пустого состояния. Слот `#empty` сильнее.

Слоты

SlotTypeОписание
defaultanyСекции аккордеона (`GrCollapseItem`).
emptyanyСодержимое пустого состояния вместо текста по умолчанию.

События

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

GrCollapsePropsinterface

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;
}
GrCollapseEmitsinterface
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

GrCollapseItemPropsinterface

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

grCollapseConfigconst
const grCollapseConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCollapse">;

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

GrCollapseConfigurablePropsinterface

Пропы 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

grCollapseSafelistconst
const grCollapseSafelist: string[];

Доступность

Паттерн APG

accordion

Клавиши

Space / Enter↓ / ↑Home / End

КлавишаДействие
Space / Enterраскрыть-свернуть
↓ / ↑к соседнему заголовку (по кругу)
Home / Endк первому/последнему. Стрелки ходят только по заголовкам своего аккордеона: вложенный GrCollapse в них не попадает. Содержимое свёрнутой секции inert: Tab в него не заходит

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

Уровень заголовка

Заголовок секции рендерится тегом h3, а headingLevel подгоняет его под структуру страницы: в разделе <h4> аккордеон обязан начинаться с h5, иначе навигация по заголовкам получает разрыв уровней. Кнопка остаётся внутри заголовка — этого требует APG для accordion.

Поверхность

borderless убирает обёртку в GrCard: аккордеон внутри карточки, сайдбара или панели фильтров иначе получает вторую рамку и вторую тень. divided управляет разделителями между секциями.

vue
<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 не ответил, повторный клик по тому же заголовку игнорируется: иначе два подтверждения подряд вернули бы состояние к исходному.

ts
async function beforeChange(name: GrCollapseValue, expanding: boolean): Promise<boolean> {
  if (expanding)
    return true
  return confirmDiscardChanges(name)
}
Клавиатура и вложенность

Стрелки ↑/↓ (по кругу) и Home/End ходят только по заголовкам своего аккордеона: вложенный GrCollapse внутри раскрытой панели в обход не попадает.

Свёрнутая панель помечена inert — ни Tab, ни скринридер в неё не заходят, при этом (в отличие от hidden) анимация раскрытия сохраняется.

Пустое состояние
vue
<GrCollapse>
  <GrCollapseItem v-for="item in filtered" :key="item.name" v-bind="item" />
</GrCollapse>

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

Пустоту считает содержимое слота, а не длина ваших данных, поэтому работает и v-for по пустому массиву, и v-if, ничего не отрисовавший. Комментарии, которые оставляет после себя v-if, и переносы строк из шаблона содержимым не считаются — иначе заглушка не появилась бы никогда.

Текст берётся из локали (gr.collapse.empty). Перебить его можно двумя способами, от простого к общему:

vue
<GrCollapse empty-text="Нет разделов" />

<GrCollapse>
  <template #empty>
    <GrEmptyState title="Ничего не найдено" description="Смягчите фильтр" />
  </template>
</GrCollapse>

:empty="false" подавляет автоопределение — это нужно, когда секции приезжают асинхронно и мигать заглушкой в первом кадре не надо. :empty="true" показывает её принудительно.