@ryuzaki13/react-foundation-lib 1.4.0 → 1.4.2
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 +172 -114
- package/dist/chunks/{formatters-CBgrBeSA.js → formatters-D33Uh3si.js} +7 -8
- package/dist/chunks/{formatters-CBgrBeSA.js.map → formatters-D33Uh3si.js.map} +1 -1
- package/dist/chunks/odata-service-CWkHASdw.js +2211 -0
- package/dist/chunks/odata-service-CWkHASdw.js.map +1 -0
- package/dist/chunks/{validate-BW-7BZy_.js → validate-DyzojM0w.js} +2 -2
- package/dist/chunks/{validate-BW-7BZy_.js.map → validate-DyzojM0w.js.map} +1 -1
- package/dist/error-report/index.js +2 -2
- package/dist/excel/index.js +20 -21
- package/dist/excel/index.js.map +1 -1
- package/dist/formatters/index.js +4 -5
- package/dist/{odata → odata-service}/buildSeparatedArrays.d.ts +1 -1
- package/dist/odata-service/buildSeparatedArrays.d.ts.map +1 -0
- package/dist/{odata/types.d.ts → odata-service/collectionTypes.d.ts} +1 -1
- package/dist/odata-service/collectionTypes.d.ts.map +1 -0
- package/dist/{odata → odata-service}/dateUtils.d.ts +1 -1
- package/dist/odata-service/dateUtils.d.ts.map +1 -0
- package/dist/{odata → odata-service}/findCollectionPairs.d.ts +1 -1
- package/dist/odata-service/findCollectionPairs.d.ts.map +1 -0
- package/dist/odata-service/index.d.ts +5 -0
- package/dist/odata-service/index.d.ts.map +1 -1
- package/dist/odata-service/index.js +2 -2
- package/dist/{odata/store.d.ts → odata-service/odataCollectionStore.d.ts} +1 -1
- package/dist/odata-service/odataCollectionStore.d.ts.map +1 -0
- package/dist/query-client/index.js.map +1 -1
- package/dist/query-client/queryClient.d.ts +1 -3
- package/dist/query-client/queryClient.d.ts.map +1 -1
- package/package.json +1 -5
- package/src/array/README.mdx +1630 -0
- package/src/binary/README.mdx +1405 -0
- package/src/bounded-copy-stack/README.mdx +257 -0
- package/src/context-menu/README.mdx +326 -0
- package/src/copy/README.mdx +992 -0
- package/src/crypto/README.mdx +1052 -0
- package/src/currency/README.mdx +155 -0
- package/src/date-segments/README.mdx +319 -0
- package/src/dom/README.mdx +2179 -0
- package/src/error/README.mdx +95 -0
- package/src/error-report/README.mdx +612 -0
- package/src/excel/README.mdx +449 -0
- package/src/file/README.mdx +1957 -0
- package/src/fingerprint/README.mdx +100 -0
- package/src/form/README.mdx +101 -0
- package/src/formatters/README.mdx +469 -0
- package/src/formatters/boolean/README.mdx +494 -0
- package/src/formatters/date/README.mdx +732 -0
- package/src/formatters/number/README.mdx +542 -0
- package/src/formatters/pipeline/README.mdx +629 -0
- package/src/formatters/rowBased/README.mdx +486 -0
- package/src/formatters/strings/README.mdx +442 -0
- package/src/formatters/valueState/README.mdx +459 -0
- package/src/formulas/README.mdx +1283 -0
- package/src/foundationLib.mdx +70 -54
- package/src/hooks/README.mdx +988 -0
- package/src/http/README.mdx +126 -0
- package/src/media/README.mdx +1853 -0
- package/src/notifications/README.mdx +323 -0
- package/src/number-scale/README.mdx +318 -0
- package/src/odata-service/README.mdx +2916 -0
- package/src/presets/README.mdx +136 -0
- package/src/pwa/README.mdx +394 -0
- package/src/query-client/README.mdx +2500 -0
- package/src/range-output/README.mdx +161 -0
- package/src/seo/README.mdx +279 -0
- package/src/session-storage/README.mdx +125 -0
- package/src/string-comparison/README.mdx +96 -0
- package/src/table/README.mdx +3106 -0
- package/src/three-scene/README.mdx +248 -0
- package/src/types/README.mdx +172 -0
- package/src/utils/README.mdx +1724 -0
- package/src/validators/README.mdx +239 -0
- package/src/virtualizer/README.mdx +138 -0
- package/src/xml/README.mdx +149 -0
- package/dist/chunks/dateUtils-B5V5aXB-.js +0 -847
- package/dist/chunks/dateUtils-B5V5aXB-.js.map +0 -1
- package/dist/chunks/odata-service-B6TxBSLm.js +0 -1277
- package/dist/chunks/odata-service-B6TxBSLm.js.map +0 -1
- package/dist/odata/buildSeparatedArrays.d.ts.map +0 -1
- package/dist/odata/dateUtils.d.ts.map +0 -1
- package/dist/odata/findCollectionPairs.d.ts.map +0 -1
- package/dist/odata/index.d.ts +0 -6
- package/dist/odata/index.d.ts.map +0 -1
- package/dist/odata/index.js +0 -102
- package/dist/odata/index.js.map +0 -1
- package/dist/odata/store.d.ts.map +0 -1
- package/dist/odata/types.d.ts.map +0 -1
- package/src/binary/README.md +0 -110
- package/src/docs/browserAndReact.mdx +0 -150
- package/src/docs/filesAndExcel.mdx +0 -163
- package/src/docs/formatters-runtime.mdx +0 -455
- package/src/docs/formatters.mdx +0 -380
- package/src/docs/odataAndTables.mdx +0 -178
- package/src/docs/pureUtilities.mdx +0 -96
- package/src/docs/runtimeAndState.mdx +0 -155
- package/src/file/README.md +0 -289
- package/src/formatters/date/README.md +0 -520
- package/src/formatters/number/README.md +0 -123
- package/src/formatters/valueState/README.md +0 -275
- package/src/formulas/README.md +0 -42
- package/src/pwa/README.md +0 -638
- package/src/query-client/README.md +0 -565
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation Lib/Data/Bounded Copy Stack" />
|
|
4
|
+
|
|
5
|
+
# Ограниченный стек копий через `@ryuzaki13/react-foundation-lib/bounded-copy-stack`
|
|
6
|
+
|
|
7
|
+
Модуль хранит небольшой список последних скопированных сущностей. Новый элемент помещается в начало, повтор по `fingerprint` обновляет существующую запись, а старые записи удаляются при превышении лимита.
|
|
8
|
+
|
|
9
|
+
Документ рассчитан на разработчика без доступа к исходникам пакета. Ниже объясняются структура элемента, различие `id` и `fingerprint`, порядок записей, ссылки на payload и все граничные случаи.
|
|
10
|
+
|
|
11
|
+
## Содержание
|
|
12
|
+
|
|
13
|
+
- [Назначение и границы](#назначение-и-границы)
|
|
14
|
+
- [Установка и импорт](#установка-и-импорт)
|
|
15
|
+
- [Основные понятия](#основные-понятия)
|
|
16
|
+
- [Быстрый старт](#быстрый-старт)
|
|
17
|
+
- [`pushBoundedCopyStackItem`](#pushboundedcopystackitem)
|
|
18
|
+
- [`getBoundedCopyStackCandidates`](#getboundedcopystackcandidates)
|
|
19
|
+
- [Ссылки, хранение и сериализация](#ссылки-хранение-и-сериализация)
|
|
20
|
+
- [Полный API](#полный-api)
|
|
21
|
+
- [Типичные ошибки](#типичные-ошибки)
|
|
22
|
+
|
|
23
|
+
## Назначение и границы
|
|
24
|
+
|
|
25
|
+
Типичный сценарий — показать пользователю несколько последних вариантов для повторной вставки:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
копирование A ──► [A]
|
|
29
|
+
копирование B ──► [B, A]
|
|
30
|
+
повтор A ──► [новая A, B]
|
|
31
|
+
копирование C ──► [C, новая A, B]
|
|
32
|
+
копирование D ──► [D, C, новая A] при maxSize = 3
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Модуль:
|
|
36
|
+
|
|
37
|
+
- не использует системный clipboard;
|
|
38
|
+
- не сохраняет список в storage;
|
|
39
|
+
- не создаёт React state или Zustand store;
|
|
40
|
+
- не клонирует `payload` и `meta`;
|
|
41
|
+
- не сравнивает payload глубоко;
|
|
42
|
+
- не нормализует данные;
|
|
43
|
+
- не выполняет вставку выбранного кандидата.
|
|
44
|
+
|
|
45
|
+
Clipboard helpers находятся в `@ryuzaki13/react-foundation-lib/copy`. Владелец state/storage находится в host-приложении.
|
|
46
|
+
|
|
47
|
+
## Установка и импорт
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install @ryuzaki13/react-foundation-lib
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import {
|
|
55
|
+
DEFAULT_BOUNDED_COPY_STACK_SIZE,
|
|
56
|
+
getBoundedCopyStackCandidates,
|
|
57
|
+
pushBoundedCopyStackItem
|
|
58
|
+
} from "@ryuzaki13/react-foundation-lib/bounded-copy-stack";
|
|
59
|
+
|
|
60
|
+
import type {
|
|
61
|
+
BoundedCopyStackItem,
|
|
62
|
+
BoundedCopyStackOptions,
|
|
63
|
+
BoundedCopyStackPushInput,
|
|
64
|
+
BoundedCopyStackPushResult
|
|
65
|
+
} from "@ryuzaki13/react-foundation-lib/bounded-copy-stack";
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Корневой и глубокий импорты не поддерживаются. Модуль ESM, не зависит от React и browser APIs.
|
|
69
|
+
|
|
70
|
+
## Основные понятия
|
|
71
|
+
|
|
72
|
+
### Элемент стека
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
type BoundedCopyStackItem<TPayload, TMeta = undefined> = {
|
|
76
|
+
id: string;
|
|
77
|
+
fingerprint: string;
|
|
78
|
+
payload: TPayload;
|
|
79
|
+
meta: TMeta;
|
|
80
|
+
copiedAt: number;
|
|
81
|
+
};
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Поле | Назначение |
|
|
85
|
+
| ------------- | -------------------------------------------------------------------------- |
|
|
86
|
+
| `id` | Идентификатор конкретной записи/операции копирования |
|
|
87
|
+
| `fingerprint` | Идентичность содержимого для удаления повторов |
|
|
88
|
+
| `payload` | Данные, которые host затем сможет вставить |
|
|
89
|
+
| `meta` | Подпись, тип, источник или другая информация для UI |
|
|
90
|
+
| `copiedAt` | Время копирования в миллисекундах Unix; функция не сортирует по этому полю |
|
|
91
|
+
|
|
92
|
+
`id` и `fingerprint` решают разные задачи. При повторном копировании содержимого `fingerprint` остаётся прежним, а `id`, `payload`, `meta` и `copiedAt` могут стать новыми.
|
|
93
|
+
|
|
94
|
+
### Порядок
|
|
95
|
+
|
|
96
|
+
Индекс `0` всегда содержит элемент последней операции `push`. Функция доверяет входному порядку и не сортирует список по `copiedAt`.
|
|
97
|
+
|
|
98
|
+
## Быстрый старт
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
type ColumnCopy = {
|
|
102
|
+
columnId: string;
|
|
103
|
+
width: number;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
type ColumnCopyMeta = {
|
|
107
|
+
label: string;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
let stack: BoundedCopyStackItem<ColumnCopy, ColumnCopyMeta>[] = [];
|
|
111
|
+
|
|
112
|
+
const result = pushBoundedCopyStackItem(stack, {
|
|
113
|
+
id: crypto.randomUUID(),
|
|
114
|
+
fingerprint: "column:amount:width:120",
|
|
115
|
+
payload: { columnId: "amount", width: 120 },
|
|
116
|
+
meta: { label: "Сумма — ширина 120" }
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
stack = result.items;
|
|
120
|
+
console.log(result.updatedExisting); // false.
|
|
121
|
+
console.log(result.addedItem.copiedAt); // Date.now() на момент вызова.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## `pushBoundedCopyStackItem`
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const result = pushBoundedCopyStackItem(items, input, { maxSize: 3 });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Алгоритм:
|
|
131
|
+
|
|
132
|
+
1. Создаёт новый элемент из `input`.
|
|
133
|
+
2. Если `copiedAt` отсутствует, подставляет `Date.now()`.
|
|
134
|
+
3. Ищет любой элемент с точно равным `fingerprint`.
|
|
135
|
+
4. Удаляет из старого списка все элементы с таким fingerprint.
|
|
136
|
+
5. Помещает новый элемент в начало.
|
|
137
|
+
6. Обрезает результат до `maxSize`.
|
|
138
|
+
|
|
139
|
+
Входной массив не изменяется. Возвращается новый массив.
|
|
140
|
+
|
|
141
|
+
### Повторный fingerprint
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const oldItems = [
|
|
145
|
+
{
|
|
146
|
+
id: "old",
|
|
147
|
+
fingerprint: "same-content",
|
|
148
|
+
payload: { value: 1 },
|
|
149
|
+
meta: undefined,
|
|
150
|
+
copiedAt: 1
|
|
151
|
+
}
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
const result = pushBoundedCopyStackItem(oldItems, {
|
|
155
|
+
id: "new",
|
|
156
|
+
fingerprint: "same-content",
|
|
157
|
+
payload: { value: 2 },
|
|
158
|
+
meta: undefined,
|
|
159
|
+
copiedAt: 2
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
console.log(result.items.length); // 1.
|
|
163
|
+
console.log(result.items[0].id); // "new".
|
|
164
|
+
console.log(result.updatedExisting); // true.
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Сравнение fingerprint регистрозависимое и использует `===`. Строки `"A"` и `"a"` различны.
|
|
168
|
+
|
|
169
|
+
### Нормализация `maxSize`
|
|
170
|
+
|
|
171
|
+
Default `DEFAULT_BOUNDED_COPY_STACK_SIZE` равен `3`.
|
|
172
|
+
|
|
173
|
+
| Переданное значение | Фактический лимит |
|
|
174
|
+
| ------------------------------------ | ----------------: |
|
|
175
|
+
| `undefined`, `NaN`, `Infinity` | 3 |
|
|
176
|
+
| `5.9` | 5 |
|
|
177
|
+
| `1` | 1 |
|
|
178
|
+
| `0`, отрицательное конечное значение | 1 |
|
|
179
|
+
|
|
180
|
+
Полностью отключить хранение через `maxSize: 0` нельзя. Если список не нужен, host должен не вызывать push или хранить пустой state самостоятельно.
|
|
181
|
+
|
|
182
|
+
### Результат
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
type BoundedCopyStackPushResult<TPayload, TMeta> = {
|
|
186
|
+
items: BoundedCopyStackItem<TPayload, TMeta>[];
|
|
187
|
+
addedItem: BoundedCopyStackItem<TPayload, TMeta>;
|
|
188
|
+
updatedExisting: boolean;
|
|
189
|
+
};
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- `items` — итоговый ограниченный список;
|
|
193
|
+
- `addedItem` — созданная запись, даже если из-за будущей логики host её не сохранит;
|
|
194
|
+
- `updatedExisting` — был ли во входном списке такой fingerprint.
|
|
195
|
+
|
|
196
|
+
## `getBoundedCopyStackCandidates`
|
|
197
|
+
|
|
198
|
+
Функция фильтрует записи предикатом и сохраняет исходный порядок:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const tableCandidates = getBoundedCopyStackCandidates(stack, (item) => item.meta.kind === "table-column");
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Она эквивалентна типизированному `items.filter(predicate)`:
|
|
205
|
+
|
|
206
|
+
- возвращает новый массив;
|
|
207
|
+
- не клонирует элементы;
|
|
208
|
+
- не изменяет исходный массив;
|
|
209
|
+
- не ограничивает количество дополнительно;
|
|
210
|
+
- исключение из predicate пробрасывается вызывающему коду.
|
|
211
|
+
|
|
212
|
+
## Ссылки, хранение и сериализация
|
|
213
|
+
|
|
214
|
+
`payload` и `meta` переносятся по ссылке:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const payload = { nested: { value: 1 } };
|
|
218
|
+
const result = pushBoundedCopyStackItem([], {
|
|
219
|
+
id: "1",
|
|
220
|
+
fingerprint: "one",
|
|
221
|
+
payload,
|
|
222
|
+
meta: undefined
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
console.log(result.addedItem.payload === payload); // true.
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Если нужна независимая data-only копия, создайте её через `deepCopyWithoutFunctions` из `/utils` до push. Clone не должен нормализовать данные.
|
|
229
|
+
|
|
230
|
+
Для `localStorage`/`sessionStorage` host самостоятельно отвечает за:
|
|
231
|
+
|
|
232
|
+
- JSON-compatible payload/meta;
|
|
233
|
+
- версию persisted schema;
|
|
234
|
+
- runtime-проверку восстановленных данных;
|
|
235
|
+
- удаление устаревших записей;
|
|
236
|
+
- отсутствие секретов и персональных данных.
|
|
237
|
+
|
|
238
|
+
## Полный API
|
|
239
|
+
|
|
240
|
+
| Export | Назначение |
|
|
241
|
+
| --------------------------------- | ------------------------------------------------ |
|
|
242
|
+
| `DEFAULT_BOUNDED_COPY_STACK_SIZE` | Default-лимит `3` |
|
|
243
|
+
| `pushBoundedCopyStackItem` | Добавляет/обновляет запись и ограничивает список |
|
|
244
|
+
| `getBoundedCopyStackCandidates` | Фильтрует кандидатов |
|
|
245
|
+
| `BoundedCopyStackItem` | Полная сохранённая запись |
|
|
246
|
+
| `BoundedCopyStackPushInput` | Вход push; `copiedAt` optional |
|
|
247
|
+
| `BoundedCopyStackOptions` | `{ maxSize?: number }` |
|
|
248
|
+
| `BoundedCopyStackPushResult` | Итоговый список и сведения об операции |
|
|
249
|
+
|
|
250
|
+
## Типичные ошибки
|
|
251
|
+
|
|
252
|
+
- Использовать уникальный `id` как fingerprint: дубликаты содержимого никогда не объединятся.
|
|
253
|
+
- Ожидать сортировку по `copiedAt`: порядок определяется вызовами push.
|
|
254
|
+
- Мутировать `payload` после push и ожидать snapshot: ссылка общая.
|
|
255
|
+
- Передавать `maxSize: 0` для отключения: получится лимит `1`.
|
|
256
|
+
- Хранить callback/DOM node в persisted stack: модуль не делает данные сериализуемыми.
|
|
257
|
+
- Вызывать helper вместо clipboard API: стек ничего не копирует в ОС.
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation Lib/Interaction/Context Menu State" />
|
|
4
|
+
|
|
5
|
+
# Состояние и anchors контекстного меню через `@ryuzaki13/react-foundation-lib/context-menu`
|
|
6
|
+
|
|
7
|
+
Модуль описывает открытое/закрытое состояние контекстного меню, источник открытия и два вида anchor: DOM element или экранная точка. Также он создаёт virtual anchor для Floating UI.
|
|
8
|
+
|
|
9
|
+
Модуль не содержит React hook и не рисует меню.
|
|
10
|
+
|
|
11
|
+
## Содержание
|
|
12
|
+
|
|
13
|
+
- [Назначение и границы](#назначение-и-границы)
|
|
14
|
+
- [Установка и импорт](#установка-и-импорт)
|
|
15
|
+
- [Модель данных](#модель-данных)
|
|
16
|
+
- [Получение точки](#получение-точки)
|
|
17
|
+
- [`createVirtualAnchor`](#createvirtualanchor)
|
|
18
|
+
- [State helpers](#state-helpers)
|
|
19
|
+
- [React reducer recipe](#react-reducer-recipe)
|
|
20
|
+
- [Floating UI recipe](#floating-ui-recipe)
|
|
21
|
+
- [Доступность](#доступность)
|
|
22
|
+
- [Полный API и ошибки](#полный-api-и-ошибки)
|
|
23
|
+
|
|
24
|
+
## Назначение и границы
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
click/contextmenu/keyboard
|
|
28
|
+
│
|
|
29
|
+
├─ element anchor
|
|
30
|
+
└─ point {x,y}
|
|
31
|
+
│
|
|
32
|
+
▼
|
|
33
|
+
MenuState
|
|
34
|
+
│
|
|
35
|
+
▼
|
|
36
|
+
host React/Floating UI/menu component
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Модуль:
|
|
40
|
+
|
|
41
|
+
- не добавляет event listeners;
|
|
42
|
+
- не вызывает `preventDefault`;
|
|
43
|
+
- не хранит global state;
|
|
44
|
+
- не позиционирует floating element;
|
|
45
|
+
- не закрывает меню по Escape/outside click;
|
|
46
|
+
- не управляет focus/ARIA;
|
|
47
|
+
- не клонирует DOM elements.
|
|
48
|
+
|
|
49
|
+
Готовый visual primitive находится в `@ryuzaki13/react-foundation-ui/context-menu`.
|
|
50
|
+
|
|
51
|
+
## Установка и импорт
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import {
|
|
55
|
+
closeMenu,
|
|
56
|
+
createVirtualAnchor,
|
|
57
|
+
getMenuPointFromEvent,
|
|
58
|
+
getMenuPointFromRect,
|
|
59
|
+
initialMenuState,
|
|
60
|
+
openMenu,
|
|
61
|
+
toggleMenu
|
|
62
|
+
} from "@ryuzaki13/react-foundation-lib/context-menu";
|
|
63
|
+
|
|
64
|
+
import type {
|
|
65
|
+
ElementMenuAnchor,
|
|
66
|
+
MenuAnchor,
|
|
67
|
+
MenuOpenSource,
|
|
68
|
+
MenuPoint,
|
|
69
|
+
MenuState,
|
|
70
|
+
OpenMenuPayload,
|
|
71
|
+
PointMenuAnchor
|
|
72
|
+
} from "@ryuzaki13/react-foundation-lib/context-menu";
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Functions pure, но types/anchors используют DOM `Element`/`HTMLElement`. Публичный subpath не импортирует Floating UI runtime.
|
|
76
|
+
|
|
77
|
+
## Модель данных
|
|
78
|
+
|
|
79
|
+
### Point
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
interface MenuPoint {
|
|
83
|
+
x: number;
|
|
84
|
+
y: number;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Координаты соответствуют viewport/client coordinates, а не page scroll coordinates.
|
|
89
|
+
|
|
90
|
+
### Источник открытия
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
type MenuOpenSource = "click" | "contextmenu" | "keyboard" | "programmatic";
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Source помогает UI выбирать focus/animation/recovery, но helpers сами не меняют поведение по source.
|
|
97
|
+
|
|
98
|
+
### Anchors
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
type ElementMenuAnchor = {
|
|
102
|
+
type: "element";
|
|
103
|
+
element: HTMLElement;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
type PointMenuAnchor = {
|
|
107
|
+
type: "point";
|
|
108
|
+
point: MenuPoint;
|
|
109
|
+
contextElement?: Element | null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
type MenuAnchor = ElementMenuAnchor | PointMenuAnchor;
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`contextElement` помогает Floating UI определить document/layout context для virtual element.
|
|
116
|
+
|
|
117
|
+
### State
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
interface MenuState {
|
|
121
|
+
open: boolean;
|
|
122
|
+
source: MenuOpenSource | null;
|
|
123
|
+
anchor: MenuAnchor | null;
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Нормальное закрытое состояние:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
{
|
|
131
|
+
open: false,
|
|
132
|
+
source: null,
|
|
133
|
+
anchor: null
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Получение точки
|
|
138
|
+
|
|
139
|
+
### `getMenuPointFromEvent`
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
function onContextMenu(event: React.MouseEvent<HTMLElement>) {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
|
|
145
|
+
const point = getMenuPointFromEvent(event);
|
|
146
|
+
// { x: event.clientX, y: event.clientY }.
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Функции нужен только structural object с `clientX`/`clientY`, поэтому подходят native и React mouse events.
|
|
151
|
+
|
|
152
|
+
Она не вызывает `preventDefault` и не проверяет finite coordinates.
|
|
153
|
+
|
|
154
|
+
### `getMenuPointFromRect`
|
|
155
|
+
|
|
156
|
+
Возвращает геометрический центр rect:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const point = getMenuPointFromRect({
|
|
160
|
+
left: 10,
|
|
161
|
+
top: 20,
|
|
162
|
+
width: 80,
|
|
163
|
+
height: 40
|
|
164
|
+
});
|
|
165
|
+
// { x: 50, y: 40 }.
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Подходит для keyboard opening от центра `element.getBoundingClientRect()`:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const point = getMenuPointFromRect(button.getBoundingClientRect());
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## `createVirtualAnchor`
|
|
175
|
+
|
|
176
|
+
Создаёт object с `getBoundingClientRect`, представляющий точку нулевого размера:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const virtualAnchor = createVirtualAnchor({ x: 120, y: 240 }, triggerElement);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Возвращаемый rect:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
{
|
|
186
|
+
x: 120,
|
|
187
|
+
y: 240,
|
|
188
|
+
top: 240,
|
|
189
|
+
left: 120,
|
|
190
|
+
bottom: 240,
|
|
191
|
+
right: 120,
|
|
192
|
+
width: 0,
|
|
193
|
+
height: 0,
|
|
194
|
+
toJSON: () => undefined
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Если `contextElement` равен `null`/`undefined`, в результате поле становится `undefined`.
|
|
199
|
+
|
|
200
|
+
Point object читается при каждом `getBoundingClientRect`. Не мутируйте его; создайте новый anchor при изменении координат.
|
|
201
|
+
|
|
202
|
+
## State helpers
|
|
203
|
+
|
|
204
|
+
### `initialMenuState`
|
|
205
|
+
|
|
206
|
+
Экспортируется один module-level object. Рассматривайте его как readonly-константу и не мутируйте:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const [state, setState] = useState<MenuState>(initialMenuState);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### `openMenu`
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const state = openMenu({
|
|
216
|
+
source: "contextmenu",
|
|
217
|
+
anchor: {
|
|
218
|
+
type: "point",
|
|
219
|
+
point: { x: 100, y: 200 }
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Всегда возвращает новый state `{ open: true, source, anchor }`. Anchor/element переносятся по ссылке.
|
|
225
|
+
|
|
226
|
+
### `closeMenu`
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const closed = closeMenu(opened);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Открытое состояние превращается в canonical closed state. Если `state.open === false` и `state.anchor === null`, возвращается исходная ссылка `state`, что удобно для reducer no-op.
|
|
233
|
+
|
|
234
|
+
Передавайте согласованное состояние. Если вручную создать `{ open:false, source:"click", anchor:null }`, `closeMenu` вернёт его без очистки source.
|
|
235
|
+
|
|
236
|
+
### `toggleMenu`
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
const next = toggleMenu(current, payload);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
- если `current.open === true`, закрывает меню и игнорирует payload;
|
|
243
|
+
- если меню закрыто, вызывает `openMenu(payload)`.
|
|
244
|
+
|
|
245
|
+
Toggle подходит для click button. Для `contextmenu` чаще нужен явный `openMenu`, чтобы повторный right-click перемещал anchor, а не закрывал меню.
|
|
246
|
+
|
|
247
|
+
## React reducer recipe
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
const [menu, setMenu] = useState<MenuState>(initialMenuState);
|
|
251
|
+
|
|
252
|
+
function handleContextMenu(event: React.MouseEvent<HTMLElement>) {
|
|
253
|
+
event.preventDefault();
|
|
254
|
+
|
|
255
|
+
setMenu(
|
|
256
|
+
openMenu({
|
|
257
|
+
source: "contextmenu",
|
|
258
|
+
anchor: {
|
|
259
|
+
type: "point",
|
|
260
|
+
point: getMenuPointFromEvent(event),
|
|
261
|
+
contextElement: event.currentTarget
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function handleClose() {
|
|
268
|
+
setMenu((current) => closeMenu(current));
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Не сохраняйте `MenuState` в JSON/localStorage: он может содержать `HTMLElement`.
|
|
273
|
+
|
|
274
|
+
## Floating UI recipe
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
const reference =
|
|
278
|
+
menu.anchor?.type === "element"
|
|
279
|
+
? menu.anchor.element
|
|
280
|
+
: menu.anchor?.type === "point"
|
|
281
|
+
? createVirtualAnchor(menu.anchor.point, menu.anchor.contextElement)
|
|
282
|
+
: null;
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Передайте `reference` в public API выбранного Floating UI wrapper. Не импортируйте private package files.
|
|
286
|
+
|
|
287
|
+
## Доступность
|
|
288
|
+
|
|
289
|
+
Host обязан реализовать:
|
|
290
|
+
|
|
291
|
+
- открытие клавишей Context Menu/Shift+F10 или принятой командой;
|
|
292
|
+
- focus первого/выбранного item;
|
|
293
|
+
- Arrow/Home/End navigation;
|
|
294
|
+
- закрытие по Escape и возврат focus;
|
|
295
|
+
- `role="menu"`/`menuitem` только если semantics действительно menu;
|
|
296
|
+
- disabled items;
|
|
297
|
+
- outside click и focus dismissal;
|
|
298
|
+
- сохранение trigger element для keyboard recovery.
|
|
299
|
+
|
|
300
|
+
Для keyboard source element anchor обычно лучше point без pointer coordinates.
|
|
301
|
+
|
|
302
|
+
## Полный API и ошибки
|
|
303
|
+
|
|
304
|
+
| Export | Назначение |
|
|
305
|
+
| ----------------------- | --------------------------- |
|
|
306
|
+
| `getMenuPointFromEvent` | Читает client coordinates |
|
|
307
|
+
| `getMenuPointFromRect` | Центр rect |
|
|
308
|
+
| `createVirtualAnchor` | Point-like DOMRect provider |
|
|
309
|
+
| `initialMenuState` | Canonical closed state |
|
|
310
|
+
| `openMenu` | Новый opened state |
|
|
311
|
+
| `closeMenu` | Закрывает/очищает state |
|
|
312
|
+
| `toggleMenu` | Toggle по `open` |
|
|
313
|
+
| `MenuPoint` | `{ x, y }` |
|
|
314
|
+
| `MenuOpenSource` | Источник открытия |
|
|
315
|
+
| `MenuAnchor` | Element/point union |
|
|
316
|
+
| `MenuState` | Полное state |
|
|
317
|
+
| `OpenMenuPayload` | `{ source, anchor }` |
|
|
318
|
+
|
|
319
|
+
Типичные ошибки:
|
|
320
|
+
|
|
321
|
+
- Использовать `pageX/pageY` вместо client coordinates.
|
|
322
|
+
- Вызывать toggle при каждом `contextmenu` и неожиданно закрывать открытое меню.
|
|
323
|
+
- Мутировать `initialMenuState`.
|
|
324
|
+
- Persist DOM element.
|
|
325
|
+
- Считать virtual anchor готовым positioner-ом.
|
|
326
|
+
- Не возвращать focus после keyboard menu.
|