GrAffix
Берут, когда действия длинной формы.
Обзор
Когда брать
- действия длинной формы — «Сохранить» не должен уезжать за экран на двадцатом поле; панель прилипает к низу и отпускается, когда форма кончилась;
- заголовок раздела в длинном списке — читатель всегда видит, где находится, а не отматывает вверх;
- оглавление или фильтры сбоку — колонка едет с содержимым и останавливается
под шапкой приложения; подсветку раздела в этой колонке даёт
GrScrollSpy; - панель внутри своего скроллера — в раздвижной области, в теле окна, в прокручиваемой карточке: край берётся у ближайшего скроллпорта, а не у окна;
- вид меняется от того, прилипло ли — подзаголовок уходит, кнопка ужимается
до иконки: состояние приезжает в слот и в
data-stuck.
Когда взять другое
| Нужно | Берите |
|---|---|
| Шапка приложения с зонами, кнопкой меню и лендмарком | GrNavbar |
| Заголовок таблицы, стоящий на месте при прокрутке тела | GrTable / GrDataTable |
| Нижняя навигация по разделам на мобильном | GrBottomNav |
| Подсветить пункт оглавления, до которого дочитали | GrScrollSpy |
| Панель поверх страницы, а не в её потоке | GrDrawer / GrModal |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrAffix><div class="flex items-center justify-between gap-3 px-4 py-2"><span class="font-600">Invoice INV-0418 · €5,166.50</span><GrButton size="sm">Save</GrButton></div></GrAffix>Примеры 3 у каждого — живое демо и его код
Панель, которая знает, что прилипла
Панель прилипает к краю своего скроллера, а не окна.
stuckприходит в слот: когда заголовок наверху уехал, панель повторяет номер и сумму, и «Send» не нажимают вслепую. Поверхность и тень приходят вместе с прилипанием — отлипшая, панель остаётся частью страницы.INV-0418 DraftNorthwind Traders · due 21 October 20267 lines4 × Ergonomic chair€1,156.002 × Standing desk€1,280.0012 × Monitor arm€948.004 × Desk lamp€216.002 × Whiteboard€290.001 × Delivery to Hamburg€120.001 × Assembly on site€240.00Total€4,250.00Заголовки разделов под шапкой приложения
Каждый заголовок прилипает под уже закреплённой шапкой и только внутри своего раздела — следующий раздел его выталкивает. Отступ задан один раз для всех токеном
--gr-affix-offsetна контейнере; наблюдатель читает вычисленный стиль, поэтому3remиvar(…)работают так же, как проп.Billing settingsInvoices
Numbering: INV-0001Payment terms: 14 daysDefault currency: EURFooter note on every PDFReminders
First reminder: 3 days after dueSecond reminder: 10 days after dueCopy to the account ownerTaxes
VAT number: DE 812 345 678Reverse charge for EU customersRates by country of delivery«Сохранить» под рукой, пока есть что сохранять
placement="bottom"держит действия у нижнего края, пока форму читают, и отпускает их на место в её конце.disabledвыключает прилипание, пока ни одно поле не изменилось, — разметка остаётся, и фокус на кнопках не теряется;@sticky-changeсообщает состояние.Billing profileAll changes saved
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
placement | "top" | "bottom" | undefined | "top" | Край, к которому прилипает панель. |
offset | string | number | undefined | undefined | Отступ от края в прилипшем состоянии. Число — пиксели, строка — как есть (`4rem`, `var(--gr-navbar-height)`, `calc(…)`). Общий отступ для группы панелей задаётся не пропом, а токеном `--gr-affix-offset` на контейнере или на `:root`: наблюдатель читает вычисленный стиль и подхватывает его так же, как значение пропа. |
disabled | boolean | undefined | false | Прилипание выключено: разметка та же, `sticky` не ставится. Именно проп, а не `v-if` снаружи: размонтирование убило бы фокус на кнопке панели и состояние вложенных контролов. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | { stuck: boolean; } | Содержимое панели. `stuck` позволяет менять его в прилипшем виде. |
События
| Event | Сигнатура | Описание |
|---|---|---|
stickyChange | (event: "stickyChange", stuck: boolean): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
stuck | boolean | Прилипла ли панель сейчас — то же состояние, что в слоте и в `data-stuck`. |
remeasure | () => void | Перечитать отступ и пересобрать наблюдателя: `vh` после ресайза, смена высоты шапки. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrAffixProps, GrAffixEmits, GrAffixPlacement } from '@feugene/granularity/components/GrAffix'Файлdist/types/components/GrAffix/GrAffix.vue.d.ts
GrAffix — панель, прилипающая к краю при прокрутке: заголовок раздела,
оглавление, кнопки «Сохранить/Отмена» под длинной формой.
Позиционирует position: sticky; наблюдатель нужен только чтобы **узнать**,
прилипла ли панель, — от этого зависят фон, тень и содержимое. Отлипшая панель
стоит внутри формы и обязана выглядеть её частью, поэтому поверхность
включается вместе с состоянием, а не всегда, как у GrNavbar и GrTable.
Корень компонента — сама липкая коробка, а не обёртка вокруг неё: sticky
зажат своим containing block, и обёртка высотой в коробку не дала бы ей ехать
вовсе. Сентинел — сосед коробки, отмечающий её место в потоке.
interface GrAffixProps {
/** Край, к которому прилипает панель. */
placement?: GrAffixPlacement;
/**
* Отступ от края в прилипшем состоянии. Число — пиксели, строка — как есть
* (`4rem`, `var(--gr-navbar-height)`, `calc(…)`).
*
* Общий отступ для группы панелей задаётся не пропом, а токеном
* `--gr-affix-offset` на контейнере или на `:root`: наблюдатель читает
* вычисленный стиль и подхватывает его так же, как значение пропа.
*/
offset?: number | string;
/**
* Прилипание выключено: разметка та же, `sticky` не ставится.
*
* Именно проп, а не `v-if` снаружи: размонтирование убило бы фокус на кнопке
* панели и состояние вложенных контролов.
*/
disabled?: boolean;
}interface GrAffixEmits {
/** Панель прилипла (`true`) или отлипла (`false`); шлётся только при смене состояния. */
(e: 'stickyChange', stuck: boolean): void;
}Файлdist/types/components/GrAffix/config.d.ts
const grAffixConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrAffix">;Файлdist/types/components/GrAffix/affixState.d.ts
type GrAffixPlacement = typeof GR_AFFIX_PLACEMENTS[number];Файлdist/types/components/GrAffix/safelist.d.ts
const grAffixSafelist: string[];Файлdist/types/components/GrAffix/index.d.ts
type GrAffixInstance = ComponentExposed<typeof GrAffixComponent>;Использование из прозы библиотеки
«Прилипло» вычисляется зондом, а не догадкой
Рядом с панелью встаёт невидимый сентинел высотой в пиксель — он отмечает место,
где панель стояла бы, не будь она липкой, и раскладку не сдвигает: пиксель высоты
гасится пикселем отрицательного поля. За ним следит IntersectionObserver, у
которого край корня поджат ровно на отступ прилипания, — тогда граница корня и
есть линия, пересечение которой означает «прилипло».
Считается это по прямоугольникам записи, а не по её isIntersecting: тот
одинаково равен false и когда панель ещё не доехала до края, и когда её
containing block кончился и она уехала за него.
Цена сентинела ровно одна и видна в вёрстке: во flex- или grid-родителе он
считается отдельным элементом — получает свой gap, а в гриде занимает ещё и
ячейку, сдвигая соседей. Панель в таком родителе оборачивают обычным div.
Обёртка здесь не противоречит правилу выше: запрещена обёртка по высоте
панели, а эта высоту получает от родителя — грид растягивает ячейку на весь
ряд, — и потому остаётся containing block’ом, внутри которого панели есть куда
ехать. То же даёт явная высота или align-self: stretch во flex-колонке.
Отступ можно задать группе, а не каждой панели
Проп offset — это данные экземпляра: «эта панель стоит под шапкой в 112 пикселей».
Общий отступ для всех панелей раздела задаётся не пропом, а токеном на контейнере
или на :root:
:root { --gr-affix-offset: 112px; }Наблюдатель читает вычисленный стиль, а не значение пропа, поэтому каскад,
4rem, var(--gr-navbar-height) и calc(…) работают одинаково и одинаково
подхватываются. Исключение из общего правила задаётся пропом на конкретной панели.
Отступ, изменившийся без смены пропа — после ресайза у единиц vh, после смены
высоты шапки, — перечитывается методом remeasure() через ref на компонент.
Поверхность появляется вместе с прилипанием
Отлипшая панель стоит внутри формы и обязана выглядеть её частью, поэтому фон и
тень включаются только в прилипшем виде — в отличие от GrNavbar и заголовка
GrTable, которые непрозрачны всегда.
Фон обязателен: без него уезжающее под панель содержимое просвечивает насквозь. Отделяет её тень, а не рамка: рамка добавляет высоту и сдвинула бы раскладку ровно в момент прилипания — страница дёрнулась бы на пиксель.
Из тех же соображений панель обязана быть во всю ширину своего скроллпорта. Отступ, заданный на прокручиваемом блоке, оставляет поля по бокам от неё — и в этих просветах видно уезжающее содержимое. Отступы поэтому живут на содержимом: внутри самой панели и на её соседях, а не на общем контейнере.
Панель внутри карточки берёт её поверхность одной строкой:
<GrAffix style="--gr-affix-bg: var(--gr-card)">
…
</GrAffix>Совсем без фона — --gr-affix-bg: transparent; --gr-affix-shadow: none.
Слой локальный, и это не упущение
Панель садится на z-index: 10 внутри своего stacking-контекста, а не на ступень
общей шкалы. Шаг между слоями шкалы оставлен приложению ровно под такой случай
(«свой sticky-хедер под дропдауном»), а глобального ответа на вопрос «выше или
ниже нижней навигации» у липкой панели нет — он зависит от приложения. Нужен слой
шкалы: --gr-affix-z: var(--gr-z-navbar).
Несколько панелей друг под другом
Две верхние панели в одном скроллере прилипают на одном отступе и накладываются:
автоматической стопки нет — она требовала бы замера высот на каждом кадре. Второй
панели отступ задаётся вручную: :offset="112 + высотаПервой". При равном слое
поверх окажется та, что позже в разметке.
Когда не липнет
position: sticky молча перестаёт работать, если между панелью и настоящим
скроллером есть предок с overflow, отличным от visible: он сам становится
скроллпортом, а прокручивать его нечем. Сборка при этом зелёная, класс в разметке
стоит, и отличить это от собственной ошибки без DevTools нельзя — поэтому в
dev-режиме компонент называет виновного предка в консоли.
Так же тихо прилипание ломают contain: paint, contain: strict | content и
content-visibility: auto у предка. Их компонент не проверяет: разбор значений
contain дал бы ложные срабатывания дороже пропущенного случая. Панель не липнет,
а предупреждения нет — смотреть в первую очередь на них.
Границы
- горизонтального прилипания нет. Края только
topиbottom; закреплённая колонка — этоGrDataTable, и там она сделана иначе.
Пропы этого примера