@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/README.md +3 -2
- package/dist/field.js +2 -2
- package/dist/field.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/menu.d.ts +92 -0
- package/dist/menu.d.ts.map +1 -0
- package/dist/menu.js +597 -0
- package/dist/menu.js.map +1 -0
- package/dist/placement.d.ts +56 -0
- package/dist/placement.d.ts.map +1 -0
- package/dist/placement.js +60 -0
- package/dist/placement.js.map +1 -0
- package/dist/table.d.ts +56 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +175 -0
- package/dist/table.js.map +1 -0
- package/dist/tokens.d.ts +2 -2
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +62 -0
- package/dist/tokens.js.map +1 -1
- package/dist/tooltip.d.ts +7 -3
- package/dist/tooltip.d.ts.map +1 -1
- package/dist/tooltip.js +16 -15
- package/dist/tooltip.js.map +1 -1
- package/package.json +1 -1
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
|
package/dist/menu.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu.js","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|