@weasel-js/ui-logic 0.0.0-stage → 1.15.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/LICENSE +21 -0
- package/README.md +108 -2
- package/dist/index.d.ts +562 -0
- package/dist/index.js +502 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -4
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { Rect } from '@weasel-js/geom';
|
|
2
|
+
export { COARSE_STEPS } from '@weasel-js/quantity';
|
|
3
|
+
|
|
4
|
+
/** How a list that folds is read: which items are branches, which are open, and which item is whose child. */
|
|
5
|
+
interface TreeShape<T> {
|
|
6
|
+
/** Whether `item` can open, its children shown or not. */
|
|
7
|
+
isBranch(item: T): boolean;
|
|
8
|
+
isOpen(item: T): boolean;
|
|
9
|
+
/** Whether `child` is one of `parent`'s own children. */
|
|
10
|
+
isChildOf(child: T, parent: T): boolean;
|
|
11
|
+
/** Whether a branch can open or close now. A disabled one can't. Default: every branch. */
|
|
12
|
+
canToggle?(item: T): boolean;
|
|
13
|
+
/** The inline direction the rows read in. Under `'rtl'` Left opens and Right closes. Default `'ltr'`. */
|
|
14
|
+
dir?: 'ltr' | 'rtl';
|
|
15
|
+
}
|
|
16
|
+
/** What Right or Left does on one item of a tree: open or close it, move the highlight, or nothing (`handled` false).
|
|
17
|
+
* `at` null is a key the tree takes without moving. */
|
|
18
|
+
type TreeKeyResult = {
|
|
19
|
+
handled: false;
|
|
20
|
+
} | {
|
|
21
|
+
handled: true;
|
|
22
|
+
toggle: number;
|
|
23
|
+
} | {
|
|
24
|
+
handled: true;
|
|
25
|
+
at: number | null;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Right and Left on the item at `at`, the WAI-ARIA tree way: Right opens a closed branch, else steps to its first
|
|
29
|
+
* child; Left closes an open branch, else steps to the parent. Mirrored under `dir: 'rtl'`. `items` are the rows
|
|
30
|
+
* shown, in drawn order.
|
|
31
|
+
*/
|
|
32
|
+
declare function treeKey<T>(items: readonly T[], at: number, key: string, shape: TreeShape<T>): TreeKeyResult;
|
|
33
|
+
|
|
34
|
+
/** How a list's items are read: which ones the highlight can land on, and which way its arrows run. */
|
|
35
|
+
interface ListShape<T> {
|
|
36
|
+
/** Whether the highlight can land on `item`. A separator or a heading can't. Default: every item. */
|
|
37
|
+
canStop?(item: T): boolean;
|
|
38
|
+
/** Which arrow keys move the highlight. `'both'` answers to all four, the way a radio group does. Default `'vertical'`. */
|
|
39
|
+
orientation?: 'vertical' | 'horizontal' | 'both';
|
|
40
|
+
/** The inline direction the items are laid out in. Under `'rtl'` Left steps forward and Right back. Default `'ltr'`. */
|
|
41
|
+
dir?: 'ltr' | 'rtl';
|
|
42
|
+
/** An arrow past either end comes round to the other. Default true. */
|
|
43
|
+
wrap?: boolean;
|
|
44
|
+
}
|
|
45
|
+
/** Index of the first item the highlight can land on, or null for a list with none. */
|
|
46
|
+
declare function firstStop<T>(items: readonly T[], shape?: ListShape<T>): number | null;
|
|
47
|
+
/** The next item the highlight can land on from `from` in direction `dir`; from `null`, the first or last.
|
|
48
|
+
* Without wrap, an end stays where it is. Null for a list with no such item. */
|
|
49
|
+
declare function stepStop<T>(items: readonly T[], from: number | null, dir: 1 | -1, shape?: ListShape<T>): number | null;
|
|
50
|
+
/** What one key does to a list's highlight. `handled` false for a key that is not the list's to move. */
|
|
51
|
+
type RovingResult = {
|
|
52
|
+
handled: false;
|
|
53
|
+
} | {
|
|
54
|
+
handled: true;
|
|
55
|
+
at: number | null;
|
|
56
|
+
};
|
|
57
|
+
/** Move the highlight `at` by one key: the arrows along the list's orientation, Home and End. */
|
|
58
|
+
declare function rovingKey<T>(items: readonly T[], at: number | null, key: string, shape?: ListShape<T>): RovingResult;
|
|
59
|
+
/** A key as {@link rowKey} reads it: the key and the modifiers held. */
|
|
60
|
+
interface KeyPress {
|
|
61
|
+
key: string;
|
|
62
|
+
shiftKey?: boolean;
|
|
63
|
+
ctrlKey?: boolean;
|
|
64
|
+
metaKey?: boolean;
|
|
65
|
+
altKey?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** A list of rows, and which of the keys past the arrows its caller answers. */
|
|
68
|
+
interface RowKeysShape<T> extends ListShape<T> {
|
|
69
|
+
/** Set for a list that folds: Right and Left open, close, and walk it ({@link treeKey}). */
|
|
70
|
+
tree?: TreeShape<T>;
|
|
71
|
+
/** Cmd/Ctrl+A selects every row. */
|
|
72
|
+
selectAll?: boolean;
|
|
73
|
+
/** Enter and Space activate a row. */
|
|
74
|
+
activate?: boolean;
|
|
75
|
+
/** Typing a character searches the rows' text. */
|
|
76
|
+
typeahead?: boolean;
|
|
77
|
+
}
|
|
78
|
+
/** What one key on a row asks for, for the caller to carry out. */
|
|
79
|
+
type RowKeyAction =
|
|
80
|
+
/** Select every row. */
|
|
81
|
+
{
|
|
82
|
+
kind: 'selectAll';
|
|
83
|
+
}
|
|
84
|
+
/** Move to row `at`, extending a range from the anchor when `extend`. `at` null takes the key without moving. */
|
|
85
|
+
| {
|
|
86
|
+
kind: 'move';
|
|
87
|
+
at: number | null;
|
|
88
|
+
extend: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** Open or close the branch at `at`. */
|
|
91
|
+
| {
|
|
92
|
+
kind: 'toggle';
|
|
93
|
+
at: number;
|
|
94
|
+
} | {
|
|
95
|
+
kind: 'activate';
|
|
96
|
+
}
|
|
97
|
+
/** Hand the key to typeahead. `held` when the key is the list's whether or not the search finds a row. */
|
|
98
|
+
| {
|
|
99
|
+
kind: 'type';
|
|
100
|
+
held: boolean;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* What a key on row `at` of a list of rows does, in the order the keys are tried: Cmd/Ctrl+A, the arrows with Home
|
|
104
|
+
* and End (Shift to extend), a tree's Right and Left, a space typed mid-search, Enter or Space to activate, then any
|
|
105
|
+
* other character to search. `searching` says a typeahead search is under way. Null for a key that is not the list's.
|
|
106
|
+
*/
|
|
107
|
+
declare function rowKey<T>(items: readonly T[], at: number, press: KeyPress, shape?: RowKeysShape<T>, searching?: boolean): RowKeyAction | null;
|
|
108
|
+
|
|
109
|
+
/** How typeahead reads a list: its shape, and the text each item is found by. */
|
|
110
|
+
interface TypeaheadShape<T> extends Pick<ListShape<T>, 'canStop'> {
|
|
111
|
+
label(item: T): string;
|
|
112
|
+
}
|
|
113
|
+
/** How long typing can pause before the typeahead buffer starts over, in milliseconds. */
|
|
114
|
+
declare const TYPEAHEAD_RESET_MS = 500;
|
|
115
|
+
/** What has been typed so far, and when the last key came. */
|
|
116
|
+
interface TypeaheadBuffer {
|
|
117
|
+
text: string;
|
|
118
|
+
at: number;
|
|
119
|
+
}
|
|
120
|
+
/** A buffer nothing has been typed into. */
|
|
121
|
+
declare const EMPTY_TYPEAHEAD: TypeaheadBuffer;
|
|
122
|
+
/** The item a typed `query` lands on, searching forward from `from` with
|
|
123
|
+
* wrap. A one-letter query starts after the highlight so repeating a letter
|
|
124
|
+
* cycles; a longer one starts at it so typing on keeps the match. */
|
|
125
|
+
declare function typeaheadMatch<T>(items: readonly T[], from: number | null, query: string, shape: TypeaheadShape<T>): number | null;
|
|
126
|
+
/** One typed character: the new buffer and the item it lands on. A key that
|
|
127
|
+
* repeats the whole buffer searches that one letter, so it cycles. */
|
|
128
|
+
declare function typeaheadStep<T>(items: readonly T[], from: number | null, buffer: TypeaheadBuffer, key: string, now: number, shape: TypeaheadShape<T>, resetMs?: number): {
|
|
129
|
+
buffer: TypeaheadBuffer;
|
|
130
|
+
hit: number | null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** How a focus change came about. */
|
|
134
|
+
interface FocusScopeOptions {
|
|
135
|
+
/** Whether the target shows its focus ring: the scope's `:focus-visible`. Keyboard moves show one, a press does
|
|
136
|
+
* not. Default `false` for {@link FocusScope.focus}, `true` for {@link FocusScope.moveFocus}. */
|
|
137
|
+
readonly visible?: boolean;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Keyboard focus over a set of targets, whatever draws them: a HUD's widgets and a DOM subtree's elements both
|
|
141
|
+
* answer it, so code that moves or reads focus is written once for either.
|
|
142
|
+
*/
|
|
143
|
+
interface FocusScope<T> {
|
|
144
|
+
/** The target holding keyboard focus, or null. One per scope. */
|
|
145
|
+
readonly focused: T | null;
|
|
146
|
+
/** Whether the focused target shows its focus ring. */
|
|
147
|
+
readonly focusVisible: boolean;
|
|
148
|
+
/** Focus `target`, or blur with `null`. Returns whether `target` now holds focus: false when it is not in this
|
|
149
|
+
* scope or cannot take focus, and focus is then unchanged. */
|
|
150
|
+
focus(target: T | null, options?: FocusScopeOptions): boolean;
|
|
151
|
+
/** Move focus one step through {@link tabOrder}. From nothing, `'next'` lands on the first target and `'prev'` on
|
|
152
|
+
* the last; past either end the scope blurs and returns null, which is where focus leaves it. */
|
|
153
|
+
moveFocus(direction: 'next' | 'prev', options?: FocusScopeOptions): T | null;
|
|
154
|
+
/** The targets Tab visits, in order. */
|
|
155
|
+
tabOrder(): T[];
|
|
156
|
+
/** Run `fn` whenever the focused target changes. Returns an unsubscribe. */
|
|
157
|
+
subscribeFocus(fn: (focused: T | null) => void): () => void;
|
|
158
|
+
}
|
|
159
|
+
/** Tab order: targets that declare a place come first, ascending, then the rest in the order given. In the DOM a
|
|
160
|
+
* positive `tabindex` declares one and `0` does not. */
|
|
161
|
+
declare function tabSort<T>(targets: readonly T[], declared: (target: T) => number | undefined): T[];
|
|
162
|
+
/** The target one Tab step from `from` through `order`, or null past either end. From nothing, `'next'` is the
|
|
163
|
+
* first and `'prev'` the last. A `from` that is out of the order (hidden since it took focus, say) steps from where
|
|
164
|
+
* it sits, which `isBefore` says; without it, such a `from` steps as nothing does. */
|
|
165
|
+
declare function tabStep<T>(order: readonly T[], from: T | null, direction: 'next' | 'prev', isBefore?: (a: T, b: T) => boolean): T | null;
|
|
166
|
+
|
|
167
|
+
/** Steps an arrow key asks for along one axis: one, or {@link COARSE_STEPS} with Shift, positive
|
|
168
|
+
* toward ArrowRight and ArrowUp. Under `'rtl'` ArrowLeft and ArrowRight swap, as a native range
|
|
169
|
+
* input's do. `null` for any other key. A caller multiplies by its own step. */
|
|
170
|
+
declare function arrowSteps(key: string, shift: boolean, dir?: 'ltr' | 'rtl'): number | null;
|
|
171
|
+
/** Steps an arrow key asks for on a plane, in screen axes: `dy` is positive toward ArrowDown.
|
|
172
|
+
* One step, or {@link COARSE_STEPS} with Shift; `null` for any other key. */
|
|
173
|
+
declare function arrowMove(key: string, shift: boolean): {
|
|
174
|
+
dx: number;
|
|
175
|
+
dy: number;
|
|
176
|
+
} | null;
|
|
177
|
+
/** What a key does along a range: {@link arrowSteps}, the Page keys ({@link COARSE_STEPS} steps),
|
|
178
|
+
* or Home and End (an end of the range). `null` for any other key. */
|
|
179
|
+
declare function rangeKey(key: string, shift: boolean, dir?: 'ltr' | 'rtl'): number | 'home' | 'end' | null;
|
|
180
|
+
|
|
181
|
+
/** A value on a slider's track the thumb can come to rest on, with whatever label draws under it. */
|
|
182
|
+
interface SliderStop<L = unknown> {
|
|
183
|
+
value: number;
|
|
184
|
+
label?: L;
|
|
185
|
+
}
|
|
186
|
+
/** The stops that are actually reachable: inside the range, deduped, ascending.
|
|
187
|
+
* A duplicate keeps the first label given for its value. */
|
|
188
|
+
declare function usableStops<L>(stops: readonly (number | SliderStop<L>)[] | undefined, min: number, max: number): SliderStop<L>[];
|
|
189
|
+
/** The stop nearest `v` along the track, or `v` itself when none lies within
|
|
190
|
+
* `tolerance` (a track fraction). Measured on the track rather than in value
|
|
191
|
+
* units so evenly spaced stops attract over the same distance. */
|
|
192
|
+
declare function attract(v: number, stops: readonly number[], tolerance: number, toFraction: (v: number) => number): number;
|
|
193
|
+
/** Keep thumb `index` strictly between its neighbors: a step apart, or a thousandth of the span without one. */
|
|
194
|
+
declare function clampOrdered(v: number, thumbs: readonly {
|
|
195
|
+
value: number;
|
|
196
|
+
}[], index: number, min: number, max: number, step: number | undefined): number;
|
|
197
|
+
/** What a key press on one thumb of a slider reads. */
|
|
198
|
+
interface SliderKeyShape {
|
|
199
|
+
thumbs: readonly {
|
|
200
|
+
value: number;
|
|
201
|
+
}[];
|
|
202
|
+
/** The thumb the key went to. */
|
|
203
|
+
index: number;
|
|
204
|
+
/** The ends of the track. */
|
|
205
|
+
min: number;
|
|
206
|
+
max: number;
|
|
207
|
+
step?: number;
|
|
208
|
+
/** Usable stops, ascending. With any, a keystroke moves by whole stops rather than by value. */
|
|
209
|
+
stops?: readonly number[];
|
|
210
|
+
/** Keep the thumb between its neighbors. */
|
|
211
|
+
ordered?: boolean;
|
|
212
|
+
/** The finite range, where the track runs past it to an endless end's stop. Default the track. */
|
|
213
|
+
range?: readonly [number, number];
|
|
214
|
+
/** Under `'rtl'` the horizontal arrows are mirrored, as the values run from the right end. */
|
|
215
|
+
dir?: 'ltr' | 'rtl';
|
|
216
|
+
/** This thumb's own limits inside the track. Read only for a key the slider answers to. */
|
|
217
|
+
bounds?(): readonly [number, number];
|
|
218
|
+
/** Where the thumb lands `steps` presses away, for a track whose values aren't what the user counts in. */
|
|
219
|
+
keyStep?(steps: number): number;
|
|
220
|
+
}
|
|
221
|
+
/** The value one key moves thumb `index` to: arrows by a step (ten with Shift), the Page keys by ten,
|
|
222
|
+
* Home and End to the thumb's ends. `null` for a key the slider does not answer to. */
|
|
223
|
+
declare function sliderKey(key: string, shift: boolean, shape: SliderKeyShape): number | null;
|
|
224
|
+
|
|
225
|
+
/** How a text field's edit ends: `'commit'` writes what was typed, `'cancel'` drops it and puts back what was there. */
|
|
226
|
+
type EditEnd = 'commit' | 'cancel';
|
|
227
|
+
/** What a field varies in how its edit ends: what Escape does (`'cancel'` by default), and whether the field holds
|
|
228
|
+
* more than one line, which leaves Shift+Enter to type a line break rather than commit. */
|
|
229
|
+
interface EditEndShape {
|
|
230
|
+
escape?: EditEnd;
|
|
231
|
+
multiline?: boolean;
|
|
232
|
+
}
|
|
233
|
+
/** What may end an edit: a key, with whether Shift is held, or the field losing focus. A key {@link imeKey} claims is
|
|
234
|
+
* the caller's to hold back. */
|
|
235
|
+
type EditCause = 'blur' | {
|
|
236
|
+
key: string;
|
|
237
|
+
shift?: boolean;
|
|
238
|
+
};
|
|
239
|
+
/** How `cause` ends a text field's edit, or null when it leaves the edit open. Enter and blur commit, Escape cancels
|
|
240
|
+
* unless `shape` says otherwise. */
|
|
241
|
+
declare function editEnd(cause: EditCause, shape?: EditEndShape): EditEnd | null;
|
|
242
|
+
/** A keydown as the platform reports it, enough to tell whether an input method owns it. */
|
|
243
|
+
interface KeySource {
|
|
244
|
+
key: string;
|
|
245
|
+
isComposing?: boolean;
|
|
246
|
+
keyCode?: number;
|
|
247
|
+
}
|
|
248
|
+
/** Whether a keydown belongs to an input method, which a field must leave alone: Enter there confirms a candidate and
|
|
249
|
+
* Escape dismisses them. Safari sends the Enter that ends a composition with `isComposing` already false, but with
|
|
250
|
+
* `keyCode` 229. */
|
|
251
|
+
declare function imeKey(e: KeySource): boolean;
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Moving items within an ordered list. A destination is a gap, counted before anything is taken out: 0 before the
|
|
255
|
+
* first item, `length` after the last, and `i` between `items[i - 1]` and `items[i]` — the gap a UI marks while it
|
|
256
|
+
* carries something. The items moved are named by their indices, so a selection of several moves in one go.
|
|
257
|
+
*/
|
|
258
|
+
/** `items` with the items at indices `at` taken out and put back, in their order, in gap `to`. */
|
|
259
|
+
declare function moveItems<T>(items: readonly T[], at: readonly number[], to: number): T[];
|
|
260
|
+
/** The index the first of the items at `at` holds once {@link moveItems} has put them in gap `to`. */
|
|
261
|
+
declare function movedIndex(at: readonly number[], to: number): number;
|
|
262
|
+
/** Whether putting the items at `at` in gap `to` leaves them where they are: a contiguous run dropped in a gap at
|
|
263
|
+
* either of its ends or inside it. False for no items. */
|
|
264
|
+
declare function isNoopDrop(at: readonly number[], to: number): boolean;
|
|
265
|
+
/** The gap a one-place move sends the items at `at` to: before the item ahead of the first (`by` -1), or after the
|
|
266
|
+
* item past the last (`by` 1). Null where that runs off either end of a list `length` long, or for no items. */
|
|
267
|
+
declare function nudgeGap(at: readonly number[], by: -1 | 1, length: number): number | null;
|
|
268
|
+
/**
|
|
269
|
+
* The gap a point `p` along a line of items falls in, where each item splits at its middle: how many of the
|
|
270
|
+
* middles `mids`, given in item order, `p` has reached. A line whose middles run backward, as a right-to-left row
|
|
271
|
+
* does along x, counts the other way. For items that sit side by side — rows, tabs, chips.
|
|
272
|
+
*/
|
|
273
|
+
declare function gapByMidpoint(mids: readonly number[], p: number): number;
|
|
274
|
+
/** Which of the gaps at positions `gaps` along a line is nearest `p`, as an index into `gaps`; -1 for none. For
|
|
275
|
+
* gaps drawn at places of their own, between items set apart however the layout put them: picking the nearest
|
|
276
|
+
* keeps the gap chosen the one whose mark is drawn closest. */
|
|
277
|
+
declare function nearestGap(gaps: readonly number[], p: number): number;
|
|
278
|
+
/** The keys and modifiers a nudge chord reads off a keystroke. */
|
|
279
|
+
interface NudgeKey {
|
|
280
|
+
key: string;
|
|
281
|
+
altKey: boolean;
|
|
282
|
+
ctrlKey: boolean;
|
|
283
|
+
metaKey: boolean;
|
|
284
|
+
}
|
|
285
|
+
/** Alt with neither Ctrl nor Cmd held: the chord that moves an item instead of moving focus. */
|
|
286
|
+
declare function isNudgeChord(e: NudgeKey): boolean;
|
|
287
|
+
/** Which way the nudge chord moves an item: `-1` for Alt+`back`, `1` for Alt+`on`, `null` for any other key.
|
|
288
|
+
* The keys are Up and Down unless named. */
|
|
289
|
+
declare function nudgeOf(e: NudgeKey, back?: string, on?: string): -1 | 1 | null;
|
|
290
|
+
|
|
291
|
+
/** Clock and timer, injected so tests and hosts control time. */
|
|
292
|
+
interface Scheduler {
|
|
293
|
+
now(): number;
|
|
294
|
+
/** Run `fn` after `ms`; returns a cancel. */
|
|
295
|
+
set(fn: () => void, ms: number): () => void;
|
|
296
|
+
}
|
|
297
|
+
/** The real-time scheduler: `Date.now` and `setTimeout`. */
|
|
298
|
+
declare const timerScheduler: Scheduler;
|
|
299
|
+
|
|
300
|
+
/** How long a held stepper waits before it repeats, in milliseconds. */
|
|
301
|
+
declare const REPEAT_DELAY_MS = 400;
|
|
302
|
+
/** How often a held stepper steps once it repeats, in milliseconds. */
|
|
303
|
+
declare const REPEAT_EVERY_MS = 60;
|
|
304
|
+
/** The timing of a {@link createStepRepeat}. Each is read when it is next used, so a getter can change it mid-hold. */
|
|
305
|
+
interface StepRepeatOptions {
|
|
306
|
+
/** Default {@link REPEAT_DELAY_MS}. */
|
|
307
|
+
delayMs?: number;
|
|
308
|
+
/** Default {@link REPEAT_EVERY_MS}. */
|
|
309
|
+
everyMs?: number;
|
|
310
|
+
/** Default {@link timerScheduler}. */
|
|
311
|
+
scheduler?: Pick<Scheduler, 'set'>;
|
|
312
|
+
}
|
|
313
|
+
/** A held stepper: {@link createStepRepeat}. */
|
|
314
|
+
interface StepRepeat<K> {
|
|
315
|
+
/** Step `key` once now, then after the delay once every interval until {@link stop}. Replaces any hold. */
|
|
316
|
+
start(key: K): void;
|
|
317
|
+
stop(): void;
|
|
318
|
+
/** The key being held, or null. */
|
|
319
|
+
held(): K | null;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Hold-to-repeat stepping, whatever draws the stepper: `step` moves the value by `key`, and `spent` says `key`
|
|
323
|
+
* has nowhere left to go, which ends the hold. Both are called at the moment of each step.
|
|
324
|
+
*/
|
|
325
|
+
declare function createStepRepeat<K>(step: (key: K) => void, spent: (key: K) => boolean, opts?: StepRepeatOptions): StepRepeat<K>;
|
|
326
|
+
|
|
327
|
+
/** The sizes a box may take, in CSS px. `Infinity` for a max leaves that axis free. */
|
|
328
|
+
interface Constraints {
|
|
329
|
+
readonly minWidth: number;
|
|
330
|
+
readonly maxWidth: number;
|
|
331
|
+
readonly minHeight: number;
|
|
332
|
+
readonly maxHeight: number;
|
|
333
|
+
}
|
|
334
|
+
/** No bound on either axis: a box at its own size. */
|
|
335
|
+
declare const UNBOUNDED: Constraints;
|
|
336
|
+
/** Exactly `width` by `height`: a box placed into a fixed rectangle. */
|
|
337
|
+
declare function exactly(width: number, height: number): Constraints;
|
|
338
|
+
/** Exactly `width` wide and as tall as the box likes: a box filling its column. */
|
|
339
|
+
declare function fixedWidth(width: number): Constraints;
|
|
340
|
+
/** `size` clamped to `[min, max]`. A max below its min yields the min, as CSS does. */
|
|
341
|
+
declare function clampTo(size: number, min: number, max: number): number;
|
|
342
|
+
/** The font a run of text is measured in. `family` is a CSS font-family list; a target that draws with
|
|
343
|
+
* fonts of its own maps it onto one. */
|
|
344
|
+
interface FontSpec {
|
|
345
|
+
readonly family?: string;
|
|
346
|
+
readonly size: number;
|
|
347
|
+
readonly weight?: number;
|
|
348
|
+
}
|
|
349
|
+
/** A run of text's advance width and the ascent and descent its line is set with, in px. */
|
|
350
|
+
interface TextExtent {
|
|
351
|
+
readonly width: number;
|
|
352
|
+
readonly ascent: number;
|
|
353
|
+
readonly descent: number;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* What measuring reads from where the component is drawn: its theme's tokens and its text metrics. A DOM element's
|
|
357
|
+
* computed style answers it, and so does a HUD's resolved theme and font atlas, which is what lets one `measure`
|
|
358
|
+
* serve both.
|
|
359
|
+
*/
|
|
360
|
+
interface MeasureEnv {
|
|
361
|
+
/** A token's value (`'28px'`, `'500'`), or undefined where nothing sets it. */
|
|
362
|
+
token(name: string): string | undefined;
|
|
363
|
+
text(text: string, font: FontSpec): TextExtent;
|
|
364
|
+
}
|
|
365
|
+
/** A px token as a number: `name`, else `fallback`. Throws where neither is a px length, since silently reading
|
|
366
|
+
* 0 collapses whatever the token sizes. */
|
|
367
|
+
declare function envPx(env: MeasureEnv, name: string, fallback?: number | string): number;
|
|
368
|
+
/** A unitless numeric token, else `fallback`. */
|
|
369
|
+
declare function envNumber(env: MeasureEnv, name: string, fallback: number): number;
|
|
370
|
+
/** A box's size, its first baseline, and where its parts sit inside it. */
|
|
371
|
+
interface Measured<L> {
|
|
372
|
+
readonly width: number;
|
|
373
|
+
readonly height: number;
|
|
374
|
+
/** From the box's top to its first line of text's alphabetic baseline, as CSS `align-items: baseline` reads
|
|
375
|
+
* it; the bottom edge for a box with no text. */
|
|
376
|
+
readonly baseline: number;
|
|
377
|
+
/** Where each part sits, relative to the box's top left. Its shape is the component's own. */
|
|
378
|
+
readonly layout: L;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* The baseline of a line of text `lineHeight` tall centered in a box `boxHeight` tall, from the box's top: CSS's
|
|
382
|
+
* half-leading model, the line's ascent and descent centered inside its line height.
|
|
383
|
+
*/
|
|
384
|
+
declare function centeredBaseline(boxHeight: number, lineHeight: number, extent: TextExtent): number;
|
|
385
|
+
/** `r` moved by `(dx, dy)`. */
|
|
386
|
+
declare function offsetRect(r: Rect, dx: number, dy: number): Rect;
|
|
387
|
+
/**
|
|
388
|
+
* The one contract a control implements on every target it is drawn on: measure, then draw.
|
|
389
|
+
*
|
|
390
|
+
* - `measure` is shared code. It reads only `state` and the target's {@link MeasureEnv}, so a DOM component and a HUD
|
|
391
|
+
* widget measuring the same state in the same theme answer the same box, and a host can lay either out before it
|
|
392
|
+
* exists.
|
|
393
|
+
* - `draw` is per target, and its output is the target's own: a React element for the DOM, draw commands for a
|
|
394
|
+
* HUD. It is not handed the measured box. A target that positions by coordinates (a HUD) calls `measure` itself
|
|
395
|
+
* with its bounds as {@link exactly}; a DOM target lays its parts out with CSS from the same tokens `measure`
|
|
396
|
+
* reads, which a measure-to-DOM test holds it to. A paint sink shared by both was the alternative, and it would
|
|
397
|
+
* make the DOM rebuild by hand what its stylesheet already says.
|
|
398
|
+
*
|
|
399
|
+
* Both halves read which parts `state` shows from one function the component exports (`buttonParts`,
|
|
400
|
+
* `listMode`), so neither target can show a part the other lacks.
|
|
401
|
+
*/
|
|
402
|
+
interface HeadlessComponent<S, L, T, O> {
|
|
403
|
+
measure(state: S, constraints: Constraints, env: MeasureEnv): Measured<L>;
|
|
404
|
+
draw(state: S, target: T): O;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** A button's height and type scale. */
|
|
408
|
+
type ButtonSize = 'sm' | 'md';
|
|
409
|
+
/**
|
|
410
|
+
* What a button shows, as every target reads it. `I` is how a target names an icon (a React node, a glyph name);
|
|
411
|
+
* measuring only asks whether one is there.
|
|
412
|
+
*/
|
|
413
|
+
interface ButtonFace<I = unknown> {
|
|
414
|
+
/** The text measured for the label. A label that is not text passes its text content, or `''`. Absent, the
|
|
415
|
+
* button has no label part. */
|
|
416
|
+
readonly label?: string;
|
|
417
|
+
/** Default `'md'`. */
|
|
418
|
+
readonly size?: ButtonSize;
|
|
419
|
+
/** The button is its icon alone, `icon`, in a square box. */
|
|
420
|
+
readonly iconOnly?: boolean;
|
|
421
|
+
readonly icon?: I;
|
|
422
|
+
readonly leadingIcon?: I;
|
|
423
|
+
readonly trailingIcon?: I;
|
|
424
|
+
/** A spinner stands in for the leading icon (the icon, when `iconOnly`). */
|
|
425
|
+
readonly loading?: boolean;
|
|
426
|
+
}
|
|
427
|
+
/** One piece of a button, in the order it is laid out. */
|
|
428
|
+
type ButtonPart = 'spinner' | 'icon' | 'leading' | 'label' | 'trailing';
|
|
429
|
+
/** Which parts `face` shows, left to right. Both targets draw exactly these. */
|
|
430
|
+
declare function buttonParts(face: ButtonFace): ButtonPart[];
|
|
431
|
+
/** A button's sizes in px, read from its theme. */
|
|
432
|
+
interface ButtonMetrics {
|
|
433
|
+
readonly height: number;
|
|
434
|
+
/** Inset of the content from each side. 0 on an icon-only button, which is square. */
|
|
435
|
+
readonly padX: number;
|
|
436
|
+
/** Between adjacent parts. */
|
|
437
|
+
readonly gap: number;
|
|
438
|
+
readonly iconSize: number;
|
|
439
|
+
readonly spinnerSize: number;
|
|
440
|
+
readonly font: FontSpec;
|
|
441
|
+
}
|
|
442
|
+
/** The spinner's drawn size at either button size. */
|
|
443
|
+
declare const BUTTON_SPINNER_SIZE = 14;
|
|
444
|
+
/** The tokens a button of `size` is sized by: the same ones ui's `Button.module.css` lays it out with, so the DOM
|
|
445
|
+
* button and the HUD one agree in any theme. */
|
|
446
|
+
declare function buttonMetrics(env: MeasureEnv, size?: ButtonSize, iconOnly?: boolean): ButtonMetrics;
|
|
447
|
+
/** Where each of a button's parts sits. */
|
|
448
|
+
interface ButtonLayout {
|
|
449
|
+
readonly parts: readonly {
|
|
450
|
+
readonly part: ButtonPart;
|
|
451
|
+
readonly rect: Rect;
|
|
452
|
+
}[];
|
|
453
|
+
readonly metrics: ButtonMetrics;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* A button's box: its control height, and its parts' widths plus gaps plus side padding (square when icon-only),
|
|
457
|
+
* held to `c`. The parts are centered in the box, as flex centers them. In a box too narrow for them, the label
|
|
458
|
+
* gives up width first, down to none.
|
|
459
|
+
*/
|
|
460
|
+
declare function measureButton(face: ButtonFace, c: Constraints, env: MeasureEnv): Measured<ButtonLayout>;
|
|
461
|
+
/** Where a press stands: the pointer over the control, and a press held on it. */
|
|
462
|
+
interface PressState {
|
|
463
|
+
readonly hovered: boolean;
|
|
464
|
+
readonly held: boolean;
|
|
465
|
+
}
|
|
466
|
+
/** Nothing over the control and nothing held. */
|
|
467
|
+
declare const IDLE_PRESS: PressState;
|
|
468
|
+
/** One pointer event as a press reads it. `inside` says whether the point is over the control. */
|
|
469
|
+
type PressInput = {
|
|
470
|
+
type: 'down';
|
|
471
|
+
} | {
|
|
472
|
+
type: 'move' | 'up';
|
|
473
|
+
inside: boolean;
|
|
474
|
+
} | {
|
|
475
|
+
type: 'cancel' | 'enter' | 'leave';
|
|
476
|
+
};
|
|
477
|
+
/** What a pointer event does to a press: the new state, and whether it completed one (released over the control
|
|
478
|
+
* it went down on). A drag off the control lets go of the hold and a drag back on takes it again, as a native
|
|
479
|
+
* button's `:active` does. */
|
|
480
|
+
declare function pressStep(s: PressState, input: PressInput): {
|
|
481
|
+
state: PressState;
|
|
482
|
+
pressed: boolean;
|
|
483
|
+
};
|
|
484
|
+
/** Whether a key presses a focused button: Enter or Space, as a native `<button>` takes them. */
|
|
485
|
+
declare function isPressKey(key: string): boolean;
|
|
486
|
+
|
|
487
|
+
/** What a row list can do, which decides its semantics: nothing (`list`), select (`listbox`), or hold controls in
|
|
488
|
+
* its rows (`grid`, since an option may not contain interactive content). */
|
|
489
|
+
type ListMode = 'list' | 'listbox' | 'grid';
|
|
490
|
+
/** What decides a row list's {@link ListMode}. */
|
|
491
|
+
interface ListModeInput {
|
|
492
|
+
/** Rows are selectable, one or many at once. */
|
|
493
|
+
readonly selection?: 'single' | 'multi';
|
|
494
|
+
/** Rows answer a press. */
|
|
495
|
+
readonly activatable?: boolean;
|
|
496
|
+
/** Some row holds controls of its own. */
|
|
497
|
+
readonly controls?: boolean;
|
|
498
|
+
}
|
|
499
|
+
/** A row list's mode: `grid` when a row holds controls or rows activate without selecting, else `listbox` when
|
|
500
|
+
* they select, else `list`. */
|
|
501
|
+
declare function listMode(o: ListModeInput): ListMode;
|
|
502
|
+
/** The ARIA roles of a row list's container and its rows, by mode. */
|
|
503
|
+
declare const LIST_ROLES: Readonly<Record<ListMode, {
|
|
504
|
+
readonly container: string;
|
|
505
|
+
readonly row: string;
|
|
506
|
+
}>>;
|
|
507
|
+
/** Which edge of row `i` of `count` carries the seam for a drop at gap `dropIndex`, if either. */
|
|
508
|
+
declare function dropSide(dropIndex: number | null | undefined, i: number, count: number): 'before' | 'after' | undefined;
|
|
509
|
+
/** One row as measuring reads it. The leading decoration and the trailing controls are measured by their width. */
|
|
510
|
+
interface RowFace {
|
|
511
|
+
readonly id: string;
|
|
512
|
+
readonly label: string;
|
|
513
|
+
readonly leadingWidth?: number;
|
|
514
|
+
readonly trailingWidth?: number;
|
|
515
|
+
}
|
|
516
|
+
/** A row list as measuring reads it. */
|
|
517
|
+
interface RowListFace {
|
|
518
|
+
readonly rows: readonly RowFace[];
|
|
519
|
+
/** Shown in place of the rows when there are none. Default an em dash, as ui's `ItemList` shows. */
|
|
520
|
+
readonly empty?: string;
|
|
521
|
+
}
|
|
522
|
+
/** Space between adjacent rows: ui's `ItemList.module.css` `.list { gap: 1px }`. */
|
|
523
|
+
declare const ROW_GAP = 1;
|
|
524
|
+
/** A row list's sizes in px, read from its theme. */
|
|
525
|
+
interface RowListMetrics {
|
|
526
|
+
readonly rowHeight: number;
|
|
527
|
+
readonly padX: number;
|
|
528
|
+
/** Between a row's leading decoration, its label, and its controls. */
|
|
529
|
+
readonly gap: number;
|
|
530
|
+
/** The empty state's inset from top and bottom. */
|
|
531
|
+
readonly emptyPadY: number;
|
|
532
|
+
readonly font: FontSpec;
|
|
533
|
+
/** A line of text's height: the font size times `--wzl-leading-snug`, which `.list` sets. */
|
|
534
|
+
readonly lineHeight: number;
|
|
535
|
+
}
|
|
536
|
+
/** The tokens a row list is sized by: the ones `ItemList.module.css` lays it out with. */
|
|
537
|
+
declare function rowListMetrics(env: MeasureEnv): RowListMetrics;
|
|
538
|
+
/** Where one row and its parts sit, relative to the list's top left. The label fills what the leading
|
|
539
|
+
* decoration and the controls leave. */
|
|
540
|
+
interface RowBox {
|
|
541
|
+
readonly id: string;
|
|
542
|
+
readonly rect: Rect;
|
|
543
|
+
readonly leading?: Rect;
|
|
544
|
+
readonly label: Rect;
|
|
545
|
+
readonly trailing?: Rect;
|
|
546
|
+
}
|
|
547
|
+
/** Where a row list's rows sit, or its empty state when it has none. */
|
|
548
|
+
interface RowListLayout {
|
|
549
|
+
readonly rows: readonly RowBox[];
|
|
550
|
+
readonly empty?: Rect;
|
|
551
|
+
readonly metrics: RowListMetrics;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* A row list's box: rows a control height tall, {@link ROW_GAP} apart, as wide as the widest row's leading, label
|
|
555
|
+
* and controls with their gaps and padding, held to `c`. A list stretched to its column passes that width as both
|
|
556
|
+
* bounds. With no rows it is the empty state's line, padded.
|
|
557
|
+
*/
|
|
558
|
+
declare function measureRowList(face: RowListFace, c: Constraints, env: MeasureEnv): Measured<RowListLayout>;
|
|
559
|
+
/** Index of the row under list-local `y`, or null between rows and past either end. */
|
|
560
|
+
declare function rowAtY(layout: RowListLayout, y: number): number | null;
|
|
561
|
+
|
|
562
|
+
export { BUTTON_SPINNER_SIZE, type ButtonFace, type ButtonLayout, type ButtonMetrics, type ButtonPart, type ButtonSize, type Constraints, EMPTY_TYPEAHEAD, type EditCause, type EditEnd, type EditEndShape, type FocusScope, type FocusScopeOptions, type FontSpec, type HeadlessComponent, IDLE_PRESS, type KeyPress, type KeySource, LIST_ROLES, type ListMode, type ListModeInput, type ListShape, type MeasureEnv, type Measured, type NudgeKey, type PressInput, type PressState, REPEAT_DELAY_MS, REPEAT_EVERY_MS, ROW_GAP, type RovingResult, type RowBox, type RowFace, type RowKeyAction, type RowKeysShape, type RowListFace, type RowListLayout, type RowListMetrics, type Scheduler, type SliderKeyShape, type SliderStop, type StepRepeat, type StepRepeatOptions, TYPEAHEAD_RESET_MS, type TextExtent, type TreeKeyResult, type TreeShape, type TypeaheadBuffer, type TypeaheadShape, UNBOUNDED, arrowMove, arrowSteps, attract, buttonMetrics, buttonParts, centeredBaseline, clampOrdered, clampTo, createStepRepeat, dropSide, editEnd, envNumber, envPx, exactly, firstStop, fixedWidth, gapByMidpoint, imeKey, isNoopDrop, isNudgeChord, isPressKey, listMode, measureButton, measureRowList, moveItems, movedIndex, nearestGap, nudgeGap, nudgeOf, offsetRect, pressStep, rangeKey, rovingKey, rowAtY, rowKey, rowListMetrics, sliderKey, stepStop, tabSort, tabStep, timerScheduler, treeKey, typeaheadMatch, typeaheadStep, usableStops };
|