@bun-native/protocol 0.0.0-stage → 0.33.1
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 +4 -2
- package/bun-native-source.json +75 -0
- package/package.json +26 -4
- package/src/app-menu.ts +217 -0
- package/src/dialogs.ts +146 -0
- package/src/drops.ts +49 -0
- package/src/elements.ts +473 -0
- package/src/files.ts +295 -0
- package/src/framing.ts +109 -0
- package/src/index.ts +136 -0
- package/src/message-dialogs.ts +131 -0
- package/src/messages.ts +1490 -0
- package/src/processes.ts +263 -0
- package/src/services.ts +208 -0
- package/src/values.ts +61 -0
package/src/elements.ts
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { ProtocolError } from './framing.ts'
|
|
2
|
+
import { fields, integer, keys, number, object, string } from './values.ts'
|
|
3
|
+
|
|
4
|
+
export interface Style {
|
|
5
|
+
direction?: 'row' | 'column'
|
|
6
|
+
gap?: number
|
|
7
|
+
padding?: number
|
|
8
|
+
width?: number | 'fill'
|
|
9
|
+
height?: number | 'fill'
|
|
10
|
+
min_width?: number
|
|
11
|
+
max_width?: number
|
|
12
|
+
min_height?: number
|
|
13
|
+
max_height?: number
|
|
14
|
+
flex_grow?: number
|
|
15
|
+
flex_shrink?: number
|
|
16
|
+
align_items?: 'start' | 'center' | 'end' | 'stretch'
|
|
17
|
+
justify_content?: 'start' | 'center' | 'end' | 'space_between' | 'space_around' | 'space_evenly'
|
|
18
|
+
overflow?: 'visible' | 'hidden' | 'scroll_x' | 'scroll_y' | 'scroll'
|
|
19
|
+
/** Requires `scroll_y` or `scroll`. `end` keeps a viewport already at its end pinned there. */
|
|
20
|
+
scroll_anchor?: 'start' | 'end'
|
|
21
|
+
background?: number
|
|
22
|
+
color?: number
|
|
23
|
+
font_size?: number
|
|
24
|
+
/** `style_decoration_v1`: one uniform border width, 0–64 logical pixels. */
|
|
25
|
+
border_width?: number
|
|
26
|
+
/** `style_decoration_v1`: unsigned 24-bit RGB. */
|
|
27
|
+
border_color?: number
|
|
28
|
+
/** `style_decoration_v1`: one radius for every corner, 0–4,096 logical pixels. */
|
|
29
|
+
corner_radius?: number
|
|
30
|
+
/** `style_decoration_v1`: 0–1, multiplying descendants too. */
|
|
31
|
+
opacity?: number
|
|
32
|
+
/** `style_decoration_v1`: inherited 100–900 in steps of 100. */
|
|
33
|
+
font_weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type LeafStyle = Omit<Style, 'overflow' | 'scroll_anchor'> & { overflow?: 'visible' | 'hidden' }
|
|
37
|
+
/** A split's `direction` selects its axis; gap, alignment and scrolling do not apply. */
|
|
38
|
+
export type SplitStyle = Omit<LeafStyle, 'gap' | 'align_items' | 'justify_content'>
|
|
39
|
+
|
|
40
|
+
/** Main-axis thickness of the host-drawn split divider, in logical pixels. */
|
|
41
|
+
export const SPLIT_DIVIDER = 6
|
|
42
|
+
|
|
43
|
+
/** Style fields that require `style_decoration_v1`. */
|
|
44
|
+
export const DECORATION_FIELDS = Object.freeze([
|
|
45
|
+
'border_width',
|
|
46
|
+
'border_color',
|
|
47
|
+
'corner_radius',
|
|
48
|
+
'opacity',
|
|
49
|
+
'font_weight',
|
|
50
|
+
] as const)
|
|
51
|
+
|
|
52
|
+
/** Overlays nested inside overlays, the outermost counting as one. */
|
|
53
|
+
export const MAX_OVERLAY_DEPTH = 8
|
|
54
|
+
|
|
55
|
+
/** Requires the `overlay_keyboard_v1` capability; defaults are omitted when normalized. */
|
|
56
|
+
export interface OverlayKeyboard {
|
|
57
|
+
/** Focus the layer's first enabled control when it opens. */
|
|
58
|
+
autofocus?: boolean
|
|
59
|
+
/** `menu`: Up/Down/Home/End rove focus among the layer's controls and printable keys type ahead. */
|
|
60
|
+
navigation?: 'tab' | 'menu'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* `window` covers the viewport and blocks pointer input beneath it; `parent`
|
|
65
|
+
* is content-sized below its parent's bottom-left corner. Only window
|
|
66
|
+
* overlays may be modal. `surface: 'window'` (capability `popup_windows_v1`)
|
|
67
|
+
* paints a parent-anchored overlay into an owned popup window that may extend
|
|
68
|
+
* beyond its parent window; it needs a fixed pixel `width` and `height`.
|
|
69
|
+
*/
|
|
70
|
+
export type OverlayElement = (
|
|
71
|
+
| ({
|
|
72
|
+
kind: 'overlay'
|
|
73
|
+
anchor?: 'window'
|
|
74
|
+
modal?: boolean
|
|
75
|
+
surface?: 'layer'
|
|
76
|
+
style?: Style
|
|
77
|
+
} & OverlayKeyboard)
|
|
78
|
+
| ({
|
|
79
|
+
kind: 'overlay'
|
|
80
|
+
anchor: 'parent'
|
|
81
|
+
modal?: false
|
|
82
|
+
surface?: 'layer'
|
|
83
|
+
style?: Style
|
|
84
|
+
} & OverlayKeyboard)
|
|
85
|
+
| {
|
|
86
|
+
kind: 'overlay'
|
|
87
|
+
anchor: 'parent'
|
|
88
|
+
modal?: false
|
|
89
|
+
surface: 'window'
|
|
90
|
+
/** A popup window never takes keyboard focus from its parent window. */
|
|
91
|
+
autofocus?: false
|
|
92
|
+
navigation?: 'tab'
|
|
93
|
+
style: Style & { width: number; height: number }
|
|
94
|
+
}
|
|
95
|
+
) &
|
|
96
|
+
AccessibleLabel
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Requires `accessibility_v1`: the accessible name of an overlay's dialog or
|
|
100
|
+
* a split's divider. It is never painted and counts toward the text budgets.
|
|
101
|
+
*/
|
|
102
|
+
export interface AccessibleLabel {
|
|
103
|
+
accessible_label?: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The largest `accessible_set_size` and `accessible_position`. */
|
|
107
|
+
const MAX_ACCESSIBLE_SET_SIZE = 2_147_483_647
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Requires `accessibility_lists_v1`. A box without `accessible_role` is layout
|
|
111
|
+
* only and has no accessible node. A list's `accessible_set_size` counts every
|
|
112
|
+
* item, mounted or not; an item's `accessible_position` is its 1-based place
|
|
113
|
+
* in that full set, so assistive technology knows a windowed list's extent.
|
|
114
|
+
*/
|
|
115
|
+
export interface BoxAccessibility {
|
|
116
|
+
accessible_role?: 'list' | 'list_item'
|
|
117
|
+
/** Only with a role: the list's or item's accessible name. */
|
|
118
|
+
accessible_label?: string
|
|
119
|
+
/** Only on a `list`. */
|
|
120
|
+
accessible_set_size?: number
|
|
121
|
+
/** Only on a `list_item`; 1-based. */
|
|
122
|
+
accessible_position?: number
|
|
123
|
+
/** Only on a `list_item`. Present: the item is selectable; true when selected. */
|
|
124
|
+
accessible_selected?: boolean
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type Element =
|
|
128
|
+
| ({ kind: 'box'; style?: Style } & BoxAccessibility)
|
|
129
|
+
| {
|
|
130
|
+
kind: 'text'
|
|
131
|
+
text: string
|
|
132
|
+
/**
|
|
133
|
+
* Requires `accessibility_lists_v1`: announce the text when the node
|
|
134
|
+
* appears and whenever it changes.
|
|
135
|
+
*/
|
|
136
|
+
accessible_live?: 'polite' | 'assertive'
|
|
137
|
+
style?: LeafStyle
|
|
138
|
+
}
|
|
139
|
+
| { kind: 'button'; text: string; disabled?: boolean; style?: LeafStyle }
|
|
140
|
+
| {
|
|
141
|
+
kind: 'input'
|
|
142
|
+
value: string
|
|
143
|
+
placeholder?: string
|
|
144
|
+
/** Requires `input_states_v1`. Not focusable, editable or selectable. */
|
|
145
|
+
disabled?: boolean
|
|
146
|
+
/** Requires `input_states_v1`. Focusable and copyable; native edits are rejected. */
|
|
147
|
+
read_only?: boolean
|
|
148
|
+
style?: LeafStyle
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* `split_v1`: at most two panes and a host-owned draggable divider. `ratio`
|
|
152
|
+
* (default 0.5) is the leading pane's share after the divider; the host
|
|
153
|
+
* keeps a user's drag until a different ratio is declared.
|
|
154
|
+
*/
|
|
155
|
+
| ({ kind: 'split'; ratio?: number; min_size?: number; style?: SplitStyle } & AccessibleLabel)
|
|
156
|
+
| OverlayElement
|
|
157
|
+
|
|
158
|
+
function choice<T extends string>(value: unknown, name: string, values: readonly T[]): T {
|
|
159
|
+
for (const candidate of values) if (candidate === value) return candidate
|
|
160
|
+
throw new ProtocolError(name + ' must be one of ' + values.join(', '))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function requireLeafStyle(style: Style): asserts style is LeafStyle {
|
|
164
|
+
if (style.overflow !== undefined && style.overflow !== 'visible' && style.overflow !== 'hidden')
|
|
165
|
+
throw new ProtocolError('Scrolling is supported only on box and overlay elements')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const STYLE_KEYS = fields(
|
|
169
|
+
'direction',
|
|
170
|
+
'gap',
|
|
171
|
+
'padding',
|
|
172
|
+
'width',
|
|
173
|
+
'height',
|
|
174
|
+
'min_width',
|
|
175
|
+
'max_width',
|
|
176
|
+
'min_height',
|
|
177
|
+
'max_height',
|
|
178
|
+
'flex_grow',
|
|
179
|
+
'flex_shrink',
|
|
180
|
+
'align_items',
|
|
181
|
+
'justify_content',
|
|
182
|
+
'overflow',
|
|
183
|
+
'scroll_anchor',
|
|
184
|
+
'background',
|
|
185
|
+
'color',
|
|
186
|
+
'font_size',
|
|
187
|
+
...DECORATION_FIELDS
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
function parseStyle(value: unknown): Style {
|
|
191
|
+
const data = object(value)
|
|
192
|
+
keys(data, STYLE_KEYS)
|
|
193
|
+
const style: Style = {}
|
|
194
|
+
for (const key of ['gap', 'padding'] as const)
|
|
195
|
+
if (key in data) style[key] = number(data[key], key, 0, 4_096)
|
|
196
|
+
for (const key of ['width', 'height'] as const)
|
|
197
|
+
if (key in data) style[key] = data[key] === 'fill' ? 'fill' : number(data[key], key, 0, 16_384)
|
|
198
|
+
for (const key of ['min_width', 'max_width', 'min_height', 'max_height'] as const)
|
|
199
|
+
if (key in data) style[key] = number(data[key], key, 0, 16_384)
|
|
200
|
+
for (const [minimum, maximum] of [
|
|
201
|
+
[style.min_width, style.max_width],
|
|
202
|
+
[style.min_height, style.max_height],
|
|
203
|
+
])
|
|
204
|
+
if (minimum !== undefined && maximum !== undefined && minimum > maximum)
|
|
205
|
+
throw new ProtocolError('Minimum size must not exceed maximum size')
|
|
206
|
+
for (const key of ['background', 'color', 'border_color'] as const)
|
|
207
|
+
if (key in data) style[key] = integer(data[key], key, 0, 0xff_ffff)
|
|
208
|
+
if ('direction' in data)
|
|
209
|
+
style.direction = choice(data['direction'], 'direction', ['row', 'column'])
|
|
210
|
+
for (const key of ['flex_grow', 'flex_shrink'] as const)
|
|
211
|
+
if (key in data) style[key] = number(data[key], key, 0, 1_000)
|
|
212
|
+
if ('align_items' in data)
|
|
213
|
+
style.align_items = choice(data['align_items'], 'align_items', [
|
|
214
|
+
'start',
|
|
215
|
+
'center',
|
|
216
|
+
'end',
|
|
217
|
+
'stretch',
|
|
218
|
+
])
|
|
219
|
+
if ('justify_content' in data)
|
|
220
|
+
style.justify_content = choice(data['justify_content'], 'justify_content', [
|
|
221
|
+
'start',
|
|
222
|
+
'center',
|
|
223
|
+
'end',
|
|
224
|
+
'space_between',
|
|
225
|
+
'space_around',
|
|
226
|
+
'space_evenly',
|
|
227
|
+
])
|
|
228
|
+
if ('overflow' in data)
|
|
229
|
+
style.overflow = choice(data['overflow'], 'overflow', [
|
|
230
|
+
'visible',
|
|
231
|
+
'hidden',
|
|
232
|
+
'scroll_x',
|
|
233
|
+
'scroll_y',
|
|
234
|
+
'scroll',
|
|
235
|
+
])
|
|
236
|
+
if ('scroll_anchor' in data) {
|
|
237
|
+
style.scroll_anchor = choice(data['scroll_anchor'], 'scroll_anchor', ['start', 'end'])
|
|
238
|
+
if (style.overflow !== 'scroll_y' && style.overflow !== 'scroll')
|
|
239
|
+
throw new ProtocolError('scroll_anchor requires vertical scrolling overflow')
|
|
240
|
+
}
|
|
241
|
+
if ('font_size' in data) style.font_size = number(data['font_size'], 'font_size', 1, 512)
|
|
242
|
+
if ('border_width' in data)
|
|
243
|
+
style.border_width = number(data['border_width'], 'border_width', 0, 64)
|
|
244
|
+
if ('corner_radius' in data)
|
|
245
|
+
style.corner_radius = number(data['corner_radius'], 'corner_radius', 0, 4_096)
|
|
246
|
+
if ('opacity' in data) style.opacity = number(data['opacity'], 'opacity', 0, 1)
|
|
247
|
+
if ('font_weight' in data) {
|
|
248
|
+
const weight = integer(data['font_weight'], 'font_weight', 100, 900)
|
|
249
|
+
if (weight % 100 !== 0) throw new ProtocolError('font_weight must be a multiple of 100')
|
|
250
|
+
style.font_weight = weight as NonNullable<Style['font_weight']>
|
|
251
|
+
}
|
|
252
|
+
return style
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function accessibleLabel(element: Record<string, unknown>): AccessibleLabel {
|
|
256
|
+
return 'accessible_label' in element
|
|
257
|
+
? { accessible_label: string(element['accessible_label'], 'accessible_label') }
|
|
258
|
+
: {}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function boxAccessibility(element: Record<string, unknown>): BoxAccessibility {
|
|
262
|
+
const label = accessibleLabel(element)
|
|
263
|
+
if (!('accessible_role' in element)) {
|
|
264
|
+
for (const name of [
|
|
265
|
+
'accessible_label',
|
|
266
|
+
'accessible_set_size',
|
|
267
|
+
'accessible_position',
|
|
268
|
+
'accessible_selected',
|
|
269
|
+
])
|
|
270
|
+
if (name in element) throw new ProtocolError(name + ' requires accessible_role')
|
|
271
|
+
return {}
|
|
272
|
+
}
|
|
273
|
+
const role = choice(element['accessible_role'], 'accessible_role', ['list', 'list_item'])
|
|
274
|
+
if (role === 'list') {
|
|
275
|
+
for (const name of ['accessible_position', 'accessible_selected'])
|
|
276
|
+
if (name in element) throw new ProtocolError(name + ' is only accepted on a list_item')
|
|
277
|
+
return {
|
|
278
|
+
accessible_role: role,
|
|
279
|
+
...label,
|
|
280
|
+
...('accessible_set_size' in element
|
|
281
|
+
? {
|
|
282
|
+
accessible_set_size: integer(
|
|
283
|
+
element['accessible_set_size'],
|
|
284
|
+
'accessible_set_size',
|
|
285
|
+
0,
|
|
286
|
+
MAX_ACCESSIBLE_SET_SIZE
|
|
287
|
+
),
|
|
288
|
+
}
|
|
289
|
+
: {}),
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if ('accessible_set_size' in element)
|
|
293
|
+
throw new ProtocolError('accessible_set_size is only accepted on a list')
|
|
294
|
+
if ('accessible_selected' in element && typeof element['accessible_selected'] !== 'boolean')
|
|
295
|
+
throw new ProtocolError('accessible_selected must be a boolean')
|
|
296
|
+
return {
|
|
297
|
+
accessible_role: role,
|
|
298
|
+
...label,
|
|
299
|
+
...('accessible_position' in element
|
|
300
|
+
? {
|
|
301
|
+
accessible_position: integer(
|
|
302
|
+
element['accessible_position'],
|
|
303
|
+
'accessible_position',
|
|
304
|
+
1,
|
|
305
|
+
MAX_ACCESSIBLE_SET_SIZE
|
|
306
|
+
),
|
|
307
|
+
}
|
|
308
|
+
: {}),
|
|
309
|
+
...('accessible_selected' in element
|
|
310
|
+
? { accessible_selected: element['accessible_selected'] as boolean }
|
|
311
|
+
: {}),
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const ELEMENT_BOX_KEYS = fields(
|
|
316
|
+
'kind',
|
|
317
|
+
'accessible_role',
|
|
318
|
+
'accessible_label',
|
|
319
|
+
'accessible_set_size',
|
|
320
|
+
'accessible_position',
|
|
321
|
+
'accessible_selected',
|
|
322
|
+
'style'
|
|
323
|
+
)
|
|
324
|
+
const ELEMENT_TEXT_KEYS = fields('kind', 'text', 'accessible_live', 'style')
|
|
325
|
+
const ELEMENT_BUTTON_KEYS = fields('kind', 'text', 'disabled', 'style')
|
|
326
|
+
const ELEMENT_INPUT_KEYS = fields('kind', 'value', 'placeholder', 'disabled', 'read_only', 'style')
|
|
327
|
+
const ELEMENT_SPLIT_KEYS = fields('kind', 'ratio', 'min_size', 'accessible_label', 'style')
|
|
328
|
+
const ELEMENT_OVERLAY_KEYS = fields(
|
|
329
|
+
'kind',
|
|
330
|
+
'anchor',
|
|
331
|
+
'modal',
|
|
332
|
+
'surface',
|
|
333
|
+
'autofocus',
|
|
334
|
+
'navigation',
|
|
335
|
+
'accessible_label',
|
|
336
|
+
'style'
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Elements this module produced. Each is frozen with its style, so it is still
|
|
341
|
+
* exactly the normalized value that passed validation.
|
|
342
|
+
*/
|
|
343
|
+
const parsed = new WeakSet<Element>()
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Normalize and validate the entire property set, including JavaScript consumers.
|
|
347
|
+
*
|
|
348
|
+
* The result is frozen and returned unchanged when parsed again, so core's
|
|
349
|
+
* normalized elements cross the runtime boundary in O(1). Any other value,
|
|
350
|
+
* including an unfrozen copy of a parsed element, is validated in full.
|
|
351
|
+
*/
|
|
352
|
+
export function parseElement(value: unknown): Element {
|
|
353
|
+
if (parsed.has(value as Element)) return value as Element
|
|
354
|
+
const element = validElement(value)
|
|
355
|
+
Object.freeze(element.style)
|
|
356
|
+
parsed.add(Object.freeze(element))
|
|
357
|
+
return element
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function validElement(value: unknown): Element {
|
|
361
|
+
const element = object(value)
|
|
362
|
+
const style = 'style' in element ? parseStyle(element['style']) : {}
|
|
363
|
+
switch (element['kind']) {
|
|
364
|
+
case 'box':
|
|
365
|
+
keys(element, ELEMENT_BOX_KEYS)
|
|
366
|
+
return { kind: 'box', ...boxAccessibility(element), style }
|
|
367
|
+
case 'text':
|
|
368
|
+
keys(element, ELEMENT_TEXT_KEYS)
|
|
369
|
+
requireLeafStyle(style)
|
|
370
|
+
return {
|
|
371
|
+
kind: 'text',
|
|
372
|
+
text: string(element['text'], 'text'),
|
|
373
|
+
...('accessible_live' in element
|
|
374
|
+
? {
|
|
375
|
+
accessible_live: choice(element['accessible_live'], 'accessible_live', [
|
|
376
|
+
'polite',
|
|
377
|
+
'assertive',
|
|
378
|
+
]),
|
|
379
|
+
}
|
|
380
|
+
: {}),
|
|
381
|
+
style,
|
|
382
|
+
}
|
|
383
|
+
case 'button': {
|
|
384
|
+
keys(element, ELEMENT_BUTTON_KEYS)
|
|
385
|
+
requireLeafStyle(style)
|
|
386
|
+
if ('disabled' in element && typeof element['disabled'] !== 'boolean')
|
|
387
|
+
throw new ProtocolError('disabled must be a boolean')
|
|
388
|
+
return {
|
|
389
|
+
kind: 'button',
|
|
390
|
+
text: string(element['text'], 'text'),
|
|
391
|
+
disabled: element['disabled'] === true,
|
|
392
|
+
style,
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
case 'input': {
|
|
396
|
+
keys(element, ELEMENT_INPUT_KEYS)
|
|
397
|
+
requireLeafStyle(style)
|
|
398
|
+
for (const name of ['disabled', 'read_only'])
|
|
399
|
+
if (name in element && typeof element[name] !== 'boolean')
|
|
400
|
+
throw new ProtocolError(name + ' must be a boolean')
|
|
401
|
+
// Only true states are carried, so ordinary inputs keep their exact wire form.
|
|
402
|
+
return {
|
|
403
|
+
kind: 'input',
|
|
404
|
+
value: string(element['value'], 'value'),
|
|
405
|
+
placeholder: 'placeholder' in element ? string(element['placeholder'], 'placeholder') : '',
|
|
406
|
+
...(element['disabled'] === true ? { disabled: true } : {}),
|
|
407
|
+
...(element['read_only'] === true ? { read_only: true } : {}),
|
|
408
|
+
style,
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
case 'split': {
|
|
412
|
+
keys(element, ELEMENT_SPLIT_KEYS)
|
|
413
|
+
requireLeafStyle(style)
|
|
414
|
+
if (
|
|
415
|
+
style.gap !== undefined ||
|
|
416
|
+
style.align_items !== undefined ||
|
|
417
|
+
style.justify_content !== undefined
|
|
418
|
+
)
|
|
419
|
+
throw new ProtocolError('Split panes do not accept gap or alignment')
|
|
420
|
+
return {
|
|
421
|
+
kind: 'split',
|
|
422
|
+
ratio: 'ratio' in element ? number(element['ratio'], 'ratio', 0, 1) : 0.5,
|
|
423
|
+
min_size: 'min_size' in element ? number(element['min_size'], 'min_size', 0, 16_384) : 0,
|
|
424
|
+
...accessibleLabel(element),
|
|
425
|
+
style,
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
case 'overlay': {
|
|
429
|
+
keys(element, ELEMENT_OVERLAY_KEYS)
|
|
430
|
+
const label = accessibleLabel(element)
|
|
431
|
+
const anchor =
|
|
432
|
+
'anchor' in element ? choice(element['anchor'], 'anchor', ['window', 'parent']) : 'window'
|
|
433
|
+
for (const flag of ['modal', 'autofocus'])
|
|
434
|
+
if (flag in element && typeof element[flag] !== 'boolean')
|
|
435
|
+
throw new ProtocolError(flag + ' must be a boolean')
|
|
436
|
+
const modal = element['modal'] === true
|
|
437
|
+
const keyboard: OverlayKeyboard = {
|
|
438
|
+
...(element['autofocus'] === true ? { autofocus: true } : {}),
|
|
439
|
+
...('navigation' in element &&
|
|
440
|
+
choice(element['navigation'], 'navigation', ['tab', 'menu']) === 'menu'
|
|
441
|
+
? { navigation: 'menu' as const }
|
|
442
|
+
: {}),
|
|
443
|
+
}
|
|
444
|
+
const surface =
|
|
445
|
+
'surface' in element ? choice(element['surface'], 'surface', ['layer', 'window']) : 'layer'
|
|
446
|
+
if (surface === 'window') {
|
|
447
|
+
if (anchor !== 'parent')
|
|
448
|
+
throw new ProtocolError('A popup window overlay must be anchored to its parent')
|
|
449
|
+
if (modal) throw new ProtocolError('A modal overlay must be anchored to the window')
|
|
450
|
+
if (keyboard.autofocus || keyboard.navigation)
|
|
451
|
+
throw new ProtocolError('A popup window overlay does not take keyboard focus')
|
|
452
|
+
const { width, height } = style
|
|
453
|
+
if (typeof width !== 'number' || width < 1 || typeof height !== 'number' || height < 1)
|
|
454
|
+
throw new ProtocolError('A popup window overlay needs a fixed width and height')
|
|
455
|
+
return {
|
|
456
|
+
kind: 'overlay',
|
|
457
|
+
anchor,
|
|
458
|
+
modal: false,
|
|
459
|
+
surface,
|
|
460
|
+
...label,
|
|
461
|
+
style: { ...style, width, height },
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (anchor === 'parent') {
|
|
465
|
+
if (modal) throw new ProtocolError('A modal overlay must be anchored to the window')
|
|
466
|
+
return { kind: 'overlay', anchor, modal: false, ...keyboard, ...label, style }
|
|
467
|
+
}
|
|
468
|
+
return { kind: 'overlay', anchor, modal, ...keyboard, ...label, style }
|
|
469
|
+
}
|
|
470
|
+
default:
|
|
471
|
+
throw new ProtocolError('Unknown native element kind')
|
|
472
|
+
}
|
|
473
|
+
}
|