@surdeddd/bottom-sheet 0.23.4 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +513 -527
- package/README.ru.md +533 -545
- package/dist/{BottomSheetCore-C5tJYVMY.d.cts → BottomSheetCore-B_r4KCQL.d.cts} +48 -13
- package/dist/{BottomSheetCore-DxSrC4QC.d.ts → BottomSheetCore-B_r4KCQL.d.ts} +48 -13
- package/dist/{BottomSheetEngine-B3F9qukX.d.cts → BottomSheetEngine-0F7mjyvF.d.cts} +1 -1
- package/dist/{BottomSheetEngine-BchzgqGG.d.ts → BottomSheetEngine-CJV_DDrM.d.ts} +1 -1
- package/dist/core.cjs +810 -436
- package/dist/core.d.cts +2 -3
- package/dist/core.d.ts +2 -3
- package/dist/core.js +810 -436
- package/dist/debug.d.cts +0 -1
- package/dist/debug.d.ts +0 -1
- package/dist/{default-features-DwVTzDwP.d.ts → default-features-4Xwv6aVt.d.ts} +4 -2
- package/dist/{default-features-BGiCSmV1.d.cts → default-features-DX2NOxP6.d.cts} +4 -2
- package/dist/element.cjs +1421 -697
- package/dist/element.d.cts +11 -3
- package/dist/element.d.ts +11 -3
- package/dist/element.iife.global.js +2 -2
- package/dist/element.js +1421 -697
- package/dist/features.cjs +334 -190
- package/dist/features.d.cts +3 -4
- package/dist/features.d.ts +3 -4
- package/dist/features.js +334 -191
- package/dist/{index-C7TSQkVK.d.cts → index-2shlxhcG.d.cts} +1 -1
- package/dist/{index-CdM1tke5.d.ts → index-CNCRQnpc.d.ts} +1 -1
- package/dist/index.cjs +1129 -631
- package/dist/index.d.cts +11 -6
- package/dist/index.d.ts +11 -6
- package/dist/index.js +1129 -632
- package/dist/integrations/formik.cjs +21 -10
- package/dist/integrations/formik.d.cts +0 -1
- package/dist/integrations/formik.d.ts +0 -1
- package/dist/integrations/formik.js +22 -11
- package/dist/integrations/react-hook-form.cjs +21 -12
- package/dist/integrations/react-hook-form.d.cts +0 -1
- package/dist/integrations/react-hook-form.d.ts +0 -1
- package/dist/integrations/react-hook-form.js +21 -12
- package/dist/option-keys-DItuAjUw.d.cts +6 -0
- package/dist/option-keys-a5DYNwwO.d.ts +6 -0
- package/dist/overlay.cjs +386 -189
- package/dist/overlay.d.cts +1 -3
- package/dist/overlay.d.ts +1 -3
- package/dist/overlay.js +386 -189
- package/dist/preact.d.cts +3 -4
- package/dist/preact.d.ts +3 -4
- package/dist/qwik-lib/index.qwik.cjs +1306 -671
- package/dist/qwik-lib/index.qwik.mjs +1306 -671
- package/dist/qwik.cjs +1326 -701
- package/dist/qwik.d.cts +12 -30
- package/dist/qwik.d.ts +12 -30
- package/dist/qwik.js +1326 -701
- package/dist/react.cjs +1373 -750
- package/dist/react.d.cts +26 -12
- package/dist/react.d.ts +26 -12
- package/dist/react.js +1375 -752
- package/dist/solid.cjs +1332 -727
- package/dist/solid.d.cts +13 -438
- package/dist/solid.d.ts +13 -438
- package/dist/solid.js +1334 -729
- package/dist/solid.source.jsx +1374 -751
- package/dist/styles.css +20 -6
- package/dist/svelte-core.cjs +1209 -676
- package/dist/svelte-core.js +1207 -677
- package/dist/svelte-src/BottomSheet.svelte +101 -58
- package/dist/svelte.cjs +1323 -705
- package/dist/svelte.d.cts +45 -25
- package/dist/svelte.d.ts +45 -25
- package/dist/svelte.js +1323 -705
- package/dist/vue.cjs +1354 -773
- package/dist/vue.d.cts +20 -3
- package/dist/vue.d.ts +20 -3
- package/dist/vue.js +1354 -773
- package/dist/webgl.cjs +38 -7
- package/dist/webgl.d.cts +0 -1
- package/dist/webgl.d.ts +0 -1
- package/dist/webgl.js +38 -7
- package/llms.txt +330 -0
- package/package.json +41 -19
- package/dist/transform-jVfAikE9.d.cts +0 -3
- package/dist/transform-jVfAikE9.d.ts +0 -3
package/README.ru.md
CHANGED
|
@@ -1,60 +1,54 @@
|
|
|
1
1
|
# @surdeddd/bottom-sheet
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
](https://www.npmjs.com/package/@surdeddd/bottom-sheet)
|
|
10
|
+
[](https://www.npmjs.com/package/@surdeddd/bottom-sheet)
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
11
14
|

|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
## 🚀 [Живое демо → bottom-sheet-demo-ashy.vercel.app](https://bottom-sheet-demo-ashy.vercel.app)
|
|
19
|
-
|
|
20
|
-
📚 [Справочник API (typedoc) → surdeddd.github.io/BottomSheet](https://surdeddd.github.io/BottomSheet/)
|
|
16
|
+
**[Живое демо](https://bottom-sheet-demo-ashy.vercel.app)** ·
|
|
17
|
+
**[Справочник API](https://surdeddd.github.io/BottomSheet/)** ·
|
|
18
|
+
[История изменений](CHANGELOG.md) ·
|
|
19
|
+
[English](https://github.com/Surdeddd/BottomSheet/blob/main/README.md)
|
|
21
20
|
|
|
22
21
|
<p align="center">
|
|
23
22
|
<picture>
|
|
24
23
|
<source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/sheet-dark.png" />
|
|
25
|
-
<img src="docs/screenshots/sheet-light.png" alt="
|
|
24
|
+
<img src="docs/screenshots/sheet-light.png" alt="Шторка на средней точке привязки поверх затемнённой страницы" width="380" />
|
|
26
25
|
</picture>
|
|
26
|
+
<img src="https://raw.githubusercontent.com/Surdeddd/BottomSheet/main/docs/gifs/01-drag-to-close.gif" alt="Шторку перетаскивают между точками привязки и закрывают броском" width="300" />
|
|
27
27
|
</p>
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
trap с возвратом фокуса.
|
|
53
|
-
- **Стек из нескольких sheet** и типизированный реестр sheet manager.
|
|
54
|
-
- **Отменяемые `before-snap` / `before-close`** — переход можно
|
|
55
|
-
синхронно заблокировать.
|
|
56
|
-
- Под капотом 500+ unit-тестов и ~200 e2e-прогонов Playwright на трёх
|
|
57
|
-
браузерных движках (`mobile-chrome` / `mobile-safari` / `firefox`).
|
|
29
|
+
## Чем она отличается
|
|
30
|
+
|
|
31
|
+
- **Один движок на все фреймворки.** Компонент в React ведёт себя так же,
|
|
32
|
+
как в Vue, Svelte, Solid, Qwik или в веб-компоненте: это тонкие адаптеры
|
|
33
|
+
над одним ядром.
|
|
34
|
+
- **Точки привязки в любых единицах.** Пиксели, проценты, `fit` (высота
|
|
35
|
+
самого содержимого) или любая длина CSS: `50dvh`,
|
|
36
|
+
`clamp(200px, 60%, 800px)`.
|
|
37
|
+
- **Четыре края.** `bottom`, `top`, `left`, `right`, а также `start` и `end`,
|
|
38
|
+
которые следуют за направлением письма.
|
|
39
|
+
- **Ощущается как нативная.** Пружина, которая подхватывает скорость пальца,
|
|
40
|
+
резиновое сопротивление за крайней точкой, отдельные пороги для пальца и
|
|
41
|
+
мыши, движение только на композиторе.
|
|
42
|
+
- **Доступность.** Ручка работает как слайдер с клавиатуры, фокус можно
|
|
43
|
+
запереть в шторке и вернуть обратно, всё за модальной шторкой получает
|
|
44
|
+
`inert`, учитываются reduced motion и forced colors.
|
|
45
|
+
- **Знает про телефон.** Встаёт на экранную клавиатуру, учитывает безопасные
|
|
46
|
+
зоны, закрывается кнопкой «Назад», блокирует прокрутку страницы способом,
|
|
47
|
+
который работает в iOS.
|
|
48
|
+
- **Стек.** Несколько шторок открыты одновременно, у каждой свой бэкдроп и
|
|
49
|
+
своя область фокуса.
|
|
50
|
+
- **Небольшой размер.** 25.6 KB gzip у полного движка, 21.9 KB у
|
|
51
|
+
облегчённого ядра, зависимостей в рантайме нет.
|
|
58
52
|
|
|
59
53
|
## Установка
|
|
60
54
|
|
|
@@ -62,636 +56,630 @@ English: [README.md](./README.md)
|
|
|
62
56
|
npm i @surdeddd/bottom-sheet
|
|
63
57
|
```
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
Фреймворки объявлены необязательными peer-зависимостями: ставится только
|
|
60
|
+
тот, которым вы пользуетесь.
|
|
67
61
|
|
|
68
62
|
## Быстрый старт
|
|
69
63
|
|
|
70
|
-
###
|
|
64
|
+
### Модальная шторка по кнопке
|
|
71
65
|
|
|
72
66
|
```tsx
|
|
67
|
+
import { useState } from 'react';
|
|
73
68
|
import { BottomSheet } from '@surdeddd/bottom-sheet/react';
|
|
74
69
|
import '@surdeddd/bottom-sheet/styles';
|
|
75
70
|
|
|
71
|
+
export function Filters() {
|
|
72
|
+
const [open, setOpen] = useState(false);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<button onClick={() => setOpen(true)}>Filters</button>
|
|
77
|
+
|
|
78
|
+
<BottomSheet
|
|
79
|
+
open={open}
|
|
80
|
+
onOpenChange={setOpen}
|
|
81
|
+
snapPoints={[
|
|
82
|
+
{ id: 'closed', size: 0 },
|
|
83
|
+
{ id: 'open', size: 'fit' },
|
|
84
|
+
]}
|
|
85
|
+
focusTrap
|
|
86
|
+
header={<h2>Filters</h2>}
|
|
87
|
+
>
|
|
88
|
+
<FilterForm />
|
|
89
|
+
</BottomSheet>
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Пропсу `open` нужна точка привязки размером `0`: это и есть закрытое
|
|
96
|
+
состояние. Тап по бэкдропу, Escape и перетаскивание вниз закрывают шторку и
|
|
97
|
+
сообщают об этом через `onOpenChange`.
|
|
98
|
+
|
|
99
|
+
### Шторка, которая всегда на экране
|
|
100
|
+
|
|
101
|
+
Как в картах: полоса снизу, которую тянут вверх.
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
76
104
|
<BottomSheet
|
|
77
105
|
snapPoints={[
|
|
78
|
-
{ id: '
|
|
106
|
+
{ id: 'peek', size: 96 },
|
|
79
107
|
{ id: 'half', size: '45dvh' },
|
|
80
|
-
{ id: 'full', size: '
|
|
108
|
+
{ id: 'full', size: '90%' },
|
|
81
109
|
]}
|
|
82
|
-
initial=
|
|
83
|
-
|
|
84
|
-
spring={{ stiffness: 260, damping: 28 }}
|
|
85
|
-
focusTrap
|
|
86
|
-
closeOnEscape
|
|
87
|
-
header={<h2>Search vehicles</h2>}
|
|
110
|
+
initial="peek"
|
|
111
|
+
header={<h2>Nearby</h2>}
|
|
88
112
|
>
|
|
89
|
-
<
|
|
90
|
-
</BottomSheet
|
|
113
|
+
<PlaceList />
|
|
114
|
+
</BottomSheet>
|
|
91
115
|
```
|
|
92
116
|
|
|
93
|
-
|
|
117
|
+
### Другие фреймворки
|
|
94
118
|
|
|
95
|
-
|
|
119
|
+
<details>
|
|
120
|
+
<summary><b>Vue 3</b></summary>
|
|
96
121
|
|
|
97
122
|
```vue
|
|
98
|
-
<script setup>
|
|
99
|
-
|
|
100
|
-
|
|
123
|
+
<script setup lang="ts">
|
|
124
|
+
import { ref } from 'vue';
|
|
125
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/vue';
|
|
126
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
127
|
+
|
|
128
|
+
const open = ref(false);
|
|
129
|
+
const points = [
|
|
130
|
+
{ id: 'closed', size: 0 },
|
|
131
|
+
{ id: 'open', size: 'fit' },
|
|
132
|
+
];
|
|
101
133
|
</script>
|
|
102
134
|
|
|
103
135
|
<template>
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
@snap="onSnap"
|
|
109
|
-
>
|
|
110
|
-
<template #header><h2>Search</h2></template>
|
|
111
|
-
<YourList />
|
|
136
|
+
<button @click="open = true">Filters</button>
|
|
137
|
+
<BottomSheet v-model:open="open" :snap-points="points" focus-trap>
|
|
138
|
+
<template #header><h2>Filters</h2></template>
|
|
139
|
+
<FilterForm />
|
|
112
140
|
</BottomSheet>
|
|
113
141
|
</template>
|
|
114
142
|
```
|
|
115
143
|
|
|
116
|
-
[
|
|
144
|
+
[Гайд по Vue →](docs/vue.md)
|
|
117
145
|
|
|
118
|
-
|
|
146
|
+
</details>
|
|
147
|
+
|
|
148
|
+
<details>
|
|
149
|
+
<summary><b>Svelte 5</b></summary>
|
|
119
150
|
|
|
120
151
|
```svelte
|
|
121
152
|
<script lang="ts">
|
|
122
|
-
import {
|
|
123
|
-
import
|
|
124
|
-
|
|
125
|
-
let sheetEl: HTMLElement | undefined = $state();
|
|
126
|
-
let handleEl: HTMLElement | undefined = $state();
|
|
127
|
-
let contentEl: HTMLElement | undefined = $state();
|
|
128
|
-
|
|
129
|
-
const ctrl = createBottomSheet({
|
|
130
|
-
snapPoints: [{ id: "min", size: 96 }, { id: "full", size: "85%" }],
|
|
131
|
-
initial: "min",
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
$effect(() => {
|
|
135
|
-
if (!sheetEl) return;
|
|
136
|
-
return ctrl.attach({ element: sheetEl, handle: handleEl, scrollContainer: contentEl });
|
|
137
|
-
});
|
|
138
|
-
</script>
|
|
139
|
-
|
|
140
|
-
<section class="bs-sheet" bind:this={sheetEl} data-mode="bottom">
|
|
141
|
-
<div class="bs-handle" bind:this={handleEl}><h2>Search</h2></div>
|
|
142
|
-
<div class="bs-content" bind:this={contentEl}><!-- list --></div>
|
|
143
|
-
</section>
|
|
144
|
-
```
|
|
153
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/svelte';
|
|
154
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
145
155
|
|
|
146
|
-
|
|
156
|
+
let open = $state(false);
|
|
157
|
+
</script>
|
|
147
158
|
|
|
148
|
-
|
|
159
|
+
<button onclick={() => (open = true)}>Filters</button>
|
|
149
160
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
<bottom-sheet
|
|
155
|
-
snap-points='[{"id":"min","size":96},{"id":"full","size":"85%"}]'
|
|
156
|
-
initial="min"
|
|
157
|
-
animation="spring"
|
|
158
|
-
focus-trap="true"
|
|
161
|
+
<BottomSheet
|
|
162
|
+
bind:open
|
|
163
|
+
snapPoints={[{ id: 'closed', size: 0 }, { id: 'open', size: 'fit' }]}
|
|
164
|
+
focusTrap
|
|
159
165
|
>
|
|
160
|
-
<h2
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
</ul>
|
|
164
|
-
</bottom-sheet>
|
|
166
|
+
{#snippet header()}<h2>Filters</h2>{/snippet}
|
|
167
|
+
{#snippet children()}<FilterForm />{/snippet}
|
|
168
|
+
</BottomSheet>
|
|
165
169
|
```
|
|
166
170
|
|
|
167
|
-
[
|
|
171
|
+
[Гайд по Svelte →](docs/svelte.md)
|
|
168
172
|
|
|
169
|
-
|
|
173
|
+
</details>
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
<details>
|
|
176
|
+
<summary><b>Solid</b></summary>
|
|
173
177
|
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
rel="stylesheet"
|
|
178
|
-
href="https://unpkg.com/@surdeddd/bottom-sheet/dist/styles.css"
|
|
179
|
-
/>
|
|
180
|
-
<script src="https://unpkg.com/@surdeddd/bottom-sheet/dist/element.iife.global.js"></script>
|
|
181
|
-
<bottom-sheet
|
|
182
|
-
snap-points='[{"id":"min","size":96},{"id":"full","size":"85%"}]'
|
|
183
|
-
initial="min"
|
|
184
|
-
>
|
|
185
|
-
<h2 slot="header">Title</h2>
|
|
186
|
-
<p>Content</p>
|
|
187
|
-
</bottom-sheet>
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### Vanilla-ядро (без фреймворка)
|
|
191
|
-
|
|
192
|
-
```ts
|
|
193
|
-
import { BottomSheetEngine } from '@surdeddd/bottom-sheet';
|
|
178
|
+
```tsx
|
|
179
|
+
import { createSignal } from 'solid-js';
|
|
180
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/solid';
|
|
194
181
|
import '@surdeddd/bottom-sheet/styles';
|
|
195
182
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
183
|
+
export function Filters() {
|
|
184
|
+
const [open, setOpen] = createSignal(false);
|
|
185
|
+
return (
|
|
186
|
+
<>
|
|
187
|
+
<button onClick={() => setOpen(true)}>Filters</button>
|
|
188
|
+
<BottomSheet
|
|
189
|
+
open={open()}
|
|
190
|
+
onOpenChange={setOpen}
|
|
191
|
+
snapPoints={[{ id: 'closed', size: 0 }, { id: 'open', size: 'fit' }]}
|
|
192
|
+
focusTrap
|
|
193
|
+
header={<h2>Filters</h2>}
|
|
194
|
+
>
|
|
195
|
+
<FilterForm />
|
|
196
|
+
</BottomSheet>
|
|
197
|
+
</>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
208
200
|
```
|
|
209
201
|
|
|
210
|
-
[
|
|
211
|
-
|
|
212
|
-
## API кратко
|
|
213
|
-
|
|
214
|
-
### Snap points
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
type SnapPoint =
|
|
218
|
-
| number // pixels
|
|
219
|
-
| `${number}%` // percent of viewport along the axis
|
|
220
|
-
| 'fit' // natural height of the whole sheet (handle + header + content + footer)
|
|
221
|
-
| 'content' // alias of 'fit'
|
|
222
|
-
| 'full' // 100 % of axis
|
|
223
|
-
| string; // any CSS length — "50dvh", "clamp(200px, 60%, 800px)"
|
|
224
|
-
```
|
|
202
|
+
[Гайд по Solid →](docs/solid.md)
|
|
225
203
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
| Опция | По умолчанию | Что делает |
|
|
229
|
-
| -------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
|
230
|
-
| `snapPoints` | обязательная | Упорядоченный список `{ id, size }` |
|
|
231
|
-
| `allowed` | все id | Подмножество snap point, на которых sheet может остановиться сейчас |
|
|
232
|
-
| `initial` | первая из allowed | Id snap point, с которой стартовать |
|
|
233
|
-
| `mode` | `"bottom"` | `bottom`, `top`, `left`, `right` или логические `start` / `end`; в адаптерах React и Vue реактивна |
|
|
234
|
-
| `animation` | `"spring"` | `spring`, `tween`, `ios-spring`, `material-bounce`, `linear`, `snappy` |
|
|
235
|
-
| `settleAnimation` | выкл. | `"waapi"` — финальная доводка идёт как Web Animation на compositor (заранее насэмплированные spring-кейфреймы); при необходимости автоматически откатывается на rAF |
|
|
236
|
-
| `features` | все по умолчанию | `EngineFeature[]` — переопределить/расширить набор composable-фич (см. «Slim core» ниже) |
|
|
237
|
-
| `spring` | snappy | `{ stiffness, damping, mass }` |
|
|
238
|
-
| `flickVelocity` | `0.65 px/ms` | Порог флика на мобильных |
|
|
239
|
-
| `dragThreshold` | `18 px` | Если drag короче, sheet возвращается назад |
|
|
240
|
-
| `rubberBand` | `true` | Мягкое «резиновое» перетягивание в стиле iOS |
|
|
241
|
-
| `backdropRange` | `[0, 1]` | Диапазон progress, в котором backdrop проявляется |
|
|
242
|
-
| `screenRange` | `[0, 1]` | Диапазон progress, в котором проявляется контент `screen`/scrim |
|
|
243
|
-
| `focusTrap` | `false` | Удерживать Tab-фокус внутри открытого sheet |
|
|
244
|
-
| `initialFocus` | первое поле | Элемент / селектор, получающий фокус при открытии; `false` фокусирует контейнер sheet (`tabindex=-1`) — так не открывается клавиатура iOS |
|
|
245
|
-
| `closeOnEscape` | `true` | Слушать Escape и закрывать |
|
|
246
|
-
| `closeOnBack` | `false` | Перехватывать кнопку «Назад» на Android (history.popstate) |
|
|
247
|
-
| `closeOnRouteChange` | `false` | Закрывать при смене URL (патчит `pushState`/`replaceState` + popstate) |
|
|
248
|
-
| `lockBodyScroll` | `true` | Блокировка прокрутки body, безопасная для iOS (`position: fixed`) |
|
|
249
|
-
| `stackEffect` | `false` | Стек карточек как в iOS: нижние sheet уменьшаются с глубиной |
|
|
250
|
-
| `inertSiblings` | `false` | Помечать соседние элементы страницы `inert`, пока sheet открыт (полноценная модалка) |
|
|
251
|
-
| `persistent` | `false` | Запретить закрытие пользователем (backdrop / Escape / «Назад»); программный `close()` работает |
|
|
252
|
-
| `disableClose` | `false` | Запретить любое закрытие, включая программное |
|
|
253
|
-
| `disableDrag` | `false` | Отключить жест drag (императивные snap продолжают работать) |
|
|
254
|
-
| `dragFrom` | `"handle"` | Какие области запускают drag: `"handle"`, `"sheet"` (весь sheet) или `"zones"` (только поддеревья `[data-bs-drag]`). Если handle не задан, по умолчанию `"sheet"`. `[data-bs-no-drag]` исключает поддерево в любом режиме. Также `setDragFrom()` / `getDragFrom()` — переключение перемонтирует жест, без перемонтирования компонента |
|
|
255
|
-
| `dragFromContent` | `true` | Тянет ли sheet touch-жест на scroll-контейнере; переопределяется для отдельной snap point через `{ id, size, dragFromContent: false }`. Также `setDragFromContent()` |
|
|
256
|
-
| `fitContentToSnap` | `false` | Делает весь scroll-контейнер достижимым, а footer видимым на каждой snap point, а не только на самой большой. По умолчанию выключено; см. [Контент достижим на каждой snap point](#контент-достижим-на-каждой-snap-point) |
|
|
257
|
-
| `radius` | токен по умолчанию | Радиус скругления (`string` — CSS-длина или `number` — px); также `setRadius()` |
|
|
258
|
-
| `maxHeight` | нет | Ограничение высоты sheet. `number` (px) или строка (`"92dvh"`, `"50%"`), пересчитывается при смене viewport / ориентации |
|
|
259
|
-
| `returnFocusTo` | opener | Куда вернуть фокус при закрытии — `HTMLElement`, строка-селектор или фабрика `() => HTMLElement` |
|
|
260
|
-
|
|
261
|
-
### Anchored-элементы, docked-бары и сцены scrim
|
|
262
|
-
|
|
263
|
-
Закрепляйте UI вокруг sheet — он едет вместе с ним на compositor (ноль JS на
|
|
264
|
-
кадр) и показывается/скрывается по состоянию с настраиваемой анимацией:
|
|
204
|
+
</details>
|
|
265
205
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
element: closeBtn,
|
|
269
|
-
position: 'sheet-top-right', // rides the sheet edge; 9 screen positions too
|
|
270
|
-
showOn: ['half', 'full'], // snap ids or a predicate
|
|
271
|
-
animation: 'pop', // fade | scale | slide | pop | custom keyframes
|
|
272
|
-
});
|
|
206
|
+
<details>
|
|
207
|
+
<summary><b>Qwik</b></summary>
|
|
273
208
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
209
|
+
```tsx
|
|
210
|
+
import { component$, useSignal } from '@builder.io/qwik';
|
|
211
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/qwik';
|
|
212
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
278
213
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
214
|
+
export const Filters = component$(() => {
|
|
215
|
+
const open = useSignal(false);
|
|
216
|
+
return (
|
|
217
|
+
<>
|
|
218
|
+
<button onClick$={() => (open.value = true)}>Filters</button>
|
|
219
|
+
<BottomSheet
|
|
220
|
+
open={open.value}
|
|
221
|
+
onOpenChange$={next => (open.value = next)}
|
|
222
|
+
snapPoints={[{ id: 'closed', size: 0 }, { id: 'open', size: 'fit' }]}
|
|
223
|
+
focusTrap
|
|
224
|
+
>
|
|
225
|
+
<h2 q:slot="header">Filters</h2>
|
|
226
|
+
<FilterForm />
|
|
227
|
+
</BottomSheet>
|
|
228
|
+
</>
|
|
229
|
+
);
|
|
285
230
|
});
|
|
286
231
|
```
|
|
287
232
|
|
|
288
|
-
[
|
|
289
|
-
|
|
290
|
-
### События
|
|
291
|
-
|
|
292
|
-
```ts
|
|
293
|
-
engine.on('snap', ({ id, size, progress }) => {}); // settled state change
|
|
294
|
-
engine.on('open', ({ id }) => {}); // 0 → >0 (enter starts)
|
|
295
|
-
engine.on('opened', ({ id }) => {}); // enter settled
|
|
296
|
-
engine.on('close', () => {}); // → 0 (exit starts)
|
|
297
|
-
engine.on('closed', () => {}); // exit settled
|
|
298
|
-
engine.on('dragstart', ({ size }) => {});
|
|
299
|
-
engine.on('drag', ({ size, delta }) => {}); // ~60 fps — payload is pooled, don't retain
|
|
300
|
-
engine.on('dragend', ({ size, velocity }) => {});
|
|
301
|
-
engine.on('progress', ({ value, size }) => {}); // ~60 fps
|
|
302
|
-
|
|
303
|
-
// Cancelable — call cancel() synchronously to veto the transition:
|
|
304
|
-
engine.on('before-snap', ({ id, size, previousId, cancel }) => {});
|
|
305
|
-
engine.on('before-close', ({ reason, cancel }) => {}); // reason: programmatic | backdrop | escape | back
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
### Императивный API
|
|
233
|
+
[Гайд по Qwik →](docs/qwik.md)
|
|
309
234
|
|
|
310
|
-
|
|
311
|
-
engine.snapTo('half');
|
|
312
|
-
engine.open(); // → first allowed snap with resolved size > 0
|
|
313
|
-
engine.close(reason?); // → first snap that resolves to size 0, else first allowed
|
|
314
|
-
engine.expand(); // → largest allowed snap
|
|
315
|
-
engine.collapse(); // → smallest allowed snap with size > 0
|
|
316
|
-
engine.setAllowed(ids, snapId); // dynamic allowlist
|
|
317
|
-
engine.setSnapPoints(points); // re-measure
|
|
318
|
-
engine.recompute(); // re-measure a 'fit' / 'content' snap on demand
|
|
319
|
-
engine.getResolvedSnaps(); // → [{ id, size }] resolved to pixels right now
|
|
320
|
-
engine.setPersistent(bool); // toggle persistence (block dismissal) at runtime
|
|
321
|
-
engine.setDisableClose(bool); // toggle close-blocking at runtime
|
|
322
|
-
engine.setDisableDrag(bool); // toggle the drag gesture at runtime
|
|
323
|
-
engine.setRadius('28px'); // corner radius
|
|
324
|
-
engine.setMaxHeight('92dvh'); // height cap (re-resolved on viewport change)
|
|
325
|
-
engine.canDismiss(); // false when persistent / disableClose
|
|
326
|
-
engine.isTop(); // top of the multi-sheet stack?
|
|
327
|
-
engine.depth(); // open sheets above this one
|
|
328
|
-
engine.destroy(); // remove listeners + cancel animation
|
|
329
|
-
```
|
|
235
|
+
</details>
|
|
330
236
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
для анонса нет). Вызывать `open()` / `snapTo()` **синхронно в тике монтирования
|
|
334
|
-
безопасно** — задержка через `requestAnimationFrame` / `nextTick` не нужна, в том
|
|
335
|
-
числе для `fit` / `content` и телепортированных sheet. Sheet, смонтированный
|
|
336
|
-
скрытым (предок с `display:none`), «лечится» и один раз испускает свою
|
|
337
|
-
последовательность открытия при появлении. См.
|
|
338
|
-
[Architecture → Opening at construction](docs/ARCHITECTURE.md).
|
|
237
|
+
<details>
|
|
238
|
+
<summary><b>Веб-компонент</b> (Angular, Lit, Astro, обычный HTML)</summary>
|
|
339
239
|
|
|
340
|
-
|
|
240
|
+
```html
|
|
241
|
+
<script type="module">
|
|
242
|
+
import '@surdeddd/bottom-sheet/element';
|
|
243
|
+
</script>
|
|
341
244
|
|
|
342
|
-
|
|
343
|
-
свойства scrim можно менять в рантайме — перемонтирование не нужно.
|
|
245
|
+
<button onclick="document.querySelector('bottom-sheet').open()">Filters</button>
|
|
344
246
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
| `setScrimColor(color \| null)` / `setScrimBlur(blur \| null)` | Живое изменение цвета и blur через backdrop-filter. `null` сбрасывает. |
|
|
351
|
-
| `setScrimInteractive(boolean)` | Переключает `pointer-events` на слое scrim. |
|
|
352
|
-
| `setBackdropRange([s, e])` / `setScreenRange([s, e])` | Отображение диапазона на opacity (progress 0..1 → opacity 0..1). |
|
|
353
|
-
| `setScrim({ ... })` | Пакетное обновление — применяет preset и отдельные поля за один проход с единственным `applySize`. |
|
|
354
|
-
| `setScrimOverlay({ children, position, interactive })` | Вставляет позиционированный плавающий элемент в область scrim. Возвращает функцию снятия. Полезно для мониторинговых UX вида «затемнить всё, кроме этого бейджа». |
|
|
355
|
-
| `getScrimState()` | Read-only снимок `{ mode, enabled }`. Стабильный публичный API для интроспекции без привязки к внутренностям. |
|
|
247
|
+
<bottom-sheet snap-points="closed:0,open:fit" focus-trap="true">
|
|
248
|
+
<h2 slot="header">Filters</h2>
|
|
249
|
+
<form>…</form>
|
|
250
|
+
</bottom-sheet>
|
|
251
|
+
```
|
|
356
252
|
|
|
357
|
-
|
|
253
|
+
Стили элемент несёт в собственном shadow root, отдельная таблица стилей не
|
|
254
|
+
нужна. Без сборщика подключите его с CDN:
|
|
358
255
|
|
|
359
|
-
|
|
256
|
+
```html
|
|
257
|
+
<script src="https://unpkg.com/@surdeddd/bottom-sheet/dist/element.iife.global.js"></script>
|
|
258
|
+
```
|
|
360
259
|
|
|
361
|
-
|
|
362
|
-
панель, которая повторяет для контента ту же runtime-изменяемость, что и scrim.
|
|
260
|
+
[Гайд по веб-компоненту →](docs/web-component.md)
|
|
363
261
|
|
|
364
|
-
|
|
365
|
-
| --- | --- |
|
|
366
|
-
| `setOverlayChildren(node \| fragment \| factory)` | Заменяет содержимое overlay в рантайме. Принимает `Node`, `DocumentFragment` или фабрику `() => Node`. |
|
|
367
|
-
| `clearOverlayChildren()` | Удаляет вставленный контент. |
|
|
368
|
-
| `setOverlay({ children, ... })` | Пакетное обновление — `children` входит в число допустимых полей наряду с позиционированием и переопределениями preset. |
|
|
262
|
+
</details>
|
|
369
263
|
|
|
370
|
-
|
|
371
|
-
|
|
264
|
+
<details>
|
|
265
|
+
<summary><b>Без фреймворка</b></summary>
|
|
372
266
|
|
|
373
|
-
|
|
267
|
+
```html
|
|
268
|
+
<div class="bs-root">
|
|
269
|
+
<div class="bs-backdrop"></div>
|
|
270
|
+
<section class="bs-sheet" data-mode="bottom" role="dialog" aria-label="Filters">
|
|
271
|
+
<div class="bs-handle" role="slider" tabindex="0" aria-label="Resize sheet"></div>
|
|
272
|
+
<div class="bs-content">…</div>
|
|
273
|
+
</section>
|
|
274
|
+
</div>
|
|
275
|
+
```
|
|
374
276
|
|
|
375
277
|
```ts
|
|
376
|
-
import {
|
|
278
|
+
import { BottomSheetEngine } from '@surdeddd/bottom-sheet';
|
|
279
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
377
280
|
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
281
|
+
const root = document.querySelector('.bs-root')!;
|
|
282
|
+
const sheet = new BottomSheetEngine({
|
|
283
|
+
element: root.querySelector('.bs-sheet')!,
|
|
284
|
+
handle: root.querySelector('.bs-handle')!,
|
|
285
|
+
scrollContainer: root.querySelector('.bs-content')!,
|
|
286
|
+
backdrop: root.querySelector('.bs-backdrop')!,
|
|
287
|
+
snapPoints: [
|
|
288
|
+
{ id: 'closed', size: 0 },
|
|
289
|
+
{ id: 'open', size: 'fit' },
|
|
290
|
+
],
|
|
291
|
+
initial: 'closed',
|
|
381
292
|
});
|
|
382
293
|
|
|
383
|
-
|
|
384
|
-
sheets.transition(prev, next);
|
|
385
|
-
const cfg = sheets.resolve(currentRoute); // typed!
|
|
294
|
+
document.querySelector('#filters')!.addEventListener('click', () => sheet.open());
|
|
386
295
|
```
|
|
387
296
|
|
|
388
|
-
|
|
297
|
+
[Гайд по движку без фреймворка →](docs/vanilla.md)
|
|
389
298
|
|
|
390
|
-
|
|
299
|
+
</details>
|
|
391
300
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
2. **Sub-stepping spring с единственной записью в DOM.** Жёсткие пружины
|
|
397
|
-
интегрируются внутри на 240 Гц ради стабильности, но движок пишет в DOM
|
|
398
|
-
ровно один раз за `requestAnimationFrame`.
|
|
399
|
-
3. **React-снимок только установившегося состояния.** `useSyncExternalStore`
|
|
400
|
-
обновляется лишь на `snap` / `dragstart` / `dragend`. Пиксели drag и кадры
|
|
401
|
-
анимации передаются через CSS-переменные (`--bs-progress`, `--bs-size`) и
|
|
402
|
-
прямые императивные подписки — никаких ре-рендеров React на каждом кадре.
|
|
301
|
+
Preact: `@surdeddd/bottom-sheet/preact` — это React-адаптер, работающий через
|
|
302
|
+
`preact/compat` ([гайд](docs/preact.md)). В Angular, Lit и Astro используется
|
|
303
|
+
веб-компонент или сам движок: [Angular](docs/angular.md), [Lit](docs/lit.md),
|
|
304
|
+
[Astro](docs/astro.md).
|
|
403
305
|
|
|
404
|
-
|
|
306
|
+
Гайды в `docs/` написаны на английском.
|
|
405
307
|
|
|
406
|
-
|
|
407
|
-
поведениями. Если нужно только необходимое, импортируйте slim core (~17.5 KB
|
|
408
|
-
gzip против ~23 KB) и подключайте только нужные фичи:
|
|
308
|
+
## Основные понятия
|
|
409
309
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
import { routeFeature, contentSwipeFeature } from '@surdeddd/bottom-sheet/features';
|
|
310
|
+
**Точки привязки** (snap points) — размеры, на которых шторка может
|
|
311
|
+
остановиться. У каждой есть `id`, по которому к ней обращаются, и `size`:
|
|
413
312
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
313
|
+
```ts
|
|
314
|
+
type SnapPoint =
|
|
315
|
+
| number // пиксели
|
|
316
|
+
| `${number}%` // доля вьюпорта вдоль оси шторки
|
|
317
|
+
| 'fit' | 'content' // высота самой шторки: ручка, шапка, содержимое, подвал
|
|
318
|
+
| 'full' // вся ось
|
|
319
|
+
| string; // любая длина CSS: "50dvh", "clamp(200px, 60%, 800px)"
|
|
419
320
|
```
|
|
420
321
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
322
|
+
Точка размером `0` — это **закрытое** состояние. Шторку без такой точки
|
|
323
|
+
нельзя закрыть, только свернуть до самой маленькой.
|
|
324
|
+
|
|
325
|
+
**Три слоя.** Сама *шторка*, *бэкдроп*, который затемняет страницу, и
|
|
326
|
+
необязательный *скрим* (`screen`) для содержимого, которое проявляется за
|
|
327
|
+
шторкой: превью карты, размытая подложка.
|
|
328
|
+
|
|
329
|
+
**Состояние** — один объект: `{ activeId, size, progress, phase, isDragging,
|
|
330
|
+
isAnimating }`. `progress` идёт от 0 на самой маленькой разрешённой точке до
|
|
331
|
+
1 на самой большой. `phase` принимает значения `closed`, `opening`, `open` и
|
|
332
|
+
`closing`.
|
|
333
|
+
|
|
334
|
+
## API
|
|
335
|
+
|
|
336
|
+
Все опции, методы и события ниже работают одинаково в каждом адаптере.
|
|
337
|
+
Компоненты принимают опции как пропсы (`focus-trap` в шаблонах Vue,
|
|
338
|
+
`focus-trap="true"` у веб-компонента).
|
|
339
|
+
|
|
340
|
+
### Опции
|
|
341
|
+
|
|
342
|
+
Размер и положение
|
|
343
|
+
|
|
344
|
+
| Опция | По умолчанию | Что делает |
|
|
345
|
+
| --- | --- | --- |
|
|
346
|
+
| `snapPoints` | обязательна | Список `{ id, size }` |
|
|
347
|
+
| `allowed` | все id | Точки, на которых шторка может остановиться сейчас |
|
|
348
|
+
| `initial` | первая разрешённая | Точка, с которой шторка начинает. Если её размер больше `0`, шторка появляется открытой, без события `open` |
|
|
349
|
+
| `mode` | `"bottom"` | Край: `bottom`, `top`, `left`, `right` или `start` / `end` |
|
|
350
|
+
| `maxHeight` | нет | Предел размера шторки: пиксели или длина CSS, которая пересчитывается при изменении вьюпорта |
|
|
351
|
+
| `radius` | `20px` | Радиус скругления |
|
|
352
|
+
| `fitContentToSnap` | `false` | Вся прокручиваемая область и подвал достижимы на каждой точке, а не только на самой большой. [Подробнее](docs/content-fit.md) |
|
|
353
|
+
|
|
354
|
+
Движение
|
|
355
|
+
|
|
356
|
+
| Опция | По умолчанию | Что делает |
|
|
357
|
+
| --- | --- | --- |
|
|
358
|
+
| `animation` | `"spring"` | `spring`, `ios-spring`, `material-bounce`, `snappy`, `tween`, `linear` |
|
|
359
|
+
| `spring` | из пресета | `{ stiffness, damping, mass }` |
|
|
360
|
+
| `duration`, `easing` | `220`, ease-out-back | Длительность в мс и кривая `(t) => number` для анимации `tween` |
|
|
361
|
+
| `settleAnimation` | выключена | `"waapi"` выполняет посадку как Web Animation на композиторе |
|
|
362
|
+
| `respectReducedMotion` | `true` | При `prefers-reduced-motion: reduce` шторка сразу встаёт на точку |
|
|
363
|
+
| `viewTransitions` | `false` | Переход между точками через `document.startViewTransition()`, если браузер его умеет |
|
|
364
|
+
|
|
365
|
+
Жесты
|
|
366
|
+
|
|
367
|
+
| Опция | По умолчанию | Что делает |
|
|
368
|
+
| --- | --- | --- |
|
|
369
|
+
| `dragFrom` | `"handle"` | Откуда можно начать перетаскивание: с ручки (`handle`), со всей шторки (`sheet`) или только из зон `[data-bs-drag]` (`zones`). `[data-bs-no-drag]` исключает поддерево. У шторки без ручки по умолчанию `sheet` |
|
|
370
|
+
| `dragFromContent` | `true` | Тянет ли шторку касание по прокручиваемой области. Можно задать для отдельной точки: `{ id, size, dragFromContent: false }` |
|
|
371
|
+
| `disableDrag` | `false` | Отключает жест; методы продолжают работать |
|
|
372
|
+
| `rubberBand` | `true` | Мягкое сопротивление за крайней точкой |
|
|
373
|
+
| `flickVelocity` | `0.65` px/мс | Скорость, с которой отпускание пальца или пера считается броском. Для мыши порог фиксированный: 0.4 |
|
|
374
|
+
| `dragThreshold` | `18` px | Перетаскивание пальцем или пером короче этого возвращает шторку назад. Для мыши порог фиксированный: 12 |
|
|
375
|
+
|
|
376
|
+
Закрытие
|
|
377
|
+
|
|
378
|
+
| Опция | По умолчанию | Что делает |
|
|
379
|
+
| --- | --- | --- |
|
|
380
|
+
| `closeOnEscape` | `true` | Escape закрывает верхнюю шторку |
|
|
381
|
+
| `closeOnBack` | `false` | Кнопка «Назад» закрывает шторку и не уводит со страницы |
|
|
382
|
+
| `routedTo` | нет | URL, который показывается, пока шторка открыта; «Назад» закрывает её и возвращает адрес |
|
|
383
|
+
| `closeOnRouteChange` | `false` | Закрыть при смене URL |
|
|
384
|
+
| `persistent` | `false` | Пользователь не может закрыть шторку: ни тапом по бэкдропу, ни Escape, ни «Назад», ни жестом, ни клавишами. `close()` по-прежнему закрывает |
|
|
385
|
+
| `disableClose` | `false` | Шторку не закрывает ничто, включая `close()` |
|
|
386
|
+
|
|
387
|
+
Фокус и доступность
|
|
388
|
+
|
|
389
|
+
| Опция | По умолчанию | Что делает |
|
|
390
|
+
| --- | --- | --- |
|
|
391
|
+
| `focusTrap` | `false` | Переносит фокус в шторку при открытии, держит Tab внутри, пока она верхняя, и возвращает фокус при закрытии. Попап, который рисуется в другом месте, пометьте `data-bs-focus-allow`, и фокус сможет туда уйти |
|
|
392
|
+
| `initialFocus` | первый фокусируемый | Вместе с `focusTrap`: элемент или селектор, который получит фокус при открытии. Первый фокусируемый элемент — ручка; `false` ставит фокус на саму шторку |
|
|
393
|
+
| `returnFocusTo` | нет | Элемент, селектор или функция: куда отправить фокус при закрытии. Ловушка фокуса и так возвращает его туда, где он был |
|
|
394
|
+
| `inertSiblings` | `false` | Делает всё вне шторки `inert`, пока она верхняя |
|
|
395
|
+
| `lockBodyScroll` | `true` | Блокирует прокрутку страницы, пока шторка открыта |
|
|
396
|
+
|
|
397
|
+
Бэкдроп и скрим
|
|
398
|
+
|
|
399
|
+
| Опция | По умолчанию | Что делает |
|
|
400
|
+
| --- | --- | --- |
|
|
401
|
+
| `backdropRange` | `[0, 1]` | Отрезок `progress`, на котором проявляется бэкдроп |
|
|
402
|
+
| `backdropOpacity` | `1` | Непрозрачность, которой бэкдроп достигает в конце этого отрезка |
|
|
403
|
+
| `backdropColor` | `rgb(0 0 0 / 0.5)` | Цвет бэкдропа; то же, что токен `--bs-backdrop-color` |
|
|
404
|
+
| `screenRange` | `[0, 1]` | Такой же отрезок для скрима |
|
|
405
|
+
| `scrimMode` | `"full"` | `full` — на весь экран, `above-sheet` — до края шторки, `off` — без слоя |
|
|
406
|
+
| `scrimPreset` | нет | `subtle`, `standard`, `monitoring`, `cinematic`: цвет, размытие и отрезок одной опцией |
|
|
407
|
+
| `scrimColor`, `scrimBlur` | из пресета | Заливка скрима и радиус его размытия `backdrop-filter` |
|
|
408
|
+
| `scrimInteractive` | `false` | Скрим принимает указатель сам, а не пропускает его на страницу |
|
|
409
|
+
| `scrimTapToClose` | `false` | Тап по скриму закрывает шторку |
|
|
410
|
+
|
|
411
|
+
Остальное
|
|
412
|
+
|
|
413
|
+
| Опция | По умолчанию | Что делает |
|
|
414
|
+
| --- | --- | --- |
|
|
415
|
+
| `stackEffect` | `false` | Уменьшает шторки под верхней, как стопка карточек в iOS |
|
|
416
|
+
| `persistKey` | нет | Ключ `localStorage`: шторка начинает с точки, на которой её оставили |
|
|
417
|
+
| `autoCollapseAfter` | выключена | Через сколько миллисекунд покоя шторка вернётся на самую маленькую открытую точку |
|
|
418
|
+
| `linkedSheets` | нет | Другие движки, которые сворачиваются, когда открывается этот |
|
|
419
|
+
| `features` | все | Список возможностей для [облегчённого ядра](#облегчённое-ядро) |
|
|
420
|
+
|
|
421
|
+
Компоненты добавляют несколько собственных пропсов:
|
|
422
|
+
|
|
423
|
+
| Проп | По умолчанию | Что делает |
|
|
424
|
+
| --- | --- | --- |
|
|
425
|
+
| `open`, `onOpenChange` | неуправляемый | Управляемое состояние «открыта». `v-model:open` во Vue, `bind:open` в Svelte, `onOpenChange$` в Qwik, атрибут `open` у веб-компонента |
|
|
426
|
+
| `snap` | неуправляемый | Активная точка привязки как проп: `v-model:snap` во Vue, `bind:snap` в Svelte, `snap` вместе с `onSnap$` в Qwik, атрибут `snap` у веб-компонента. В React и Solid для этого есть ref |
|
|
427
|
+
| `header`, `footer` | нет | Области над прокручиваемой частью и под ней (слоты во Vue, сниппеты в Svelte) |
|
|
428
|
+
| `backdrop` | `true` | Рисовать бэкдроп |
|
|
429
|
+
| `closeOnBackdrop` | `true` | Тап по бэкдропу закрывает шторку |
|
|
430
|
+
| `teleportTo` | нет | Переносит DOM шторки из предка, который её обрезает или трансформирует |
|
|
431
|
+
| `ariaLabel` | `"Bottom sheet"` | Доступное имя шторки |
|
|
432
|
+
| `handleLabel`, `contentLabel` | на английском | Доступные имена ручки и прокручиваемой области, для приложений на другом языке |
|
|
433
|
+
|
|
434
|
+
### Методы
|
|
431
435
|
|
|
432
436
|
```ts
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
437
|
+
sheet.open(); // закрыта → первая разрешённая открытая точка; открытая остаётся на месте
|
|
438
|
+
sheet.open('half'); // на конкретную точку
|
|
439
|
+
sheet.close(); // → закрытая точка; если её нет, самая маленькая
|
|
440
|
+
sheet.snapTo('full');
|
|
441
|
+
sheet.expand(); // → самая большая разрешённая точка
|
|
442
|
+
sheet.collapse(); // → самая маленькая разрешённая открытая точка
|
|
443
|
+
|
|
444
|
+
sheet.setAllowed(['half', 'full']); // сменить набор разрешённых точек
|
|
445
|
+
sheet.setSnapPoints(points); // заменить точки
|
|
446
|
+
sheet.recompute(); // перемерить точку 'fit'
|
|
447
|
+
sheet.setMode('right');
|
|
448
|
+
sheet.setPersistent(true);
|
|
449
|
+
sheet.setDisableClose(true);
|
|
450
|
+
sheet.setDisableDrag(true);
|
|
451
|
+
sheet.setRadius('28px');
|
|
452
|
+
sheet.setMaxHeight('92dvh');
|
|
453
|
+
sheet.setBackdropOpacity(0.6);
|
|
454
|
+
sheet.setBackdropColor('rgb(0 0 0 / 0.4)');
|
|
455
|
+
|
|
456
|
+
sheet.state; // { activeId, size, progress, phase, isDragging, isAnimating }
|
|
457
|
+
sheet.getResolvedSnaps(); // [{ id, size }] в пикселях на этот момент
|
|
458
|
+
sheet.canDismiss(); // false при persistent или disableClose
|
|
459
|
+
sheet.isTop(); // верхняя ли она в стеке
|
|
460
|
+
sheet.destroy();
|
|
439
461
|
```
|
|
440
462
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
DOM. Layout, прокрутка, жесты и дерево доступности из DOM никуда не уходят.
|
|
445
|
-
4.2 KB gzip, и только если вы его импортируете.
|
|
446
|
-
Откатывается на DOM-рендерер — молча и даже посреди сессии, если потребуется, —
|
|
447
|
-
когда WebGL недоступен, при `prefers-reduced-motion` или если браузер сбросил
|
|
448
|
-
GL-контекст.
|
|
463
|
+
`open()`, `close()` и `snapTo()` возвращают промис, который завершается,
|
|
464
|
+
когда шторка встала на место. Их можно вызывать в том же тике, в котором
|
|
465
|
+
шторка смонтирована.
|
|
449
466
|
|
|
450
|
-
|
|
467
|
+
Если `setAllowed()` или `setSnapPoints()` убирают активную точку, открытая
|
|
468
|
+
шторка переходит на ближайшую точку, на которой она остаётся открытой.
|
|
451
469
|
|
|
452
|
-
|
|
470
|
+
В компонентах эти методы доступны через ref (`ref.current.open()`), у
|
|
471
|
+
веб-компонента — на самом элементе (`el.open()`), у композабла — на
|
|
472
|
+
объекте, который он возвращает. Всё, чего там нет, достаётся через
|
|
473
|
+
`getEngine()`.
|
|
453
474
|
|
|
454
|
-
|
|
455
|
-
import { attachDebugOverlay } from '@surdeddd/bottom-sheet/debug';
|
|
475
|
+
### События
|
|
456
476
|
|
|
457
|
-
|
|
477
|
+
```ts
|
|
478
|
+
sheet.on('open', ({ id }) => {}); // шторка начинает появляться
|
|
479
|
+
sheet.on('opened', ({ id }) => {}); // встала на место
|
|
480
|
+
sheet.on('close', () => {}); // начинает уходить
|
|
481
|
+
sheet.on('closed', () => {}); // ушла: размонтировать нужно здесь
|
|
482
|
+
sheet.on('snap', ({ id, size, progress }) => {}); // остановилась на точке
|
|
483
|
+
|
|
484
|
+
sheet.on('dragstart', ({ size }) => {});
|
|
485
|
+
sheet.on('drag', ({ size, delta }) => {}); // каждый кадр
|
|
486
|
+
sheet.on('dragend', ({ size, velocity }) => {});
|
|
487
|
+
sheet.on('progress', ({ value, size }) => {}); // каждый кадр любого движения
|
|
488
|
+
|
|
489
|
+
sheet.on('before-snap', ({ id, previousId, cancel }) => {});
|
|
490
|
+
sheet.on('before-close', ({ reason, cancel }) => {});
|
|
458
491
|
```
|
|
459
492
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
493
|
+
Чтобы остановить переход, вызовите `cancel()` синхронно внутри `before-snap`
|
|
494
|
+
или `before-close`; `reason` — это `programmatic`, `backdrop`, `escape` или
|
|
495
|
+
`back`.
|
|
496
|
+
|
|
497
|
+
Блокировка прокрутки, ловушка фокуса и запись в истории включаются на
|
|
498
|
+
`open`. Запись в истории снимается на `close`, блокировка и ловушка — на
|
|
499
|
+
`closed`, когда шторки уже нет. Переход, развёрнутый на полпути, снова
|
|
500
|
+
вызывает `open` или `close`, поэтому эти два события всегда чередуются.
|
|
464
501
|
|
|
465
|
-
|
|
502
|
+
В компонентах события приходят как пропсы `onSnap`, `onOpen`… (React,
|
|
503
|
+
Solid), `@snap`, `@open`… (Vue), `onsnap`… (Svelte), `onSnap$`… (Qwik) и как
|
|
504
|
+
DOM-события у веб-компонента.
|
|
466
505
|
|
|
467
|
-
|
|
468
|
-
`dist/qwik-lib/index.qwik.mjs`. Приложения на Qwik подхватывают адаптер
|
|
469
|
-
автоматически как vendor root: оптимизатор на стороне потребителя режет его на
|
|
470
|
-
QRL-чанки, а `renderToString` выдаёт возобновляемый (`q:container="paused"`)
|
|
471
|
-
sheet. Дополнительная конфигурация не нужна —
|
|
472
|
-
`import { BottomSheet } from '@surdeddd/bottom-sheet/qwik'` по-прежнему
|
|
473
|
-
работает для CSR, а Qwik City SSR теперь работает из коробки.
|
|
506
|
+
### Клавиатура
|
|
474
507
|
|
|
475
|
-
|
|
508
|
+
Ручка работает как слайдер. Стрелка, направленная от края шторки, увеличивает
|
|
509
|
+
её (`ArrowUp` для нижней, `ArrowLeft` для правой), противоположная уменьшает,
|
|
510
|
+
`Home` и `End` переводят на самую маленькую и самую большую точку, `Escape`
|
|
511
|
+
закрывает.
|
|
476
512
|
|
|
477
|
-
|
|
513
|
+
### Остальной API
|
|
514
|
+
|
|
515
|
+
| Тема | Гайд |
|
|
516
|
+
| --- | --- |
|
|
517
|
+
| Кнопки и панели, закреплённые у края шторки; содержимое скрима для разных состояний | [Anchors and scrim stages](docs/anchors.md) |
|
|
518
|
+
| Изменение скрима на лету | [Theming → the scrim](docs/theming.md#the-scrim) |
|
|
519
|
+
| Лёгкая выезжающая панель без точек привязки | [Overlay](docs/overlay.md) |
|
|
520
|
+
| Шторки, настроенные по маршрутам | [Vanilla → sheet manager](docs/vanilla.md#sheet-manager) |
|
|
521
|
+
| Навигация сразу после закрытия при `closeOnBack` | [Vanilla → waiting for the history](docs/vanilla.md#waiting-for-the-history) |
|
|
522
|
+
| Движение шторки из кода, покадрово | [Recipe: programmatic drag](docs/recipes/programmatic-drag.md) |
|
|
523
|
+
| Свои расширения движка | [Plugins](docs/plugins.md) |
|
|
524
|
+
| Интерфейсы справа налево | [RTL](docs/rtl.md) |
|
|
525
|
+
|
|
526
|
+
## Стилизация
|
|
527
|
+
|
|
528
|
+
Всё визуальное задано пользовательскими свойствами CSS. Их можно выставить на
|
|
529
|
+
`.bs-root`, на любом предке или на `:root`; до shadow root веб-компонента они
|
|
530
|
+
тоже доходят.
|
|
478
531
|
|
|
479
532
|
```css
|
|
480
533
|
.bs-root {
|
|
481
534
|
--bs-surface: #18181b;
|
|
482
|
-
--bs-handle-color: #71717a;
|
|
483
535
|
--bs-radius: 28px;
|
|
484
|
-
--bs-
|
|
536
|
+
--bs-handle-color: #71717a;
|
|
537
|
+
--bs-backdrop-color: rgb(0 0 0 / 0.6);
|
|
485
538
|
}
|
|
486
539
|
```
|
|
487
540
|
|
|
488
|
-
Движок
|
|
489
|
-
|
|
541
|
+
Движок на каждом кадре пишет `--bs-progress` (от 0 до 1) и `--bs-size`
|
|
542
|
+
(пиксели) на шторку и на `.bs-root`, поэтому всё, что лежит внутри корня,
|
|
543
|
+
например слой `screen` или плавающая кнопка, следует за шторкой без
|
|
544
|
+
JavaScript:
|
|
490
545
|
|
|
491
546
|
```css
|
|
492
|
-
.
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
.map-canvas {
|
|
496
|
-
filter: blur(calc(var(--bs-progress) * 8px));
|
|
497
|
-
}
|
|
498
|
-
.search-input {
|
|
499
|
-
opacity: calc(1 - var(--bs-progress));
|
|
500
|
-
}
|
|
547
|
+
.bs-root .map-preview { filter: blur(calc(var(--bs-progress) * 8px)); }
|
|
548
|
+
.bs-root .fab { bottom: calc(var(--bs-size) + 16px); }
|
|
501
549
|
```
|
|
502
550
|
|
|
503
|
-
|
|
504
|
-
> перезаписывает inline-`z-index` у `.bs-sheet`, `.bs-backdrop` и обёрток
|
|
505
|
-
> anchor (база `100`, шаг `10` на уровень глубины), поэтому заданное вами
|
|
506
|
-
> значение будет перезаписано и начнёт конфликтовать со стеком. Чтобы
|
|
507
|
-
> расположить слой sheet относительно UI приложения, переместите контейнер
|
|
508
|
-
> (цель teleport / родителя `.bs-root`) или измените порядок открытия — не
|
|
509
|
-
> стилизуйте `z-index` у элементов, которыми управляет bs. См.
|
|
510
|
-
> [Architecture → z-index & the sheet stack](docs/ARCHITECTURE.md).
|
|
511
|
-
|
|
512
|
-
**У каждого sheet свой backdrop.** Откройте список, затем поверх него sheet с
|
|
513
|
-
деталями — оба backdrop остаются активными: затемнение усиливается с каждым
|
|
514
|
-
уровнем, а нижний sheet остаётся замаскированным всё время, пока закрывается
|
|
515
|
-
верхний, так что касания не доходят до страницы посреди анимации. Каждый
|
|
516
|
-
backdrop следует за progress собственного sheet, поэтому закрывающийся
|
|
517
|
-
«погребённый» sheet убирает свой scrim. Стек сохраняет порядок, в котором
|
|
518
|
-
sheet начали открываться: sheet, открытый, пока другой ещё анимируется, остаётся
|
|
519
|
-
сверху. Если нужен единый уровень затемнения, передайте `backdrop={false}`
|
|
520
|
-
(`:backdrop="false"` во Vue) тем sheet, которые открываете поверх.
|
|
521
|
-
|
|
522
|
-
### Контент достижим на каждой snap point
|
|
523
|
-
|
|
524
|
-
Sheet всегда имеет высоту своей самой большой snap point и двигается через
|
|
525
|
-
transform — именно это держит drag на compositor. Цена: ниже самой большой
|
|
526
|
-
snap point низ scroll-контейнера оказывается под краем экрана ровно на
|
|
527
|
-
`largest snap − current size`. На половинной snap point прокручивать можно, но
|
|
528
|
-
последние строки не появятся, пока sheet не вытянут выше.
|
|
529
|
-
|
|
530
|
-
`fitContentToSnap` закрывает этот зазор:
|
|
551
|
+
Три готовых оформления лежат в отдельных таблицах стилей:
|
|
531
552
|
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
<LongList />
|
|
535
|
-
</BottomSheet>
|
|
553
|
+
```ts
|
|
554
|
+
import '@surdeddd/bottom-sheet/themes/ios'; // а также /material, /vercel
|
|
536
555
|
```
|
|
537
556
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
scroll-контейнер заканчивается у края экрана: последняя строка, поле в фокусе
|
|
541
|
-
и полоса прокрутки остаются в видимой части, а ползунок рассчитан на то, что
|
|
542
|
-
реально видно. В момент начала движения sheet переключается на контейнер
|
|
543
|
-
полной высоты со спейсером такого же размера в конце списка, поэтому строки
|
|
544
|
-
ниже края уже разложены, когда drag или анимация их открывает. Переключение
|
|
545
|
-
меняет и бокс, и диапазон прокрутки на одну и ту же величину, так что на
|
|
546
|
-
экране ничего не сдвигается. Это один layout при старте движения и один при
|
|
547
|
-
остановке, ни одного на кадр, поэтому движение остаётся чисто transform-овым.
|
|
548
|
-
Пока sheet движется, список, прокрученный до самого конца, удерживается на
|
|
549
|
-
месте на экране, а не уезжает вверх вместе с sheet и не откатывается обратно
|
|
550
|
-
в конце.
|
|
551
|
-
|
|
552
|
-
У footer та же проблема, и лечится она так же. Он последний в sheet, поэтому
|
|
553
|
-
ниже самой большой snap point он оказывается под краем экрана вместе с концом
|
|
554
|
-
списка. При включённом `fitContentToSnap` уменьшенный контейнер ставит footer
|
|
555
|
-
у низа видимой части sheet в покое, а пока sheet движется, стили поднимают
|
|
556
|
-
`.bs-footer` на `--bs-size − --bs-max-size`, так что он едет по этому краю на
|
|
557
|
-
каждом кадре drag или анимации — опять же без layout. Список заканчивается над
|
|
558
|
-
ним, а не под ним. На snap point, слишком короткой для обоих, footer уходит под
|
|
559
|
-
край экрана, а не закрывает handle. WebGL-рендерер получает ту же раскладку;
|
|
560
|
-
пока sheet движется, его footer закрашивается цветом поверхности, а drag
|
|
561
|
-
переносит список в текстуру, которая следует за позицией прокрутки списка, так
|
|
562
|
-
что список, удерживаемый в конце, остаётся там же.
|
|
563
|
-
|
|
564
|
-
#### Прокрутка списка на каждой snap point на таче
|
|
565
|
-
|
|
566
|
-
Сделать конец списка *достижимым* и дать пальцу *прокрутить* до него — две
|
|
567
|
-
разные вещи. По умолчанию свайп, начатый на контенте, тянет sheet, пока он не
|
|
568
|
-
дойдёт до самой большой snap point, и только потом прокручивает список — так
|
|
569
|
-
ведут себя нативные sheet. На мыши или трекпаде колесо прокручивает на любой
|
|
570
|
-
snap point, на таче — нет. Чтобы прокручивать пальцем на половинной snap point,
|
|
571
|
-
отдайте свайпы по контенту списку, а handle (и header) оставьте для движения
|
|
572
|
-
sheet:
|
|
573
|
-
|
|
574
|
-
```tsx
|
|
575
|
-
<BottomSheet snapPoints={points} fitContentToSnap dragFromContent={false}>
|
|
576
|
-
<LongList />
|
|
577
|
-
</BottomSheet>
|
|
557
|
+
```html
|
|
558
|
+
<div class="bs-theme-ios"><!-- шторка --></div>
|
|
578
559
|
```
|
|
579
560
|
|
|
580
|
-
|
|
581
|
-
нативное ощущение pull-to-expand на маленьком peek и освобождает список начиная
|
|
582
|
-
с половины:
|
|
561
|
+
[Все токены, темы, порядок наложения и части веб-компонента →](docs/theming.md)
|
|
583
562
|
|
|
584
|
-
|
|
585
|
-
const points = [
|
|
586
|
-
{ id: "peek", size: 120 },
|
|
587
|
-
{ id: "half", size: "50%", dragFromContent: false },
|
|
588
|
-
{ id: "full", size: "90%", dragFromContent: false },
|
|
589
|
-
];
|
|
590
|
-
```
|
|
563
|
+
## Доступность
|
|
591
564
|
|
|
592
|
-
|
|
593
|
-
потомок flex-колонки sheet, как `.bs-content`. Для любого другого контейнера
|
|
594
|
-
атрибут `data-bs-fit-content` на sheet принимает значения `rest` или `moving`,
|
|
595
|
-
а `--bs-content-inset` хранит скрытую величину: в покое уменьшайте контейнер на
|
|
596
|
-
неё, а при движении добавляйте её как отступ в конец содержимого. На slim core
|
|
597
|
-
добавьте `contentFitFeature()` из `@surdeddd/bottom-sheet/features`.
|
|
598
|
-
|
|
599
|
-
### Scrim, blur и высокий контраст
|
|
600
|
-
|
|
601
|
-
| Токен | По умолчанию | Область |
|
|
602
|
-
| ------------------------ | ------------ | -------------------------------------------------------- |
|
|
603
|
-
| `--bs-scrim-color` | transparent | Заливка слоя scrim (`.bs-screen`) |
|
|
604
|
-
| `--bs-scrim-blur` | `0px` | `backdrop-filter: blur()`, применяемый к слою scrim |
|
|
605
|
-
| `--bs-header-min-height` | `0px` | Минимальная высота области `.bs-header` |
|
|
606
|
-
|
|
607
|
-
`--bs-scrim-blur` включает настоящий blur через `backdrop-filter` на scrim —
|
|
608
|
-
задайте его для эффекта матового стекла (или используйте пресеты scrim
|
|
609
|
-
`monitoring` / `cinematic`, которые задают и цвет, и blur). В режиме Windows
|
|
610
|
-
High Contrast / `forced-colors` таблица стилей содержит блок
|
|
611
|
-
`@media (forced-colors: active)`, чтобы поверхность sheet, handle и кольцо
|
|
612
|
-
фокуса оставались видимыми.
|
|
613
|
-
|
|
614
|
-
### Отступы и раскладка
|
|
615
|
-
|
|
616
|
-
Отступы каждой области — это токен: переопределите или обнулите его, чтобы
|
|
617
|
-
контент шёл от края до края. Значения по умолчанию соответствуют встроенному
|
|
618
|
-
виду. Задавайте переменную на хосте / `.bs-root` / `:root`; custom properties
|
|
619
|
-
наследуются через shadow-границу Web Component, поэтому одно и то же
|
|
620
|
-
переопределение работает для всех адаптеров:
|
|
621
|
-
|
|
622
|
-
| Токен | По умолчанию | Область |
|
|
623
|
-
| ---------------------- | --------------------- | ----------------------- |
|
|
624
|
-
| `--bs-content-padding` | `0 16px 16px` | прокручиваемое тело |
|
|
625
|
-
| `--bs-handle-padding` | `8px 16px 4px` | полоса захвата (сверху) |
|
|
626
|
-
| `--bs-header-padding` | `0 16px 8px` | слот header |
|
|
627
|
-
| `--bs-footer-padding` | `12px 16px (+ inset)` | слот footer |
|
|
628
|
-
|
|
629
|
-
(Значение footer по умолчанию — `12px 16px calc(12px + env(safe-area-inset-bottom))`;
|
|
630
|
-
оно не даёт кнопкам действий оказаться под индикатором «Домой».)
|
|
565
|
+
Что встроено и что включается опцией:
|
|
631
566
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
567
|
+
- **Ручка — это слайдер.** У неё есть имя, текущее значение и шаги по
|
|
568
|
+
стрелкам, и она входит в порядок обхода по Tab. Назовите её на своём языке
|
|
569
|
+
через `handleLabel`; `contentLabel` делает то же для прокручиваемой
|
|
570
|
+
области.
|
|
571
|
+
- **У шторки есть имя.** `ariaLabel` или `ariaLabelledBy`, указывающий на
|
|
572
|
+
заголовок. С `focusTrap` шторка получает роль `dialog` и `aria-modal`.
|
|
573
|
+
- **Фокус.** `focusTrap` переносит фокус в шторку, держит Tab внутри и при
|
|
574
|
+
закрытии возвращает фокус туда, откуда шторку открыли. В стеке фокус держит
|
|
575
|
+
только верхняя шторка.
|
|
576
|
+
- **Страница за шторкой.** `inertSiblings` убирает всё вне верхней шторки из
|
|
577
|
+
досягаемости клавиатуры и экранных читалок.
|
|
578
|
+
- **Объявления.** Каждая остановка объявляется через вежливую live-область.
|
|
579
|
+
- **Движение и контраст.** При `prefers-reduced-motion` анимации заменяются
|
|
580
|
+
мгновенным переходом, при `forced-colors` шторка получает рамку, заметную
|
|
581
|
+
ручку и системное кольцо фокуса.
|
|
582
|
+
|
|
583
|
+
Модальной шторке нужны обе опции: `focusTrap` и `inertSiblings`.
|
|
584
|
+
|
|
585
|
+
## Формы и экранная клавиатура
|
|
586
|
+
|
|
587
|
+
Нижняя шторка встаёт на экранную клавиатуру, а не уходит под неё, поэтому
|
|
588
|
+
поле, в которое печатают, остаётся на виду. С `focusTrap` фокус при открытии
|
|
589
|
+
попадает на ручку, и клавиатура не поднимается, пока пользователь не коснётся
|
|
590
|
+
поля; `initialFocus="#email"` ставит фокус сразу в поле.
|
|
591
|
+
|
|
592
|
+
Чтобы шторка с несохранённым вводом не закрылась, отмените переход:
|
|
593
|
+
|
|
594
|
+
```ts
|
|
595
|
+
sheet.on('before-close', ({ cancel }) => {
|
|
596
|
+
if (form.isDirty) cancel();
|
|
597
|
+
});
|
|
636
598
|
```
|
|
637
599
|
|
|
638
|
-
|
|
639
|
-
snap point `'content'` / `'fit'`, превышающая ограничение, останавливается
|
|
640
|
-
вплотную к краю, а тело прокручивается внутри — sheet никогда не отрывается от
|
|
641
|
-
своей опоры, а `--bs-size` всегда равен отрисованной высоте.
|
|
600
|
+
Для React Hook Form и Formik это одна строка:
|
|
642
601
|
|
|
643
|
-
|
|
602
|
+
```ts
|
|
603
|
+
import { useFormGuard } from '@surdeddd/bottom-sheet/integrations/react-hook-form';
|
|
644
604
|
|
|
645
|
-
|
|
646
|
-
npm run demo
|
|
647
|
-
# http://localhost:5173
|
|
605
|
+
useFormGuard(sheetRef, form); // не двигается во время отправки, не закрывается с несохранённым вводом
|
|
648
606
|
```
|
|
649
607
|
|
|
650
|
-
|
|
651
|
-
(`bottom · top · left · right · overlay`) и живые показания движка (активная
|
|
652
|
-
snap point, progress, скорость, FPS). В углу переключатели EN/RU и светлой/тёмной темы.
|
|
608
|
+
## Серверный рендеринг
|
|
653
609
|
|
|
654
|
-
|
|
610
|
+
Каждый адаптер можно импортировать и отрендерить на сервере; движок
|
|
611
|
+
подключается на клиенте. Шторка, которая начинает закрытой, скрыта уже в
|
|
612
|
+
серверной разметке, поэтому до гидратации ничего не мелькает. Это относится
|
|
613
|
+
к Next.js, Nuxt, SvelteKit, SolidStart, Qwik City и Astro; детали, например
|
|
614
|
+
`"use client"` в Next.js App Router, описаны в гайдах по фреймворкам.
|
|
655
615
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
616
|
+
## Облегчённое ядро
|
|
617
|
+
|
|
618
|
+
Движок по умолчанию включает все необязательные возможности. Если важнее
|
|
619
|
+
размер, возьмите облегчённое ядро и добавьте к нему только нужное:
|
|
660
620
|
|
|
661
621
|
```ts
|
|
662
|
-
|
|
622
|
+
import { BottomSheetCore } from '@surdeddd/bottom-sheet/core';
|
|
623
|
+
import { routeFeature, contentSwipeFeature } from '@surdeddd/bottom-sheet/features';
|
|
624
|
+
|
|
625
|
+
const sheet = new BottomSheetCore({
|
|
626
|
+
element, handle, snapPoints,
|
|
627
|
+
closeOnBack: true,
|
|
628
|
+
features: [routeFeature(), contentSwipeFeature()],
|
|
629
|
+
});
|
|
663
630
|
```
|
|
664
631
|
|
|
665
|
-
|
|
632
|
+
| Точка входа | gzip | Что внутри |
|
|
633
|
+
| --- | --- | --- |
|
|
634
|
+
| `@surdeddd/bottom-sheet` | 25.6 KB | Полный движок |
|
|
635
|
+
| `/core` | 21.9 KB | Жесты, точки привязки, фокус, блокировка прокрутки, скрим, стек |
|
|
636
|
+
| `/features` | 4.2 KB | `routeFeature`, `visualViewportFeature`, `contentSwipeFeature`, `contentFitFeature`, `persistFeature`, `autoCollapseFeature` |
|
|
637
|
+
| `/react` `/vue` `/svelte` `/solid` `/qwik` | от 27 до 33 KB | Полный движок вместе с адаптером |
|
|
638
|
+
| `/element` | 33.3 KB | Полный движок, элемент и его стили |
|
|
639
|
+
| `/overlay` | 8.4 KB | Отдельный [оверлей](docs/overlay.md) |
|
|
640
|
+
| `/webgl` | 5.9 KB | Необязательный [рендерер на GPU](docs/webgl.md) для поверхности шторки |
|
|
641
|
+
| `/debug` | 0.9 KB | `attachDebugOverlay(engine)`: панель с состоянием движка в реальном времени |
|
|
642
|
+
|
|
643
|
+
Опция, для которой не подключена возможность, один раз предупреждает в
|
|
644
|
+
режиме разработки и игнорируется.
|
|
666
645
|
|
|
667
646
|
## Поддержка браузеров
|
|
668
647
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
648
|
+
Chrome и Edge 80, Safari 13.1 (iOS 13.4), Firefox 72. Эту границу задают
|
|
649
|
+
сборка под ES2020 и API, без которых движок не работает: Pointer Events,
|
|
650
|
+
пользовательские свойства CSS, `ResizeObserver`. Всё остальное, от `inert`
|
|
651
|
+
до `backdrop-filter` и WebGL, определяется на месте и при отсутствии мягко
|
|
652
|
+
отключается. [Таблица поддержки →](docs/browser-support.md)
|
|
674
653
|
|
|
675
|
-
|
|
654
|
+
## Документация
|
|
676
655
|
|
|
677
|
-
|
|
656
|
+
| | |
|
|
657
|
+
| --- | --- |
|
|
658
|
+
| Фреймворки | [React](docs/react.md) · [Vue](docs/vue.md) · [Svelte](docs/svelte.md) · [Solid](docs/solid.md) · [Qwik](docs/qwik.md) · [Preact](docs/preact.md) · [Веб-компонент](docs/web-component.md) · [Без фреймворка](docs/vanilla.md) · [Angular](docs/angular.md) · [Lit](docs/lit.md) · [Astro](docs/astro.md) |
|
|
659
|
+
| Темы | [Стилизация](docs/theming.md) · [Якоря и сцены скрима](docs/anchors.md) · [Содержимое на каждой точке](docs/content-fit.md) · [Оверлей](docs/overlay.md) · [RTL](docs/rtl.md) · [Рендерер WebGL](docs/webgl.md) · [Плагины](docs/plugins.md) |
|
|
660
|
+
| Рецепты | [Десять разобранных примеров](docs/recipes/README.md): панель фильтров, оформление заказа, тред комментариев, навигационная панель, мини-плеер и другие |
|
|
661
|
+
| Примеры | [Запускаемые проекты](https://github.com/Surdeddd/BottomSheet/tree/main/examples) для React, Vue, Svelte, Solid, Lit, веб-компонента и движка без фреймворка |
|
|
662
|
+
| Переход с другой библиотеки | [С vaul или react-modal-sheet](docs/MIGRATION.md) |
|
|
663
|
+
| Проект | [Архитектура](docs/ARCHITECTURE.md) · [Стабильность API](docs/api-stability.md) · [Поддержка браузеров](docs/browser-support.md) · [Участие в разработке](CONTRIBUTING.md) |
|
|
678
664
|
|
|
679
|
-
|
|
680
|
-
npm test # 500+ unit tests via vitest + happy-dom
|
|
681
|
-
npx playwright test # e2e via Playwright on mobile-Chrome / mobile-Safari / Firefox
|
|
682
|
-
npm run typecheck # TypeScript --noEmit
|
|
683
|
-
```
|
|
665
|
+
## Для ИИ-ассистентов
|
|
684
666
|
|
|
685
|
-
|
|
667
|
+
[`llms.txt`](llms.txt) — компактное и полное описание API, написанное для
|
|
668
|
+
чтения моделью: точки входа, все опции, имена методов и событий и ошибки,
|
|
669
|
+
которых стоит избегать. Файл входит в npm-пакет и лежит рядом с этим README.
|
|
686
670
|
|
|
687
671
|
## Участие в разработке
|
|
688
672
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
673
|
+
```bash
|
|
674
|
+
npm install
|
|
675
|
+
npm run demo # демо-сайт на localhost:5173
|
|
676
|
+
npm test # юнит-тесты
|
|
677
|
+
npm run e2e # тесты в браузере
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
Подробности в [CONTRIBUTING.md](CONTRIBUTING.md). Ошибки и идеи — в
|
|
681
|
+
[issues](https://github.com/Surdeddd/BottomSheet/issues).
|
|
694
682
|
|
|
695
683
|
## Лицензия
|
|
696
684
|
|
|
697
|
-
MIT
|
|
685
|
+
MIT, см. [LICENSE](LICENSE).
|