GrSplitter
Берут, когда двум областям нужна общая граница.
Когда брать
- двум областям нужна общая граница — дерево и контент, редактор и предпросмотр, список и деталь;
- размер выбирает пользователь — граница двигается мышью и с клавиатуры,
min/maxдержат края; - одну область сворачивают —
collapsibleубирает панель целиком и возвращает обратно; - раскладка сохраняется —
v-modelотдаёт размер, и его остаётся записать.
Когда взять другое
| Нужно | Берите |
|---|---|
| Областей много и они на сетке | GrDashboard |
| Раскладка фиксирована | CSS Grid и GrCard |
| Панель выезжает поверх контента | GrDrawer |
| Разделы переключаются | GrTabs |
| Просто линия между блоками | GrDivider |
Размер — доля, а не пиксели
modelValue — процент, который занимает первая панель. Проценты переживают
смену ширины окна: раскладка, настроенная на широком мониторе, остаётся
осмысленной на ноутбуке, а пиксельная ширина там же съела бы половину экрана.
Второе следствие важнее первого: доля известна до рендера, поэтому серверная
разметка совпадает с клиентской и гидрация проходит чисто. Компонент меряет DOM
ровно в одном месте — когда переводит координату указателя в проценты; ни
клавиатура, ни Home/End, ни сворачивание замеров не требуют.
v-model не обязателен: без него сплиттер помнит размер сам (как collapsed у
GrSidebar). change эмитится в конце жеста и на каждом шаге с клавиатуры —
это точка, куда вешают сохранение раскладки.
Три панели — это вложение
Компонент держит ровно две панели и один разделитель. Раскладка «дерево | редактор / консоль» собирается вложением, как в редакторах кода:
<GrSplitter v-model="treeWidth">
<template #start><FileTree /></template>
<template #end>
<GrSplitter v-model="consoleHeight" orientation="vertical">
<template #start><Editor /></template>
<template #end><Console /></template>
</GrSplitter>
</template>
</GrSplitter>
Вложенные сплиттеры независимы: каждый ведёт свою модель.
Границы панелей
min и max ограничивают первую панель, minEnd — вторую. Без minEnd
вторую панель можно задавить в ноль, и вернуть её будет нечем.
При конфликте min и minEnd побеждает min: первая панель не уезжает под
свой минимум, даже если вторая при этом окажется уже своего. Обратный порядок
дал бы панель, из которой пользователь не может выбраться.
Клавиатура и мышь
Разделитель стоит в таб-порядке (role="separator", tabindex="0"):
←/→в горизонтальной раскладке,↑/↓в вертикальной — шаг;Shift+ стрелка — крупный шаг;Home/End— кmin/max;Enter— свернуть и вернуть (приcollapsible);- двойной клик — сброс к
defaultSize; - перетаскивание вплотную к краю (ниже половины
min) сворачивает панель мышью: двойной клик занят сбросом, а свернуть мышью чем-то надо.
Зона захвата шире видимой полосы: шестипиксельная мишень мышью не берётся, а делать ради этого полосу толще значило бы красить раскладку под удобство мыши.
`aria-orientation` инвертирован — и это правильно
orientation описывает раскладку: horizontal — панели стоят рядом.
aria-orientation описывает сам разделитель, а панели, стоящие рядом,
разделяет вертикальная полоса. Поэтому у горизонтальной раскладки
aria-orientation="vertical", и наоборот.
Разделитель объявляет aria-valuenow/valuemin/valuemax в тех же процентах,
что и модель, и ссылается на первую панель через aria-controls.
Свёрнутость — не ноль в модели
collapsed — отдельное состояние (v-model:collapsed), а не modelValue: 0.
Прежний размер обязан пережить сворачивание, иначе Enter вернул бы панель не
туда, откуда её свернули. В треке свёрнутая панель занимает 0 %, и
aria-valuenow показывает 0 — то, что видно на экране.
Границы
GrResizable(ресайз блока за уголок) сюда не входит — другой сценарий и другой паттерн;- сохранение раскладки — задача приложения:
v-modelиchangeдают всё необходимое, аlocalStorageвнутри компонента был бы состоянием, о котором приложение не знает; - snap-точек и «магнитов» нет — без сценария это лишняя механика поверх клампа;
- своей физики касания нет —
pointer-события покрывают тач как есть.
Playground 12
Загружается…
<GrSplitter />Установка
npm i @feugene/granularityИмпорт
import { GrSplitter } from '@feugene/granularity/components/GrSplitter'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
modelValue | number | undefined | undefined | Доля первой панели в процентах. Поддерживает `v-model`; без него компонент помнит размер сам. |
disabled | boolean | undefined | false | Разделитель не тянется, не фокусируется и не отвечает на клавиши. |
ariaLabel | string | undefined | undefined | Имя разделителя для скринридера. Не задано — из локали. |
max | number | undefined | 90 | Максимум первой панели в процентах. |
orientation | "horizontal" | "vertical" | undefined | undefined | `horizontal` — панели рядом, `vertical` — одна над другой. |
step | number | undefined | 1 | Шаг стрелок. |
min | number | undefined | 10 | Минимум первой панели в процентах. |
collapsed | boolean | undefined | false | Первая панель свёрнута. Поддерживает `v-model:collapsed`. |
defaultSize | number | undefined | 50 | Стартовая доля и точка возврата по двойному клику. |
minEnd | number | undefined | 10 | Минимум второй панели: без него её можно задавить в ноль. |
bigStep | number | undefined | 10 | Шаг стрелок с `Shift`. |
collapsible | boolean | undefined | undefined | Разрешить сворачивание первой панели. |
Slots
| Slot | Type | Описание |
|---|---|---|
start | any | Первая панель — та, чей размер задаёт модель. |
end | any | Вторая панель: занимает остаток. |
Events
| Event | Type | Описание |
|---|---|---|
update:modelValue | [value: number] | — |
change | [value: number] | — |
update:collapsed | [value: boolean] | — |
Примеры 3
Дерево и контент
Размер первой панели — доля контейнера в процентах: она переживает смену ширины окна и рендерится на сервере без замеров. Граница тянется мышью и ходит с клавиатуры.
src/
components/
composables/
styles/
docs/
package.json
Тяните границу мышью или доведите до неё фокус клавишей Tab: стрелки двигают на процент, Shift + стрелка — на десять, Home и End упираются в границы.
Доля первой панели: 28 %
<script setup lang="ts">
import { ref } from 'vue'
import { GrSplitter } from '@feugene/granularity'
const size = ref(28)
const files = ['src/', ' components/', ' composables/', ' styles/', 'docs/', 'package.json']
</script>
<template>
<div class="grid gap-3">
<div class="h-64 overflow-hidden rounded-[var(--gr-radius-md)] border border-[var(--gr-brd)]">
<GrSplitter v-model="size" :min="15" :max="60" aria-label="Ширина дерева файлов">
<template #start>
<div class="h-full overflow-auto bg-[var(--gr-muted)] p-3" tabindex="0">
<p
v-for="file in files"
:key="file"
class="whitespace-pre text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-relaxed)] text-[var(--gr-muted-fg)]"
>{{ file }}</p>
</div>
</template>
<template #end>
<div class="h-full overflow-auto p-4">
<p class="text-[length:var(--gr-text-sm)] text-[var(--gr-fg)]">
Тяните границу мышью или доведите до неё фокус клавишей Tab: стрелки двигают на процент,
Shift + стрелка — на десять, Home и End упираются в границы.
</p>
</div>
</template>
</GrSplitter>
</div>
<p class="text-[length:var(--gr-text-xs)] text-[var(--gr-muted-fg)]">
Доля первой панели: {{ Math.round(size) }} %
</p>
</div>
</template>Три панели — это вложение
Сплиттер держит две панели и один разделитель; редактор с консолью под деревом собирается вложенным вертикальным сплиттером — ровно так, как это делают редакторы кода.
$ yarn build
✓ built in 692ms
<script setup lang="ts">
import { ref } from 'vue'
import { GrSplitter } from '@feugene/granularity'
const treeWidth = ref(25)
const consoleHeight = ref(65)
</script>
<template>
<div class="h-72 overflow-hidden rounded-[var(--gr-radius-md)] border border-[var(--gr-brd)]">
<GrSplitter v-model="treeWidth" :min="15" :max="50" aria-label="Ширина дерева">
<template #start>
<div class="h-full bg-[var(--gr-muted)] p-3 text-[length:var(--gr-text-xs)] text-[var(--gr-muted-fg)]">
Дерево
</div>
</template>
<template #end>
<!-- Трёхпанельная раскладка — это вложение, а не третья панель у сплиттера. -->
<GrSplitter v-model="consoleHeight" orientation="vertical" :min="30" aria-label="Высота консоли">
<template #start>
<div class="h-full p-4 text-[length:var(--gr-text-sm)] text-[var(--gr-fg)]">
Редактор
</div>
</template>
<template #end>
<div class="h-full overflow-auto bg-[var(--gr-muted)] p-3 font-mono text-[length:var(--gr-text-xs)] text-[var(--gr-muted-fg)]" tabindex="0">
<p>$ yarn build</p>
<p>✓ built in 692ms</p>
</div>
</template>
</GrSplitter>
</template>
</GrSplitter>
</div>
</template>Минимумы и сворачивание
min и minEnd не дают задавить ни одну из панелей. Enter сворачивает первую и возвращает её к прежнему размеру, двойной клик сбрасывает границу к defaultSize.
Доля панели: 30 %
<script setup lang="ts">
import { ref } from 'vue'
import { GrSplitter } from '@feugene/granularity'
const size = ref(30)
const collapsed = ref(false)
</script>
<template>
<div class="grid gap-3">
<div class="h-56 overflow-hidden rounded-[var(--gr-radius-md)] border border-[var(--gr-brd)]">
<GrSplitter
v-model="size"
v-model:collapsed="collapsed"
collapsible
:min="20"
:min-end="30"
:default-size="30"
aria-label="Ширина боковой панели"
>
<template #start>
<div class="h-full bg-[var(--gr-muted)] p-3 text-[length:var(--gr-text-xs)] text-[var(--gr-muted-fg)]">
Фильтры
</div>
</template>
<template #end>
<div class="h-full p-4 text-[length:var(--gr-text-sm)] text-[var(--gr-fg)]">
Enter на разделителе сворачивает панель и возвращает её туда же, откуда свернул.
Двойной клик сбрасывает границу к значению по умолчанию, а перетаскивание вплотную
к левому краю схлопывает панель мышью.
</div>
</template>
</GrSplitter>
</div>
<p class="text-[length:var(--gr-text-xs)] text-[var(--gr-muted-fg)]">
{{ collapsed ? 'Панель свёрнута' : `Доля панели: ${Math.round(size)} %` }}
</p>
</div>
</template>Доступность
- Паттерн APG
window splitter- Клавиши
- разделитель стоит в таб-порядке:
←/→(в вертикальной раскладке↑/↓) — шаг,Shift+стрелка — крупный шаг,Home/End— кmin/max,Enter— свернуть и вернуть панель (приcollapsible). Свёрнутая панель на первое нажатие стрелки возвращается: двигать то, чего не видно, значит менять раскладку вслепую