@rak200/ui 0.2.16 → 0.2.18

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/dist/menu.js ADDED
@@ -0,0 +1,597 @@
1
+ import { LitElement, css, html } from 'lit';
2
+ import { place } from './placement.js';
3
+ import { reference } from './reference.js';
4
+ /**
5
+ * The panel's id, and it needs no counter behind it.
6
+ *
7
+ * `src/tooltip.ts` and `src/field.ts` both number theirs, because the elements they point
8
+ * at are the host's and share the document's tree scope. Both ends of this reference are
9
+ * rendered here, so the id is scoped to one shadow root and two menus on a page cannot
10
+ * collide — which is the same property that lets `aria-controls` resolve at all.
11
+ */
12
+ const panelId = 'menu';
13
+ /** Which way the panel went, for a host that wants to select on it. */
14
+ const side = 'data-side';
15
+ /**
16
+ * How long a typeahead buffer survives a pause, in milliseconds.
17
+ *
18
+ * Not a token, for the reason the dwell in `src/toast.ts` is not one: every
19
+ * `--ui-duration-*` name collapses under reduced motion, and this is a reader's typing
20
+ * rhythm rather than a movement. Half a second is the interval the platform's own
21
+ * `<select>` typeahead uses.
22
+ */
23
+ const rhythm = 500;
24
+ /** The namespace an svg has to be created in, which `document.createElement` does not use. */
25
+ const svgNS = 'http://www.w3.org/2000/svg';
26
+ /** One of the component's slots — named, or the default one when the name is empty. */
27
+ function slot(name = '') {
28
+ return Object.assign(document.createElement('slot'), { name });
29
+ }
30
+ /**
31
+ * The chevron, drawn here rather than imported from `src/icons/`.
32
+ *
33
+ * A component may not require a host to import a glyph module in order for its own control
34
+ * to have a mark — the argument `src/toast.ts` makes beside its dismiss cross. It is the
35
+ * adopted set's geometry on the adopted set's grid, so it reads as one of them, and it
36
+ * turns over when the menu opens because a caret that never moves says nothing.
37
+ */
38
+ function caret() {
39
+ const mark = document.createElementNS(svgNS, 'svg');
40
+ const stroke = document.createElementNS(svgNS, 'path');
41
+ stroke.setAttribute('d', 'm6 9 6 6 6-6');
42
+ mark.setAttribute('viewBox', '0 0 24 24');
43
+ mark.setAttribute('fill', 'none');
44
+ mark.setAttribute('stroke', 'currentColor');
45
+ mark.setAttribute('stroke-linecap', 'round');
46
+ mark.setAttribute('stroke-linejoin', 'round');
47
+ // The name is the trigger's own text; a mark repeating it is a second thing to hear.
48
+ mark.setAttribute('aria-hidden', 'true');
49
+ mark.append(stroke);
50
+ return mark;
51
+ }
52
+ /**
53
+ * A menu button: one trigger, one menu, and the ARIA relationship between them.
54
+ *
55
+ * **The trigger is rendered here rather than slotted, which is the opposite of every other
56
+ * component in this kit** — and the reason is the one issue #23 gave: the relationship
57
+ * cannot be miswired by a consumer if the consumer never writes it. `aria-haspopup`,
58
+ * `aria-expanded` and `aria-controls` all live on an element this file owns, pointing at
59
+ * another element this file owns, inside one tree scope where an IDREF resolves.
60
+ *
61
+ * **The items stay yours.** They are native `<button>`s and `<a href>`s in your tree, so
62
+ * activation, the accessible name and the disabled state are the platform's; this file
63
+ * writes `role="menuitem"` and a tabindex on them and nothing else. Containment is what
64
+ * crosses the boundary here — a slotted item really is inside the `role="menu"` panel,
65
+ * which `ARCHITECTURE.md` records under `<ui-toaster>` and which a reference could not do.
66
+ *
67
+ * ## What the platform does, and what is left
68
+ *
69
+ * The panel is `popover="auto"`, and that is most of the pattern. Measured, in this
70
+ * component's own arrangement: it takes the top layer, it light-dismisses on a click
71
+ * elsewhere, and **Escape closes it** — which is where `<ui-tooltip>` had to write a key
72
+ * handler, because a `manual` popover does not. An `auto` popover also closes any other
73
+ * open one, which is right for a menu and was exactly wrong for a tip.
74
+ *
75
+ * What the platform does **not** give, measured in the same run:
76
+ *
77
+ * - **Focus does not come back.** With the trigger in this shadow root and the focus on a
78
+ * slotted item, Escape leaves the focus on `<body>` rather than returning it — the same
79
+ * arrangement in one flat tree does return it. So the return trip is written here, and
80
+ * only where the reader did not choose to go elsewhere: a click outside the menu has
81
+ * already put the focus where it belongs.
82
+ * - **`aria-expanded`, `role` and the tabindex.** An invoker gets no attribute of its own.
83
+ * - **The single tab stop, and the keys inside it.** Every item is a real button, so
84
+ * without a tabindex the menu is a row of tab stops rather than an APG menu.
85
+ *
86
+ * ## Why this did not bring Zag either
87
+ *
88
+ * RFC 0016 adopted Zag for behaviour, `ROADMAP.md` named this component as the nearest
89
+ * candidate, and Zag ships a menu machine — so this is a refusal rather than an absence of
90
+ * an option, and it is the fourth. `ARCHITECTURE.md` carries it; the short form is that the
91
+ * test asks where the **accessible behaviour** is the expensive part, and the measurement
92
+ * above splits it: the layer, the dismissal, Escape and the ordering against other open
93
+ * layers are all the platform's. What is left is a roving tabindex and four keys, which the
94
+ * `<ui-radio-group>` refusal already named as not the trigger.
95
+ *
96
+ * The second reason is the issue's own: Zag's menu positions through `@zag-js/popper`,
97
+ * which is Floating UI. Adopting it would put a second placement in a package whose first
98
+ * one is measured, documented and shared — and *two answers to one problem is one too
99
+ * many* is the issue's sentence, not this file's.
100
+ *
101
+ * @example
102
+ * ```html
103
+ * <ui-menu>
104
+ * <span slot="trigger">Actions</span>
105
+ * <button type="button">Rename</button>
106
+ * <hr />
107
+ * <button type="button" disabled>Delete</button>
108
+ * </ui-menu>
109
+ * ```
110
+ */
111
+ export class UiMenu extends LitElement {
112
+ static { this.styles = css `
113
+ :host {
114
+ display: inline-block;
115
+ }
116
+
117
+ /* The trigger is a secondary ui-button, written out rather than shared — the same
118
+ trade src/select.ts made against ui-input's box, and answered the same way: the
119
+ duplication is real, and tests/menu.test.ts measures this box against that one
120
+ rather than trusting the two to stay in step. Sharing would mean either slotting
121
+ a ui-button, which puts the ARIA relationship back in the consumer's hands, or a
122
+ mixin, which is a second inheritance axis for one rule set. */
123
+ button {
124
+ display: inline-flex;
125
+ align-items: center;
126
+ gap: calc(${reference('--ui-space')} / 2);
127
+ font: inherit;
128
+ font-family: ${reference('--ui-font')};
129
+ border: 1px solid currentcolor;
130
+ border-radius: ${reference('--ui-radius')};
131
+ padding: ${reference('--ui-space')} calc(${reference('--ui-space')} * 2);
132
+ background: ${reference('--ui-color-surface')};
133
+ color: ${reference('--ui-color-text')};
134
+ cursor: pointer;
135
+ -webkit-tap-highlight-color: transparent;
136
+ transition: background-color ${reference('--ui-duration-state')}
137
+ ${reference('--ui-easing-state')};
138
+ }
139
+
140
+ button:hover {
141
+ background: ${reference('--ui-color-hover')};
142
+ }
143
+
144
+ /* Zero rather than a second token, for the reason src/button.ts states beside its
145
+ own: a press is over in about 100ms, so an entering transition of 150ms would
146
+ land after the finger has left. */
147
+ button:active {
148
+ background: ${reference('--ui-color-pressed')};
149
+ transition-duration: 0s;
150
+ }
151
+
152
+ button:focus-visible {
153
+ outline: 2px solid ${reference('--ui-color-focus')};
154
+ outline-offset: 2px;
155
+ }
156
+
157
+ /* One dimension, not two: an svg carrying a viewBox is a replaced element with an
158
+ intrinsic ratio, so a square glyph given a width takes the same height from it.
159
+ src/toast.ts carries the measurement that made this a rule. */
160
+ svg {
161
+ flex: none;
162
+ inline-size: ${reference('--ui-icon-size')};
163
+ stroke-width: ${reference('--ui-icon-stroke')};
164
+ color: ${reference('--ui-color-text-muted')};
165
+ transition: rotate ${reference('--ui-duration-state')} ${reference('--ui-easing-state')};
166
+ }
167
+
168
+ :host([open]) svg {
169
+ rotate: 180deg;
170
+ }
171
+
172
+ /* The panel's own box. position: fixed is what the popover already is; it is
173
+ restated because the script writes the two insets against the viewport, and a
174
+ reader of those lines should not have to know which stylesheet made them mean
175
+ that. */
176
+ [popover] {
177
+ position: fixed;
178
+ margin: 0;
179
+ inset: auto;
180
+ box-sizing: border-box;
181
+ min-inline-size: max-content;
182
+ max-inline-size: calc(${reference('--ui-space')} * 44);
183
+ padding: calc(${reference('--ui-space')} / 2);
184
+ border: 1px solid ${reference('--ui-color-border')};
185
+ border-radius: ${reference('--ui-radius')};
186
+ background: ${reference('--ui-color-surface')};
187
+ color: ${reference('--ui-color-text')};
188
+ font-family: ${reference('--ui-font')};
189
+ /* Lifted and bounded both, for the reason src/card.ts gives: a shadow is one
190
+ value in both schemes and does almost nothing on a dark page, where the
191
+ derived boundary is what separates the surface from what is under it. */
192
+ box-shadow: ${reference('--ui-elevation-raised')};
193
+ }
194
+
195
+ /* display is named under :popover-open and nowhere else, and that is the trap
196
+ src/dialog.ts fell into once: the user agent hides a closed popover with
197
+ display: none, and an author rule naming display unconditionally outranks it. */
198
+ [popover]:popover-open {
199
+ display: flex;
200
+ flex-direction: column;
201
+ }
202
+
203
+ /* The gap between the panel and its trigger, in CSS because it is a token and the
204
+ script has no way to resolve one: a custom property reads back as it was
205
+ written, so 0.5rem would arrive at a function that needs pixels. */
206
+ [popover][data-side='block-end'] {
207
+ translate: 0 calc(${reference('--ui-space')} / 2);
208
+ }
209
+
210
+ [popover][data-side='block-start'] {
211
+ translate: 0 calc(${reference('--ui-space')} / -2);
212
+ }
213
+
214
+ /* An item is the host's own control, reached through ::slotted — so what is set
215
+ here is the only thing this component decides about it: that it fills the row,
216
+ reads left to right, and answers a pointer. */
217
+ ::slotted(button),
218
+ ::slotted(a) {
219
+ display: block;
220
+ inline-size: 100%;
221
+ box-sizing: border-box;
222
+ text-align: start;
223
+ font: inherit;
224
+ border: none;
225
+ border-radius: ${reference('--ui-radius')};
226
+ padding: calc(${reference('--ui-space')} / 2) ${reference('--ui-space')};
227
+ background: none;
228
+ color: inherit;
229
+ text-decoration: none;
230
+ cursor: pointer;
231
+ transition: background-color ${reference('--ui-duration-state')}
232
+ ${reference('--ui-easing-state')};
233
+ }
234
+
235
+ /* The :not() guards are measured rather than assumed, the same way src/button.ts
236
+ measured its own: a disabled control still matches :hover, so without them an
237
+ item that cannot be chosen would light up under a pointer. */
238
+ ::slotted(button:not(:disabled):hover),
239
+ ::slotted(a:not([aria-disabled='true']):hover) {
240
+ background: ${reference('--ui-color-hover')};
241
+ }
242
+
243
+ ::slotted(button:focus-visible),
244
+ ::slotted(a:focus-visible) {
245
+ outline: 2px solid ${reference('--ui-color-focus')};
246
+ outline-offset: -2px;
247
+ }
248
+
249
+ ::slotted(button:disabled),
250
+ ::slotted([aria-disabled='true']) {
251
+ color: ${reference('--ui-color-text-muted')};
252
+ cursor: not-allowed;
253
+ }
254
+
255
+ /* An hr already claims role="separator" — measured — so the platform owns what it
256
+ means and this owns only what it looks like. */
257
+ ::slotted(hr) {
258
+ border: none;
259
+ border-block-start: 1px solid ${reference('--ui-color-border')};
260
+ margin: calc(${reference('--ui-space')} / 2) 0;
261
+ }
262
+ `; }
263
+ static { this.properties = {
264
+ open: { type: Boolean, reflect: true },
265
+ }; }
266
+ /** The button this component owns, so the relationship below cannot be miswired. */
267
+ #trigger;
268
+ /** The menu itself, in the same tree scope as the trigger that names it. */
269
+ #panel;
270
+ /** What a pending typeahead has collected, and the clock that forgets it. */
271
+ #typed;
272
+ #forgetting;
273
+ /** Whether the next open should land on the last item rather than the first. */
274
+ #fromEnd;
275
+ /**
276
+ * Whether the close should hand the focus back.
277
+ *
278
+ * Set only by the two ways out that leave the reader inside the menu — Escape, and
279
+ * choosing an item. A click elsewhere is the third way out and deliberately does not
280
+ * set it: the reader has already said where they want to be, and moving them to the
281
+ * trigger would take it away from them.
282
+ */
283
+ #returning;
284
+ /** Re-places the panel under anything that moves the trigger while it is open. */
285
+ #reflow;
286
+ /** What is watching while the menu is open, dropped in one call when it closes. */
287
+ #watching;
288
+ constructor() {
289
+ super();
290
+ /**
291
+ * Whether the menu is showing.
292
+ *
293
+ * Reflected, so a host stylesheet can select on it — and so the caret above can turn
294
+ * without this file writing a class. Settable: the platform's own state is the source
295
+ * of truth and {@link UiMenu.updated} reconciles the two, which is the shape
296
+ * `src/dialog.ts` uses and for the same reason.
297
+ *
298
+ * A plain field rather than the `accessor` keyword, for the reason `src/button.ts`
299
+ * states beside its own.
300
+ */
301
+ this.open = false;
302
+ /** The button this component owns, so the relationship below cannot be miswired. */
303
+ this.#trigger = document.createElement('button');
304
+ /** The menu itself, in the same tree scope as the trigger that names it. */
305
+ this.#panel = document.createElement('div');
306
+ /** What a pending typeahead has collected, and the clock that forgets it. */
307
+ this.#typed = '';
308
+ this.#forgetting = 0;
309
+ /** Whether the next open should land on the last item rather than the first. */
310
+ this.#fromEnd = false;
311
+ /**
312
+ * Whether the close should hand the focus back.
313
+ *
314
+ * Set only by the two ways out that leave the reader inside the menu — Escape, and
315
+ * choosing an item. A click elsewhere is the third way out and deliberately does not
316
+ * set it: the reader has already said where they want to be, and moving them to the
317
+ * trigger would take it away from them.
318
+ */
319
+ this.#returning = false;
320
+ /** Re-places the panel under anything that moves the trigger while it is open. */
321
+ this.#reflow = () => {
322
+ this.#place();
323
+ };
324
+ /** What is watching while the menu is open, dropped in one call when it closes. */
325
+ this.#watching = new AbortController();
326
+ /**
327
+ * Makes each item a menu item, and makes the menu one tab stop.
328
+ *
329
+ * On `slotchange` as well as at first render, because a framework re-render replaces
330
+ * the element rather than mutating it — and an item that is only wired the first time
331
+ * is one that silently stops being reachable.
332
+ *
333
+ * `tabindex="-1"` on every item, not a roving `0`: a popup menu is only reachable while
334
+ * it is open, so the tab stop is the trigger and the items are moved between by the
335
+ * keys below. That is also what makes Tab leave rather than walk the menu, which is
336
+ * what the APG pattern asks for and what the platform does with real buttons otherwise.
337
+ */
338
+ this.#wire = () => {
339
+ for (const item of this.#items()) {
340
+ item.setAttribute('role', 'menuitem');
341
+ item.setAttribute('tabindex', '-1');
342
+ }
343
+ };
344
+ /** Opens onto an end of the list, which is the one thing a click cannot ask for. */
345
+ this.#onTriggerKey = (event) => {
346
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
347
+ return;
348
+ }
349
+ // Or the page scrolls under the menu that just opened.
350
+ event.preventDefault();
351
+ this.#fromEnd = event.key === 'ArrowUp';
352
+ this.open = true;
353
+ };
354
+ this.#onMenuKey = (event) => {
355
+ switch (event.key) {
356
+ case 'ArrowDown':
357
+ this.#step(1);
358
+ break;
359
+ case 'ArrowUp':
360
+ this.#step(-1);
361
+ break;
362
+ case 'Home':
363
+ this.#edge(false);
364
+ break;
365
+ case 'End':
366
+ this.#edge(true);
367
+ break;
368
+ case 'Escape':
369
+ // The platform closes it; this only says where the focus should land.
370
+ this.#returning = true;
371
+ return;
372
+ case 'Tab':
373
+ // The focus is already leaving and the default is what carries it there.
374
+ // Closing without preventing is the whole of the APG rule.
375
+ this.open = false;
376
+ return;
377
+ default:
378
+ if (event.key.length !== 1) {
379
+ return;
380
+ }
381
+ this.#type(event.key);
382
+ break;
383
+ }
384
+ event.preventDefault();
385
+ };
386
+ /** Choosing an item closes the menu, which is what makes it a menu rather than a panel. */
387
+ this.#onChoose = (event) => {
388
+ const chosen = event.target;
389
+ if (!(chosen instanceof HTMLElement) || !this.#enabled().includes(chosen)) {
390
+ return;
391
+ }
392
+ this.#returning = true;
393
+ this.open = false;
394
+ };
395
+ /**
396
+ * Follows the platform's own state, whichever way it moved.
397
+ *
398
+ * The invoker, a click elsewhere and Escape all close the panel without going through
399
+ * {@link UiMenu.open}, so this is where the property learns what happened rather than
400
+ * the other way round.
401
+ */
402
+ this.#onToggle = (event) => {
403
+ this.open = event.newState === 'open';
404
+ if (!this.open) {
405
+ this.#watching.abort();
406
+ this.#give();
407
+ return;
408
+ }
409
+ this.#place();
410
+ this.#edge(this.#fromEnd);
411
+ this.#fromEnd = false;
412
+ this.#watching = new AbortController();
413
+ const watched = { signal: this.#watching.signal };
414
+ // Capturing, because the thing that scrolls is rarely the window: a scroll inside
415
+ // any ancestor moves the trigger and no event reaches the window from it.
416
+ window.addEventListener('scroll', this.#reflow, { capture: true, ...watched });
417
+ window.addEventListener('resize', this.#reflow, watched);
418
+ };
419
+ const trigger = this.#trigger;
420
+ const panel = this.#panel;
421
+ // No `type` is set here, and that is measured rather than forgotten: a `<button>`
422
+ // in a shadow root has no form owner — `.form` is null and a click submits nothing —
423
+ // so `type="button"` would defend against a submission the shadow boundary already
424
+ // makes impossible. The platform is the guarantee; a line restating it is one more
425
+ // thing that can rot.
426
+ trigger.setAttribute('part', 'trigger');
427
+ trigger.setAttribute('aria-haspopup', 'menu');
428
+ trigger.setAttribute('aria-controls', panelId);
429
+ // The platform's own invoker, rather than a click handler that opens it. A click on
430
+ // the trigger of an open `auto` popover light-dismisses it *and* reaches the
431
+ // handler, so a hand-rolled toggle closes and reopens in one click — the platform's
432
+ // invoker logic is written for exactly that and this file should not have a second
433
+ // copy of it.
434
+ trigger.setAttribute('popovertarget', panelId);
435
+ trigger.addEventListener('keydown', this.#onTriggerKey);
436
+ trigger.append(slot('trigger'), caret());
437
+ panel.id = panelId;
438
+ panel.setAttribute('part', 'menu');
439
+ panel.setAttribute('role', 'menu');
440
+ panel.setAttribute('popover', 'auto');
441
+ panel.addEventListener('toggle', this.#onToggle);
442
+ panel.addEventListener('keydown', this.#onMenuKey);
443
+ panel.addEventListener('click', this.#onChoose);
444
+ const items = slot();
445
+ items.addEventListener('slotchange', this.#wire);
446
+ panel.append(items);
447
+ }
448
+ disconnectedCallback() {
449
+ // Removed while open, the panel takes the window listeners with it. A menu a
450
+ // framework unmounted would otherwise keep re-placing an element that is no longer
451
+ // in a document.
452
+ this.#watching.abort();
453
+ super.disconnectedCallback();
454
+ }
455
+ updated() {
456
+ this.#trigger.setAttribute('aria-expanded', String(this.open));
457
+ // Compared against the platform's own state rather than against Lit's changed
458
+ // properties, and it has to be: on the first update Lit reports every initialised
459
+ // property as changed, so a menu that has never been open would run the closing
460
+ // path. It is also what makes the invoker's own toggle a no-op here rather than a
461
+ // second `showPopover()` on a panel that is already open.
462
+ //
463
+ // Stryker disable next-line ConditionalExpression,BlockStatement: measured in the
464
+ // engine the suite runs — a redundant `showPopover()` or `hidePopover()` RETURNS
465
+ // rather than throwing, so with this guard gone nothing a test here can reach
466
+ // behaves differently. It stays anyway, because the engine is not the contract:
467
+ // the invoker sets `open` to what the platform has ALREADY done, so every click
468
+ // would make one redundant call, and an engine that refuses one would throw on the
469
+ // most ordinary interaction this component has. Marked rather than deleted so the
470
+ // next reader finds the measurement instead of taking it again.
471
+ if (this.open === this.#panel.matches(':popover-open')) {
472
+ return;
473
+ }
474
+ if (this.open) {
475
+ this.#panel.showPopover();
476
+ }
477
+ else {
478
+ this.#panel.hidePopover();
479
+ }
480
+ }
481
+ render() {
482
+ return html `${this.#trigger}${this.#panel}`;
483
+ }
484
+ /** The items, which are the host's own controls rather than anything rendered here. */
485
+ #items() {
486
+ return [
487
+ ...this.querySelectorAll(':scope > button:not([slot]), :scope > a[href]:not([slot])'),
488
+ ];
489
+ }
490
+ /** The ones a reader can actually land on, which is what every key below moves over. */
491
+ #enabled() {
492
+ return this.#items().filter((item) => !item.matches('[disabled], [aria-disabled="true"]'));
493
+ }
494
+ /**
495
+ * Makes each item a menu item, and makes the menu one tab stop.
496
+ *
497
+ * On `slotchange` as well as at first render, because a framework re-render replaces
498
+ * the element rather than mutating it — and an item that is only wired the first time
499
+ * is one that silently stops being reachable.
500
+ *
501
+ * `tabindex="-1"` on every item, not a roving `0`: a popup menu is only reachable while
502
+ * it is open, so the tab stop is the trigger and the items are moved between by the
503
+ * keys below. That is also what makes Tab leave rather than walk the menu, which is
504
+ * what the APG pattern asks for and what the platform does with real buttons otherwise.
505
+ */
506
+ #wire;
507
+ /** Opens onto an end of the list, which is the one thing a click cannot ask for. */
508
+ #onTriggerKey;
509
+ #onMenuKey;
510
+ /** Choosing an item closes the menu, which is what makes it a menu rather than a panel. */
511
+ #onChoose;
512
+ /**
513
+ * Follows the platform's own state, whichever way it moved.
514
+ *
515
+ * The invoker, a click elsewhere and Escape all close the panel without going through
516
+ * {@link UiMenu.open}, so this is where the property learns what happened rather than
517
+ * the other way round.
518
+ */
519
+ #onToggle;
520
+ /**
521
+ * Hands the focus back to the trigger, where the platform does not.
522
+ *
523
+ * Measured, in this component's own arrangement: with the trigger in this shadow root
524
+ * and the focus on a slotted item, closing leaves the focus on `<body>` — the same
525
+ * markup in one flat tree returns it to the invoker. So this is a gap rather than a
526
+ * duplication of something the platform already did.
527
+ */
528
+ #give() {
529
+ if (!this.#returning) {
530
+ return;
531
+ }
532
+ this.#returning = false;
533
+ this.#trigger.focus();
534
+ }
535
+ /** Moves by one, wrapping at either end, which is what the APG pattern asks for. */
536
+ #step(offset) {
537
+ const items = this.#enabled();
538
+ const at = items.findIndex((item) => item === document.activeElement);
539
+ // A menu with no enabled item leaves the modulo undefined and the lookup empty,
540
+ // which is the same nothing a guard would return — so there is no guard.
541
+ items[(at + offset + items.length) % items.length]?.focus();
542
+ }
543
+ /** Jumps to one end, which is Home and End and also where an opening menu lands. */
544
+ #edge(last) {
545
+ const items = this.#enabled();
546
+ (last ? items[items.length - 1] : items[0])?.focus();
547
+ }
548
+ /**
549
+ * Moves to the next item whose label starts with what has been typed.
550
+ *
551
+ * The search starts *after* the current item and wraps, which is what makes a repeated
552
+ * letter cycle through the items that share it — and what lets a growing buffer still
553
+ * find the item it is already on, by coming back round to it.
554
+ */
555
+ #type(character) {
556
+ window.clearTimeout(this.#forgetting);
557
+ this.#typed += character.toLowerCase();
558
+ this.#forgetting = window.setTimeout(() => {
559
+ this.#typed = '';
560
+ }, rhythm);
561
+ const items = this.#enabled();
562
+ const at = items.findIndex((item) => item === document.activeElement);
563
+ const after = items.slice(at + 1);
564
+ // Stryker disable next-line MethodExpression: dropping the arguments makes this the
565
+ // whole list, and the search cannot tell the difference — `order` would then be the
566
+ // original array followed by entries the `find` has already rejected, so it stops at
567
+ // the same item or at none. Provably equivalent, which is the only reason to ignore
568
+ // one; the argument itself is NOT ignored, and `tests/menu.test.ts` kills a mutant on
569
+ // it by searching backwards past the item under the focus.
570
+ const wrapped = items.slice(0, at + 1);
571
+ const order = [...after, ...wrapped];
572
+ order
573
+ .find((item) => item.textContent.trim().toLowerCase().startsWith(this.#typed))
574
+ ?.focus();
575
+ }
576
+ /**
577
+ * Hangs the panel from its trigger, above it when there is no room below, and inside
578
+ * the viewport either way.
579
+ *
580
+ * The arithmetic is `src/placement.ts`, which `<ui-tooltip>` reads as well — the issue
581
+ * behind this component asked for one answer rather than two, and what differs between
582
+ * the two callers is a preferred side and an alignment.
583
+ */
584
+ #place() {
585
+ const root = document.documentElement;
586
+ const placement = place(this.#trigger.getBoundingClientRect(), this.#panel.getBoundingClientRect(), { width: root.clientWidth, height: root.clientHeight }, 'block-end', 'inline-start');
587
+ this.#panel.setAttribute(side, placement.side);
588
+ this.#panel.style.insetBlockStart = `${String(placement.blockStart)}px`;
589
+ this.#panel.style.insetInlineStart = `${String(placement.inlineStart)}px`;
590
+ }
591
+ }
592
+ // Stryker disable next-line StringLiteral: the registration runs once, at import, inside
593
+ // the warm process Stryker switches mutants in — so by the time a mutant on this line is
594
+ // active the element is already defined under the original name, and no test can observe
595
+ // the change. Outside the runner's reach, not an equivalent mutant.
596
+ customElements.define('ui-menu', UiMenu);
597
+ //# sourceMappingURL=menu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu.js","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAuB,MAAM,KAAK,CAAC;AACjE,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,OAAO,GAAG,MAAM,CAAC;AAEvB,uEAAuE;AACvE,MAAM,IAAI,GAAG,WAAW,CAAC;AAEzB;;;;;;;GAOG;AACH,MAAM,MAAM,GAAG,GAAG,CAAC;AAEnB,8FAA8F;AAC9F,MAAM,KAAK,GAAG,4BAA4B,CAAC;AAE3C,uFAAuF;AACvF,SAAS,IAAI,CAAC,IAAI,GAAG,EAAE;IACnB,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,KAAK;IACV,MAAM,IAAI,GAAG,QAAQ,CAAC,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,QAAQ,CAAC,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAEvD,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IACzC,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IAC1C,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;IAC5C,IAAI,CAAC,YAAY,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;IAC7C,IAAI,CAAC,YAAY,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;IAC9C,qFAAqF;IACrF,IAAI,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAEpB,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,MAAM,OAAO,MAAO,SAAQ,UAAU;aACT,WAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;wBAcjB,SAAS,CAAC,YAAY,CAAC;;2BAEpB,SAAS,CAAC,WAAW,CAAC;;6BAEpB,SAAS,CAAC,aAAa,CAAC;uBAC9B,SAAS,CAAC,YAAY,CAAC,SAAS,SAAS,CAAC,YAAY,CAAC;0BACpD,SAAS,CAAC,oBAAoB,CAAC;qBACpC,SAAS,CAAC,iBAAiB,CAAC;;;2CAGN,SAAS,CAAC,qBAAqB,CAAC;kBACzD,SAAS,CAAC,mBAAmB,CAAC;;;;0BAItB,SAAS,CAAC,kBAAkB,CAAC;;;;;;;0BAO7B,SAAS,CAAC,oBAAoB,CAAC;;;;;iCAKxB,SAAS,CAAC,kBAAkB,CAAC;;;;;;;;;2BASnC,SAAS,CAAC,gBAAgB,CAAC;4BAC1B,SAAS,CAAC,kBAAkB,CAAC;qBACpC,SAAS,CAAC,uBAAuB,CAAC;iCACtB,SAAS,CAAC,qBAAqB,CAAC,IAAI,SAAS,CAAC,mBAAmB,CAAC;;;;;;;;;;;;;;;;;oCAiB/D,SAAS,CAAC,YAAY,CAAC;4BAC/B,SAAS,CAAC,YAAY,CAAC;gCACnB,SAAS,CAAC,mBAAmB,CAAC;6BACjC,SAAS,CAAC,aAAa,CAAC;0BAC3B,SAAS,CAAC,oBAAoB,CAAC;qBACpC,SAAS,CAAC,iBAAiB,CAAC;2BACtB,SAAS,CAAC,WAAW,CAAC;;;;0BAIvB,SAAS,CAAC,uBAAuB,CAAC;;;;;;;;;;;;;;;gCAe5B,SAAS,CAAC,YAAY,CAAC;;;;gCAIvB,SAAS,CAAC,YAAY,CAAC;;;;;;;;;;;;;;6BAc1B,SAAS,CAAC,aAAa,CAAC;4BACzB,SAAS,CAAC,YAAY,CAAC,SAAS,SAAS,CAAC,YAAY,CAAC;;;;;2CAKxC,SAAS,CAAC,qBAAqB,CAAC;kBACzD,SAAS,CAAC,mBAAmB,CAAC;;;;;;;;0BAQtB,SAAS,CAAC,kBAAkB,CAAC;;;;;iCAKtB,SAAS,CAAC,kBAAkB,CAAC;;;;;;qBAMzC,SAAS,CAAC,uBAAuB,CAAC;;;;;;;;4CAQX,SAAS,CAAC,mBAAmB,CAAC;2BAC/C,SAAS,CAAC,YAAY,CAAC;;KAE7C,AAtJ8B,CAsJ7B;aAEuB,eAAU,GAAG;QAClC,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE;KACzC,AAFkC,CAEjC;IAeF,oFAAoF;IAC3E,QAAQ,CAAoC;IAErD,4EAA4E;IACnE,MAAM,CAAiC;IAEhD,6EAA6E;IAC7E,MAAM,CAAM;IAEZ,WAAW,CAAK;IAEhB,gFAAgF;IAChF,QAAQ,CAAS;IAEjB;;;;;;;OAOG;IACH,UAAU,CAAS;IAEnB,kFAAkF;IACzE,OAAO,CAEd;IAEF,mFAAmF;IACnF,SAAS,CAAyB;IAElC;QACI,KAAK,EAAE,CAAC;QA9CZ;;;;;;;;;;WAUG;QACH,SAAI,GAAG,KAAK,CAAC;QAEb,oFAAoF;QAC3E,aAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAErD,4EAA4E;QACnE,WAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAEhD,6EAA6E;QAC7E,WAAM,GAAG,EAAE,CAAC;QAEZ,gBAAW,GAAG,CAAC,CAAC;QAEhB,gFAAgF;QAChF,aAAQ,GAAG,KAAK,CAAC;QAEjB;;;;;;;WAOG;QACH,eAAU,GAAG,KAAK,CAAC;QAEnB,kFAAkF;QACzE,YAAO,GAAG,GAAS,EAAE;YAC1B,IAAI,CAAC,MAAM,EAAE,CAAC;QAClB,CAAC,CAAC;QAEF,mFAAmF;QACnF,cAAS,GAAG,IAAI,eAAe,EAAE,CAAC;QA6FlC;;;;;;;;;;;WAWG;QACM,UAAK,GAAG,GAAS,EAAE;YACxB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;gBAC/B,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;gBACtC,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YACxC,CAAC;QACL,CAAC,CAAC;QAEF,oFAAoF;QAC3E,kBAAa,GAAG,CAAC,KAAoB,EAAQ,EAAE;YACpD,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;gBACvD,OAAO;YACX,CAAC;YAED,uDAAuD;YACvD,KAAK,CAAC,cAAc,EAAE,CAAC;YAEvB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,GAAG,KAAK,SAAS,CAAC;YACxC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACrB,CAAC,CAAC;QAEO,eAAU,GAAG,CAAC,KAAoB,EAAQ,EAAE;YACjD,QAAQ,KAAK,CAAC,GAAG,EAAE,CAAC;gBAChB,KAAK,WAAW;oBACZ,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;oBACd,MAAM;gBACV,KAAK,SAAS;oBACV,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;oBACf,MAAM;gBACV,KAAK,MAAM;oBACP,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;oBAClB,MAAM;gBACV,KAAK,KAAK;oBACN,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBACjB,MAAM;gBACV,KAAK,QAAQ;oBACT,sEAAsE;oBACtE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;oBACvB,OAAO;gBACX,KAAK,KAAK;oBACN,yEAAyE;oBACzE,2DAA2D;oBAC3D,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;oBAClB,OAAO;gBACX;oBACI,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;wBACzB,OAAO;oBACX,CAAC;oBAED,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;oBACtB,MAAM;YACd,CAAC;YAED,KAAK,CAAC,cAAc,EAAE,CAAC;QAC3B,CAAC,CAAC;QAEF,2FAA2F;QAClF,cAAS,GAAG,CAAC,KAAiB,EAAQ,EAAE;YAC7C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;YAE5B,IAAI,CAAC,CAAC,MAAM,YAAY,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBACxE,OAAO;YACX,CAAC;YAED,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;QACtB,CAAC,CAAC;QAEF;;;;;;WAMG;QACM,cAAS,GAAG,CAAC,KAAkB,EAAQ,EAAE;YAC9C,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,QAAQ,KAAK,MAAM,CAAC;YAEtC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;gBACb,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;gBACvB,IAAI,CAAC,KAAK,EAAE,CAAC;gBAEb,OAAO;YACX,CAAC;YAED,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC1B,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;YAEtB,IAAI,CAAC,SAAS,GAAG,IAAI,eAAe,EAAE,CAAC;YAEvC,MAAM,OAAO,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;YAElD,kFAAkF;YAClF,0EAA0E;YAC1E,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;YAC/E,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7D,CAAC,CAAC;QApME,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;QAE1B,kFAAkF;QAClF,qFAAqF;QACrF,mFAAmF;QACnF,mFAAmF;QACnF,sBAAsB;QACtB,OAAO,CAAC,YAAY,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QACxC,OAAO,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;QAC9C,OAAO,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QAC/C,oFAAoF;QACpF,6EAA6E;QAC7E,oFAAoF;QACpF,mFAAmF;QACnF,cAAc;QACd,OAAO,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QAC/C,OAAO,CAAC,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QACxD,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QAEzC,KAAK,CAAC,EAAE,GAAG,OAAO,CAAC;QACnB,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACnC,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACnC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACtC,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACjD,KAAK,CAAC,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QACnD,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAEhD,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC;QACrB,KAAK,CAAC,gBAAgB,CAAC,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QACjD,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IAEQ,oBAAoB;QACzB,6EAA6E;QAC7E,mFAAmF;QACnF,iBAAiB;QACjB,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QAEvB,KAAK,CAAC,oBAAoB,EAAE,CAAC;IACjC,CAAC;IAEQ,OAAO;QACZ,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAE/D,8EAA8E;QAC9E,kFAAkF;QAClF,gFAAgF;QAChF,kFAAkF;QAClF,0DAA0D;QAC1D,EAAE;QACF,kFAAkF;QAClF,iFAAiF;QACjF,8EAA8E;QAC9E,gFAAgF;QAChF,gFAAgF;QAChF,mFAAmF;QACnF,kFAAkF;QAClF,gEAAgE;QAChE,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE,CAAC;YACrD,OAAO;QACX,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAC9B,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAC9B,CAAC;IACL,CAAC;IAEQ,MAAM;QACX,OAAO,IAAI,CAAA,GAAG,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;IAChD,CAAC;IAED,uFAAuF;IACvF,MAAM;QACF,OAAO;YACH,GAAG,IAAI,CAAC,gBAAgB,CACpB,2DAA2D,CAC9D;SACJ,CAAC;IACN,CAAC;IAED,wFAAwF;IACxF,QAAQ;QACJ,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC,CAAC;IAC/F,CAAC;IAED;;;;;;;;;;;OAWG;IACM,KAAK,CAKZ;IAEF,oFAAoF;IAC3E,aAAa,CAUpB;IAEO,UAAU,CAiCjB;IAEF,2FAA2F;IAClF,SAAS,CAShB;IAEF;;;;;;OAMG;IACM,SAAS,CAsBhB;IAEF;;;;;;;OAOG;IACH,KAAK;QACD,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACnB,OAAO;QACX,CAAC;QAED,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QACxB,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC;IAED,oFAAoF;IACpF,KAAK,CAAC,MAAc;QAChB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC9B,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,QAAQ,CAAC,aAAa,CAAC,CAAC;QAEtE,gFAAgF;QAChF,yEAAyE;QACzE,KAAK,CAAC,CAAC,EAAE,GAAG,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IAChE,CAAC;IAED,oFAAoF;IACpF,KAAK,CAAC,IAAa;QACf,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAE9B,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;IACzD,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,SAAiB;QACnB,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAEtC,IAAI,CAAC,MAAM,IAAI,SAAS,CAAC,WAAW,EAAE,CAAC;QACvC,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;YACtC,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;QACrB,CAAC,EAAE,MAAM,CAAC,CAAC;QAEX,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC9B,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,QAAQ,CAAC,aAAa,CAAC,CAAC;QACtE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QAClC,oFAAoF;QACpF,oFAAoF;QACpF,qFAAqF;QACrF,oFAAoF;QACpF,sFAAsF;QACtF,2DAA2D;QAC3D,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;QACvC,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC,CAAC;QAErC,KAAK;aACA,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAC9E,EAAE,KAAK,EAAE,CAAC;IAClB,CAAC;IAED;;;;;;;OAOG;IACH,MAAM;QACF,MAAM,IAAI,GAAG,QAAQ,CAAC,eAAe,CAAC;QACtC,MAAM,SAAS,GAAG,KAAK,CACnB,IAAI,CAAC,QAAQ,CAAC,qBAAqB,EAAE,EACrC,IAAI,CAAC,MAAM,CAAC,qBAAqB,EAAE,EACnC,EAAE,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,EACtD,WAAW,EACX,cAAc,CACjB,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QAC/C,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC;QACxE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC;IAC9E,CAAC;;AAGL,yFAAyF;AACzF,yFAAyF;AACzF,yFAAyF;AACzF,oEAAoE;AACpE,cAAc,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Where an overlay lands, as arithmetic rather than as a rendering.
3
+ *
4
+ * **One answer to one problem, which is what issue #23 asked for.** `<ui-tooltip>` settled
5
+ * how this package places a `popover` — by hand, over the top layer, with CSS anchor
6
+ * positioning measured as present in this engine and deliberately declined; its docblock
7
+ * carries the whole argument. `<ui-menu>` needs the same thing pointing the other way, so
8
+ * the answer moved here rather than being written twice. A component with two placements is
9
+ * a component that is wrong in one of them.
10
+ *
11
+ * **Taken as numbers rather than as elements, and that is the floors talking** — the same
12
+ * reason `reservedGutter` in `src/dialog.ts` is handed two widths instead of a window. A
13
+ * function of four boxes is decided here and checked directly, at the edges that a rendered
14
+ * page can only reach by luck: an anchor with exactly the tip's height above it, a box wider
15
+ * than the viewport, a menu at the last pixel of the screen. The callers keep no branch of
16
+ * their own.
17
+ *
18
+ * Internal — not re-exported from `src/index.ts`, because what a consumer positions is their
19
+ * own element, and this is how two of ours find each other.
20
+ */
21
+ /** Which way an overlay went, which is also what a component writes for a host to select on. */
22
+ export type Side = 'block-start' | 'block-end';
23
+ /** How the overlay lines up with its anchor along the inline axis. */
24
+ export type Align = 'center' | 'inline-start';
25
+ /** As much of a box as a placement needs, which is what makes this checkable without one. */
26
+ export interface Box {
27
+ readonly top: number;
28
+ readonly left: number;
29
+ readonly width: number;
30
+ readonly height: number;
31
+ }
32
+ /** Where the overlay goes: the side it took, and the two insets to write. */
33
+ export interface Placement {
34
+ readonly side: Side;
35
+ readonly blockStart: number;
36
+ readonly inlineStart: number;
37
+ }
38
+ /**
39
+ * Puts `box` beside `anchor` on the side it asked for, on the other side when there is no
40
+ * room, and inside the viewport either way.
41
+ *
42
+ * @param anchor - The element being pointed at, in viewport coordinates.
43
+ * @param box - The overlay, measured **after** it is shown: its height depends on how its
44
+ * text wrapped at this width in this font, which no caller can know and none should guess.
45
+ * @param viewport - The visible area, scrollbar excluded — `document.documentElement`'s
46
+ * client size rather than `window.inner*`, so a reserved scrollbar is not counted as room.
47
+ * @param prefer - Where it goes when it fits. A tip goes above its trigger; a menu hangs
48
+ * below its button.
49
+ * @param align - `center` for something that points at the anchor, `inline-start` for
50
+ * something that hangs from it.
51
+ */
52
+ export declare function place(anchor: Box, box: Box, viewport: {
53
+ readonly width: number;
54
+ readonly height: number;
55
+ }, prefer: Side, align: Align): Placement;
56
+ //# sourceMappingURL=placement.d.ts.map