@flighthq/menu 0.3.0-next.906.07cea63 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/menu.js CHANGED
@@ -123,7 +123,17 @@ function _validateItem(item, seen) {
123
123
  if (item.type !== 'submenu' && item.submenu !== undefined && item.submenu.length > 0) {
124
124
  return `item type "${item.type ?? 'normal'}" has a submenu (only type "submenu" should carry children)`;
125
125
  }
126
+ // `checked` only means something on the two toggle types. Setting it elsewhere is the mistake that
127
+ // renders silently: the web backend draws the checkmark from `checked` alone, so a normal item with
128
+ // checked: true grows a tick it can never clear, and native backends typically drop it instead —
129
+ // same descriptor, two different wrong results, which is exactly what validation is for.
130
+ if (item.checked !== undefined && item.type !== 'checkbox' && item.type !== 'radio') {
131
+ return `item type "${item.type ?? 'normal'}" has "checked" (only "checkbox" and "radio" items are checkable)`;
132
+ }
126
133
  if (item.submenu !== undefined) {
134
+ const groupError = _validateRadioGroups(item.submenu);
135
+ if (groupError !== null)
136
+ return groupError;
127
137
  seen.add(item);
128
138
  for (const child of item.submenu) {
129
139
  const err = _validateItem(child, seen);
@@ -134,10 +144,33 @@ function _validateItem(item, seen) {
134
144
  }
135
145
  return null;
136
146
  }
147
+ // A radio group is a run of adjacent radio items — any other item type, including a separator, starts
148
+ // a new one. Exactly one member of a run may be checked; two checked members describe a state the
149
+ // widget cannot represent, and each backend picks a different winner. Checked on a non-radio item is
150
+ // caught per-item above; this is the rule that only exists across siblings, which is why it runs where
151
+ // the child list is known rather than inside the per-item walk.
152
+ function _validateRadioGroups(items) {
153
+ let checkedInRun = 0;
154
+ for (const item of items) {
155
+ if (item.type !== 'radio') {
156
+ checkedInRun = 0;
157
+ continue;
158
+ }
159
+ if (item.checked === true)
160
+ checkedInRun++;
161
+ if (checkedInRun > 1) {
162
+ return `radio group has ${checkedInRun} checked items (a radio group may have at most one)`;
163
+ }
164
+ }
165
+ return null;
166
+ }
137
167
  // --- Web context-menu renderer ---
138
168
  // Renders a minimal DOM popup for showContextMenu in a browser environment. Returns the clicked item
139
- // id or null when the menu is dismissed without a selection. Supports keyboard navigation (ArrowUp/
140
- // Down, Enter, Escape) and submenu expansion on hover/arrow-right.
169
+ // id or null when the menu is dismissed without a selection. Keyboard support is ArrowUp/ArrowDown
170
+ // within one menu level, plus Enter/Space to select and Escape to dismiss. Submenus open on hover
171
+ // only: there is no ArrowRight/ArrowLeft traversal, so a submenu's rows are unreachable by keyboard,
172
+ // and Enter on a submenu parent currently resolves the popup with that parent's id rather than
173
+ // opening it. Both are recorded as open work in the package assessment rather than fixed here.
141
174
  function showWebContextMenu(items, x, y) {
142
175
  return new Promise((resolve) => {
143
176
  if (typeof document === 'undefined') {
@@ -164,14 +197,22 @@ function showWebContextMenu(items, x, y) {
164
197
  menu.remove();
165
198
  resolve(selectedId);
166
199
  }
167
- // Keyboard navigation
168
- const focusableItems = menu.querySelectorAll('li[data-enabled="true"]');
200
+ // Keyboard navigation. Scoped to this menu's own rows: a submenu is built as a nested <ul> inside
201
+ // its parent <li>, so an unscoped descendant query also matches the rows of every collapsed
202
+ // submenu. Those rows are display:none, which made arrow-key travel stop on invisible entries —
203
+ // the highlight vanished for a keypress — and let Enter resolve the popup with the id of an item
204
+ // the user could not see.
205
+ const focusableItems = menu.querySelectorAll(':scope > li[data-enabled="true"]');
169
206
  let focusIndex = -1;
170
207
  function moveFocus(delta) {
171
208
  const items = Array.from(focusableItems);
172
209
  if (items.length === 0)
173
210
  return;
174
- focusIndex = (focusIndex + delta + items.length) % items.length;
211
+ // From the initial no-selection state the wrap has to be spelled out: -1 is a sentinel, not a
212
+ // position, and running it through the modulo lands ArrowUp on the second-to-last row rather
213
+ // than the last one. Down enters at the top, up enters at the bottom.
214
+ focusIndex =
215
+ focusIndex === -1 ? (delta < 0 ? items.length - 1 : 0) : (focusIndex + delta + items.length) % items.length;
175
216
  items.forEach((el, i) => {
176
217
  if (i === focusIndex) {
177
218
  el.setAttribute('data-focused', 'true');
@@ -239,6 +280,11 @@ function buildWebMenuElement(items, onSelect) {
239
280
  'user-select:none',
240
281
  ].join(';');
241
282
  for (const item of items) {
283
+ // Hidden items are not rendered at all, rather than rendered and styled away: an item that is not
284
+ // in the DOM cannot be reached by arrow keys or hover, which is the difference between `visible`
285
+ // and `enabled`.
286
+ if (item.visible === false)
287
+ continue;
242
288
  const li = document.createElement('li');
243
289
  if (item.type === 'separator') {
244
290
  li.style.cssText = 'height:1px;margin:4px 8px;background:#e0e0e0;';
@@ -268,7 +314,17 @@ function buildWebMenuElement(items, onSelect) {
268
314
  const labelEl = document.createElement('span');
269
315
  labelEl.textContent = item.label ?? '';
270
316
  labelEl.style.cssText = 'flex:1;';
317
+ if (item.sublabel !== undefined) {
318
+ const sublabelEl = document.createElement('span');
319
+ sublabelEl.textContent = item.sublabel;
320
+ sublabelEl.style.cssText = 'display:block;font-size:11px;color:#888;';
321
+ labelEl.appendChild(sublabelEl);
322
+ }
271
323
  li.appendChild(labelEl);
324
+ // The accelerator/submenu spans below are matched with span:last-child on hover, so the tooltip is
325
+ // set as an attribute rather than appended as another child.
326
+ if (item.toolTip !== undefined)
327
+ li.title = item.toolTip;
272
328
  if (hasSubmenu) {
273
329
  const arrow = document.createElement('span');
274
330
  arrow.textContent = '▶';
package/dist/menu.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"menu.js","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"menu.js","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAGtE,qGAAqG;AACrG,wEAAwE;AACxE,MAAM,UAAU,iBAAiB,CAAC,QAAoC;IACpE,MAAM,KAAK,GAAqB,EAAE,GAAG,QAAQ,EAAE,CAAC;IAChD,IAAI,QAAQ,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACnC,KAAK,CAAC,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,sGAAsG;AACtG,uGAAuG;AACvG,+BAA+B;AAC/B,MAAM,UAAU,sBAAsB,CAAC,QAA8C;IACnF,MAAM,IAAI,GAAqB;QAC7B,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,IAAI;QACb,GAAG,QAAQ;KACZ,CAAC;IACF,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,uGAAuG;AACvG,uGAAuG;AACvG,+BAA+B;AAC/B,MAAM,UAAU,oBAAoB;IAClC,OAAO;QACL,kBAAkB;YAChB,6FAA6F;YAC7F,OAAO,KAAK,CAAC;QACf,CAAC;QACD,gBAAgB,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC;YAC1B,OAAO,kBAAkB,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACzC,CAAC;QACD,eAAe;YACb,4DAA4D;YAC5D,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,qGAAqG;AACrG,oGAAoG;AACpG,2FAA2F;AAC3F,MAAM,UAAU,iBAAiB;IAC/B,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QAC1B,YAAY,GAAG;YACb,kBAAkB,EAAE,YAAY,EAAE;YAClC,iBAAiB,EAAE,YAAY,EAAE;YACjC,mBAAmB,EAAE,YAAY,EAAE;YACnC,gBAAgB,EAAE,YAAY,EAAE;SACjC,CAAC;IACJ,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,cAAc;IAC5B,IAAI,QAAQ,KAAK,IAAI;QAAE,QAAQ,GAAG,oBAAoB,EAAE,CAAC;IACzD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,cAAc;IAC5B,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,qGAAqG;AACrG,qGAAqG;AACrG,oCAAoC;AACpC,MAAM,UAAU,YAAY,CAAC,QAA8B;IACzD,OAAO,cAAc,EAAE,CAAC,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE;QAC7C,QAAQ,CAAC,EAAE,CAAC,CAAC;QACb,IAAI,YAAY,KAAK,IAAI;YAAE,UAAU,CAAC,YAAY,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;AACL,CAAC;AAED,oGAAoG;AACpG,uBAAuB;AACvB,MAAM,UAAU,kBAAkB,CAAC,KAAkC;IACnE,OAAO,cAAc,EAAE,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC;AACpD,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,cAAc,CAAC,OAA2B;IACxD,QAAQ,GAAG,OAAO,CAAC;AACrB,CAAC;AAED,qGAAqG;AACrG,oGAAoG;AACpG,+DAA+D;AAC/D,MAAM,UAAU,eAAe,CAAC,KAAkC,EAAE,CAAS,EAAE,CAAS;IACtF,IAAI,YAAY,KAAK,IAAI;QAAE,UAAU,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;IACtE,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/D,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,YAAY,CAAC;QAC7B,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,qGAAqG;AACrG,oGAAoG;AACpG,oDAAoD;AACpD,MAAM,UAAU,wBAAwB,CAAC,QAAoC;IAC3E,OAAO,aAAa,CAAC,QAAQ,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED,IAAI,QAAQ,GAAuB,IAAI,CAAC;AACxC,IAAI,YAAY,GAAuB,IAAI,CAAC;AAE5C,SAAS,aAAa,CAAC,IAAgC,EAAE,IAAqC;IAC5F,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACjF,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC9B,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,KAAK,EAAE,EAAE,CAAC;YAClD,OAAO,gCAAgC,IAAI,CAAC,KAAK,uCAAuC,CAAC;QAC3F,CAAC;QACD,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YACnC,OAAO,uCAAuC,IAAI,CAAC,WAAW,GAAG,CAAC;QACpE,CAAC;QACD,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAC/B,OAAO,8BAA8B,CAAC;QACxC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrF,OAAO,cAAc,IAAI,CAAC,IAAI,IAAI,QAAQ,6DAA6D,CAAC;IAC1G,CAAC;IACD,mGAAmG;IACnG,oGAAoG;IACpG,iGAAiG;IACjG,yFAAyF;IACzF,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACpF,OAAO,cAAc,IAAI,CAAC,IAAI,IAAI,QAAQ,mEAAmE,CAAC;IAChH,CAAC;IACD,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,UAAU,GAAG,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACtD,IAAI,UAAU,KAAK,IAAI;YAAE,OAAO,UAAU,CAAC;QAC3C,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACf,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjC,MAAM,GAAG,GAAG,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACvC,IAAI,GAAG,KAAK,IAAI;gBAAE,OAAO,GAAG,CAAC;QAC/B,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sGAAsG;AACtG,kGAAkG;AAClG,qGAAqG;AACrG,uGAAuG;AACvG,gEAAgE;AAChE,SAAS,oBAAoB,CAAC,KAA4C;IACxE,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC1B,YAAY,GAAG,CAAC,CAAC;YACjB,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI;YAAE,YAAY,EAAE,CAAC;QAC1C,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO,mBAAmB,YAAY,qDAAqD,CAAC;QAC9F,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,oCAAoC;AACpC,qGAAqG;AACrG,mGAAmG;AACnG,kGAAkG;AAClG,qGAAqG;AACrG,+FAA+F;AAC/F,+FAA+F;AAC/F,SAAS,kBAAkB,CAAC,KAAkC,EAAE,CAAS,EAAE,CAAS;IAClF,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;YACpC,OAAO,CAAC,IAAI,CAAC,CAAC;YACd,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9C,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,4CAA4C,CAAC;QACrE,MAAM,IAAI,GAAG,mBAAmB,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;QAC3D,SAAS,SAAS,CAAC,EAAe,EAAE,OAAe,EAAE,OAAe;YAClE,EAAE,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,OAAO,IAAI,CAAC;YAC/B,EAAE,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,OAAO,IAAI,CAAC;YAC9B,MAAM,IAAI,GAAG,EAAE,CAAC,qBAAqB,EAAE,CAAC;YACxC,MAAM,EAAE,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;YACjE,MAAM,EAAE,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;YAClE,IAAI,IAAI,CAAC,KAAK,GAAG,EAAE;gBAAE,EAAE,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;YAC9E,IAAI,IAAI,CAAC,MAAM,GAAG,EAAE;gBAAE,EAAE,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;QACjF,CAAC;QACD,SAAS,KAAK,CAAC,UAAyB;YACtC,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACnD,OAAO,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,OAAO,CAAC,UAAU,CAAC,CAAC;QACtB,CAAC;QACD,kGAAkG;QAClG,4FAA4F;QAC5F,gGAAgG;QAChG,iGAAiG;QACjG,0BAA0B;QAC1B,MAAM,cAAc,GAAG,IAAI,CAAC,gBAAgB,CAAc,kCAAkC,CAAC,CAAC;QAC9F,IAAI,UAAU,GAAG,CAAC,CAAC,CAAC;QACpB,SAAS,SAAS,CAAC,KAAa;YAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YACzC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO;YAC/B,8FAA8F;YAC9F,6FAA6F;YAC7F,sEAAsE;YACtE,UAAU;gBACR,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC;YAC9G,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;gBACtB,IAAI,CAAC,KAAK,UAAU,EAAE,CAAC;oBACrB,EAAE,CAAC,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;oBACxC,EAAE,CAAC,KAAK,CAAC,UAAU,GAAG,SAAS,CAAC;oBAChC,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;gBAC1B,CAAC;qBAAM,CAAC;oBACN,EAAE,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC;oBACnC,EAAE,CAAC,KAAK,CAAC,UAAU,GAAG,EAAE,CAAC;oBACzB,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;gBAC1B,CAAC;YACH,CAAC,CAAC,CAAC;YACH,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;gBAC1B,MAAM,OAAO,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;gBAClC,MAAM,MAAM,GAAG,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAC1C,IAAI,MAAM,KAAK,SAAS;oBAAE,UAAU,CAAC,YAAY,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;YACjF,CAAC;QACH,CAAC;QACD,SAAS,SAAS,CAAC,CAAgB;YACjC,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;gBACvB,CAAC,CAAC,cAAc,EAAE,CAAC;gBACnB,KAAK,CAAC,IAAI,CAAC,CAAC;YACd,CAAC;iBAAM,IAAI,CAAC,CAAC,GAAG,KAAK,WAAW,EAAE,CAAC;gBACjC,CAAC,CAAC,cAAc,EAAE,CAAC;gBACnB,SAAS,CAAC,CAAC,CAAC,CAAC;YACf,CAAC;iBAAM,IAAI,CAAC,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;gBAC/B,CAAC,CAAC,cAAc,EAAE,CAAC;gBACnB,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;YAChB,CAAC;iBAAM,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;gBAC9C,CAAC,CAAC,cAAc,EAAE,CAAC;gBACnB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,UAAU,CAAC,CAAC;gBACvD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;oBAC1B,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;oBACzC,IAAI,MAAM,KAAK,SAAS;wBAAE,KAAK,CAAC,MAAM,CAAC,CAAC;gBAC1C,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACrD,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAChD,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACnC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAChC,SAAS,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,mBAAmB,CAAC,KAAkC,EAAE,QAA8B;IAC7F,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG;QACnB,gBAAgB;QAChB,oBAAoB;QACpB,UAAU;QACV,eAAe;QACf,iBAAiB;QACjB,iBAAiB;QACjB,uBAAuB;QACvB,mBAAmB;QACnB,uCAAuC;QACvC,iBAAiB;QACjB,oCAAoC;QACpC,YAAY;QACZ,kBAAkB;KACnB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,kGAAkG;QAClG,iGAAiG;QACjG,iBAAiB;QACjB,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK;YAAE,SAAS;QACrC,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC9B,EAAE,CAAC,KAAK,CAAC,OAAO,GAAG,+CAA+C,CAAC;YACnE,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;YACrB,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,KAAK,KAAK,CAAC;QACvC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;QACzE,EAAE,CAAC,YAAY,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,EAAE,KAAK,SAAS;YAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;QAC1D,EAAE,CAAC,KAAK,CAAC,OAAO,GAAG;YACjB,cAAc;YACd,oBAAoB;YACpB,2BAA2B;YAC3B,SAAS,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC;YACjD,QAAQ,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;YACtC,mBAAmB;SACpB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACZ,wBAAwB;QACxB,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;YAC5C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;YACrD,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,4CAA4C,CAAC;YAClE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;QACD,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAC/C,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QACvC,OAAO,CAAC,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC;QAClC,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;YAClD,UAAU,CAAC,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC;YACvC,UAAU,CAAC,KAAK,CAAC,OAAO,GAAG,0CAA0C,CAAC;YACtE,OAAO,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QAClC,CAAC;QACD,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACxB,mGAAmG;QACnG,6DAA6D;QAC7D,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS;YAAE,EAAE,CAAC,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC;QACxD,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;YAC7C,KAAK,CAAC,WAAW,GAAG,GAAG,CAAC;YACxB,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,2CAA2C,CAAC;YAClE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACxB,CAAC;aAAM,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YAC1C,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;YAC7C,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;YACrC,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,6CAA6C,CAAC;YACpE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACxB,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACZ,EAAE,CAAC,gBAAgB,CAAC,YAAY,EAAE,GAAG,EAAE;gBACrC,EAAE,CAAC,KAAK,CAAC,UAAU,GAAG,SAAS,CAAC;gBAChC,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;gBACxB,MAAM,OAAO,GAAG,EAAE,CAAC,aAAa,CAAc,iBAAiB,CAAC,CAAC;gBACjE,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,OAAO;oBAAE,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,sBAAsB,CAAC;gBAC1F,IAAI,YAAY,KAAK,IAAI,IAAI,IAAI,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;oBACnD,UAAU,CAAC,YAAY,CAAC,mBAAmB,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;gBACxD,CAAC;YACH,CAAC,CAAC,CAAC;YACH,EAAE,CAAC,gBAAgB,CAAC,YAAY,EAAE,GAAG,EAAE;gBACrC,EAAE,CAAC,KAAK,CAAC,UAAU,GAAG,EAAE,CAAC;gBACzB,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;gBACxB,MAAM,OAAO,GAAG,EAAE,CAAC,aAAa,CAAc,iBAAiB,CAAC,CAAC;gBACjE,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,OAAO;oBAAE,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;YAC5E,CAAC,CAAC,CAAC;YACH,IAAI,UAAU,EAAE,CAAC;gBACf,kDAAkD;gBAClD,MAAM,SAAS,GAAG,mBAAmB,CAAC,IAAI,CAAC,OAAQ,EAAE,QAAQ,CAAC,CAAC;gBAC/D,SAAS,CAAC,KAAK,CAAC,QAAQ,GAAG,UAAU,CAAC;gBACtC,SAAS,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,CAAC;gBAC1B,SAAS,CAAC,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC;gBAC9B,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;gBACjC,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;gBAC1B,EAAE,CAAC,gBAAgB,CAAC,YAAY,EAAE,GAAG,EAAE;oBACrC,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;gBACpC,CAAC,CAAC,CAAC;gBACH,EAAE,CAAC,gBAAgB,CAAC,YAAY,EAAE,GAAG,EAAE;oBACrC,SAAS,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;gBACnC,CAAC,CAAC,CAAC;YACL,CAAC;iBAAM,IAAI,IAAI,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;gBACjC,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;gBACvB,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;oBACjC,CAAC,CAAC,eAAe,EAAE,CAAC;oBACpB,QAAQ,CAAC,MAAM,CAAC,CAAC;gBACnB,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "@flighthq/menu",
3
- "version": "0.3.0-next.906.07cea63",
3
+ "version": "0.3.0",
4
+ "author": "Joshua Granick and other contributors",
5
+ "license": "MIT",
4
6
  "repository": {
5
7
  "type": "git",
6
8
  "url": "https://github.com/flighthq/flight.git",
@@ -36,8 +38,8 @@
36
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
37
39
  },
38
40
  "dependencies": {
39
- "@flighthq/signals": "0.3.0-next.906.07cea63",
40
- "@flighthq/types": "0.3.0-next.906.07cea63"
41
+ "@flighthq/signals": "0.3.0",
42
+ "@flighthq/types": "0.3.0"
41
43
  },
42
44
  "devDependencies": {
43
45
  "typescript": "^5.3.0"
package/src/menu.test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MenuBackend } from '@flighthq/types/contract';
1
+ import type { MenuBackend, MenuItemTemplate } from '@flighthq/types/contract';
2
2
 
3
3
  import {
4
4
  cloneMenuTemplate,
@@ -64,6 +64,124 @@ describe('createWebMenuBackend', () => {
64
64
  });
65
65
  });
66
66
 
67
+ describe('createWebMenuBackend context-menu renderer', () => {
68
+ afterEach(() => {
69
+ document.body.replaceChildren();
70
+ vi.restoreAllMocks();
71
+ });
72
+
73
+ function getRootMenu(): HTMLUListElement {
74
+ const menu = document.body.querySelector('ul');
75
+ expect(menu).not.toBeNull();
76
+ return menu as HTMLUListElement;
77
+ }
78
+
79
+ function getOverlay(): HTMLDivElement {
80
+ const overlay = document.body.querySelector('div');
81
+ expect(overlay).not.toBeNull();
82
+ return overlay as HTMLDivElement;
83
+ }
84
+
85
+ function press(key: string): void {
86
+ document.dispatchEvent(new KeyboardEvent('keydown', { key }));
87
+ }
88
+
89
+ it('renders separators, checked kinds, accelerators, and disabled items', async () => {
90
+ const promise = createWebMenuBackend().popupContextMenu(
91
+ [
92
+ { id: 'checked', label: 'Checked', type: 'checkbox', checked: true },
93
+ { id: 'chosen', label: 'Chosen', type: 'radio', checked: true },
94
+ { type: 'separator' },
95
+ { id: 'save', label: 'Save', accelerator: 'Ctrl+S' },
96
+ { id: 'disabled', label: 'Disabled', enabled: false },
97
+ ],
98
+ 10,
99
+ 20,
100
+ );
101
+ const items = getRootMenu().querySelectorAll(':scope > li');
102
+ expect(items).toHaveLength(5);
103
+ expect(items[0].textContent).toBe('✓Checked');
104
+ expect(items[1].textContent).toBe('●Chosen');
105
+ expect(items[2].getAttribute('data-enabled')).toBeNull();
106
+ expect(items[3].textContent).toBe('SaveCtrl+S');
107
+ expect(items[4].getAttribute('data-enabled')).toBe('false');
108
+
109
+ let settled = false;
110
+ void promise.then(() => {
111
+ settled = true;
112
+ });
113
+ items[4].dispatchEvent(new MouseEvent('click', { bubbles: true }));
114
+ await Promise.resolve();
115
+ expect(settled).toBe(false);
116
+ getOverlay().click();
117
+ await expect(promise).resolves.toBeNull();
118
+ });
119
+
120
+ it('selects enabled items by click and dismisses on outside click', async () => {
121
+ const backend = createWebMenuBackend();
122
+ const selected = backend.popupContextMenu([{ id: 'copy', label: 'Copy' }], 0, 0);
123
+ getRootMenu().querySelector<HTMLElement>('li')!.click();
124
+ await expect(selected).resolves.toBe('copy');
125
+ expect(document.body.querySelector('ul')).toBeNull();
126
+
127
+ const dismissed = backend.popupContextMenu([{ id: 'paste', label: 'Paste' }], 0, 0);
128
+ getOverlay().click();
129
+ await expect(dismissed).resolves.toBeNull();
130
+ });
131
+
132
+ it('wraps keyboard focus and selects with Enter or Space', async () => {
133
+ const items = [
134
+ { id: 'first', label: 'First' },
135
+ { id: 'disabled', label: 'Disabled', enabled: false },
136
+ { id: 'last', label: 'Last' },
137
+ ];
138
+
139
+ const enterSelection = createWebMenuBackend().popupContextMenu(items, 0, 0);
140
+ press('ArrowUp');
141
+ expect(getRootMenu().querySelector('[data-focused="true"]')?.getAttribute('data-item-id')).toBe('last');
142
+ press('Enter');
143
+ await expect(enterSelection).resolves.toBe('last');
144
+
145
+ const spaceSelection = createWebMenuBackend().popupContextMenu(items, 0, 0);
146
+ press('ArrowDown');
147
+ press('ArrowDown');
148
+ press('ArrowDown');
149
+ expect(getRootMenu().querySelector('[data-focused="true"]')?.getAttribute('data-item-id')).toBe('first');
150
+ press(' ');
151
+ await expect(spaceSelection).resolves.toBe('first');
152
+ });
153
+
154
+ it('dismisses on Escape', async () => {
155
+ const promise = createWebMenuBackend().popupContextMenu([{ id: 'copy', label: 'Copy' }], 0, 0);
156
+ press('Escape');
157
+ await expect(promise).resolves.toBeNull();
158
+ });
159
+
160
+ it('opens a submenu on hover and resolves a child selection', async () => {
161
+ const promise = createWebMenuBackend().popupContextMenu(
162
+ [{ id: 'file', label: 'File', type: 'submenu', submenu: [{ id: 'open', label: 'Open' }] }],
163
+ 0,
164
+ 0,
165
+ );
166
+ const parent = getRootMenu().querySelector<HTMLElement>(':scope > li')!;
167
+ const submenu = parent.querySelector<HTMLElement>('ul')!;
168
+ expect(submenu.style.display).toBe('none');
169
+ parent.dispatchEvent(new MouseEvent('mouseenter'));
170
+ expect(submenu.style.display).toBe('block');
171
+ submenu.querySelector<HTMLElement>('li')!.click();
172
+ await expect(promise).resolves.toBe('open');
173
+ });
174
+
175
+ it('clamps a menu that overflows the viewport', async () => {
176
+ vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue(new DOMRect(1900, 1900, 100, 100));
177
+ const promise = createWebMenuBackend().popupContextMenu([{ id: 'copy', label: 'Copy' }], 40, 30);
178
+ expect(getRootMenu().style.left).toBe('0px');
179
+ expect(getRootMenu().style.top).toBe('0px');
180
+ getOverlay().click();
181
+ await promise;
182
+ });
183
+ });
184
+
67
185
  describe('enableMenuSignals', () => {
68
186
  it('returns a stable instance across calls', () => {
69
187
  expect(enableMenuSignals()).toBe(enableMenuSignals());
@@ -143,6 +261,157 @@ describe('showContextMenu', () => {
143
261
  setMenuBackend(fakeBackend({ popupContextMenu: () => Promise.resolve('copy') }));
144
262
  await expect(showContextMenu([], 0, 0)).resolves.toBe('copy');
145
263
  });
264
+
265
+ // The rest of this block exercises the real web renderer through the default backend, in jsdom.
266
+ describe('web renderer', () => {
267
+ const menuItems: MenuItemTemplate[] = [
268
+ { id: 'open', label: 'Open' },
269
+ { id: 'disabled', label: 'Disabled', enabled: false },
270
+ { type: 'separator' },
271
+ { id: 'recent', label: 'Recent', submenu: [{ id: 'file1', label: 'File 1' }], type: 'submenu' },
272
+ { id: 'quit', label: 'Quit' },
273
+ ];
274
+
275
+ beforeEach(() => {
276
+ setMenuBackend(null);
277
+ document.body.innerHTML = '';
278
+ });
279
+
280
+ function press(key: string): void {
281
+ document.dispatchEvent(new KeyboardEvent('keydown', { key }));
282
+ }
283
+
284
+ function renderedMenu(): HTMLUListElement {
285
+ return document.body.querySelector('ul') as HTMLUListElement;
286
+ }
287
+
288
+ it('resolves with the clicked item id', async () => {
289
+ const promise = showContextMenu(menuItems, 10, 10);
290
+ renderedMenu().querySelector<HTMLElement>('li[data-item-id="quit"]')!.click();
291
+ await expect(promise).resolves.toBe('quit');
292
+ });
293
+
294
+ it('resolves null when dismissed with Escape', async () => {
295
+ const promise = showContextMenu(menuItems, 10, 10);
296
+ press('Escape');
297
+ await expect(promise).resolves.toBeNull();
298
+ });
299
+
300
+ it('removes its DOM once dismissed', async () => {
301
+ const promise = showContextMenu(menuItems, 10, 10);
302
+ expect(renderedMenu()).not.toBeNull();
303
+ press('Escape');
304
+ await promise;
305
+ expect(renderedMenu()).toBeNull();
306
+ });
307
+
308
+ it('marks a disabled item unfocusable and gives it no click handler', async () => {
309
+ const promise = showContextMenu(menuItems, 10, 10);
310
+ const disabled = renderedMenu().querySelector<HTMLElement>('li[data-item-id="disabled"]')!;
311
+ expect(disabled.getAttribute('data-enabled')).toBe('false');
312
+
313
+ disabled.click();
314
+ let settled = false;
315
+ void promise.then(() => (settled = true));
316
+ await Promise.resolve();
317
+ expect(settled).toBe(false);
318
+
319
+ press('Escape');
320
+ await promise;
321
+ });
322
+
323
+ // Arrow travel must visit only the rows of this menu level. A submenu is a nested <ul> inside its
324
+ // parent <li>, so a descendant query also matched every collapsed submenu row — display:none
325
+ // entries that stole a keypress and could be selected unseen. Third ArrowDown lands on 'quit'
326
+ // (open → recent → quit); before the fix it landed on the hidden 'file1'.
327
+ it('skips the rows of a collapsed submenu when arrowing through the menu', async () => {
328
+ const promise = showContextMenu(menuItems, 10, 10);
329
+ press('ArrowDown');
330
+ press('ArrowDown');
331
+ press('ArrowDown');
332
+ press('Enter');
333
+ await expect(promise).resolves.toBe('quit');
334
+ });
335
+
336
+ // With nothing focused, ArrowUp enters at the bottom of the menu. -1 is a sentinel rather than a
337
+ // position, so running it through the wrap arithmetic used to land one row short of the end.
338
+ it('enters at the last item when the first key is ArrowUp', async () => {
339
+ const promise = showContextMenu(menuItems, 10, 10);
340
+ press('ArrowUp');
341
+ press('Enter');
342
+ await expect(promise).resolves.toBe('quit');
343
+ });
344
+
345
+ it('omits an item marked visible: false', async () => {
346
+ const promise = showContextMenu(
347
+ [
348
+ { id: 'shown', label: 'Shown' },
349
+ { id: 'hidden', label: 'Hidden', visible: false },
350
+ ],
351
+ 0,
352
+ 0,
353
+ );
354
+ const menu = renderedMenu();
355
+ expect(menu.querySelector('li[data-item-id="hidden"]')).toBeNull();
356
+ expect(menu.querySelector('li[data-item-id="shown"]')).not.toBeNull();
357
+ press('Escape');
358
+ await promise;
359
+ });
360
+
361
+ // A hidden item must not merely be styled away: if it were still in the DOM it would keep its
362
+ // place in arrow travel, which is the bug `visible` exists to avoid.
363
+ it('does not give a hidden item a place in arrow travel', async () => {
364
+ const promise = showContextMenu(
365
+ [
366
+ { id: 'first', label: 'First' },
367
+ { id: 'hidden', label: 'Hidden', visible: false },
368
+ { id: 'last', label: 'Last' },
369
+ ],
370
+ 0,
371
+ 0,
372
+ );
373
+ press('ArrowDown');
374
+ press('ArrowDown');
375
+ press('Enter');
376
+ await expect(promise).resolves.toBe('last');
377
+ });
378
+
379
+ it('renders a tooltip and a sublabel when supplied', async () => {
380
+ const promise = showContextMenu(
381
+ [{ id: 'save', label: 'Save', sublabel: 'All files', toolTip: 'Save now' }],
382
+ 0,
383
+ 0,
384
+ );
385
+ const li = renderedMenu().querySelector<HTMLElement>('li[data-item-id="save"]')!;
386
+ expect(li.title).toBe('Save now');
387
+ expect(li.textContent).toContain('All files');
388
+ press('Escape');
389
+ await promise;
390
+ });
391
+
392
+ it('draws a radio dot and a checkbox tick from the item type', async () => {
393
+ const promise = showContextMenu(
394
+ [
395
+ { id: 'r', label: 'Radio', type: 'radio', checked: true },
396
+ { id: 'c', label: 'Check', type: 'checkbox', checked: true },
397
+ ],
398
+ 0,
399
+ 0,
400
+ );
401
+ const menu = renderedMenu();
402
+ expect(menu.querySelector<HTMLElement>('li[data-item-id="r"]')!.textContent).toContain('●');
403
+ expect(menu.querySelector<HTMLElement>('li[data-item-id="c"]')!.textContent).toContain('✓');
404
+ press('Escape');
405
+ await promise;
406
+ });
407
+
408
+ it('resolves null when the backdrop is clicked', async () => {
409
+ const promise = showContextMenu(menuItems, 10, 10);
410
+ const overlay = document.body.querySelector<HTMLElement>('div')!;
411
+ overlay.click();
412
+ await expect(promise).resolves.toBeNull();
413
+ });
414
+ });
146
415
  });
147
416
 
148
417
  describe('validateMenuItemTemplate', () => {
@@ -150,6 +419,23 @@ describe('validateMenuItemTemplate', () => {
150
419
  expect(validateMenuItemTemplate(createMenuItemTemplate({ id: 'ok', label: 'Ok' }))).toBeNull();
151
420
  });
152
421
 
422
+ it('rejects checked state on an item that is not a checkbox or radio', () => {
423
+ expect(validateMenuItemTemplate({ id: 'copy', label: 'Copy', type: 'normal', checked: true })).toContain('checked');
424
+ });
425
+
426
+ it('allows checked radios in separate contiguous groups', () => {
427
+ expect(
428
+ validateMenuItemTemplate({
429
+ type: 'submenu',
430
+ submenu: [
431
+ { id: 'left', type: 'radio', checked: true },
432
+ { id: 'copy', type: 'normal' },
433
+ { id: 'right', type: 'radio', checked: true },
434
+ ],
435
+ }),
436
+ ).toBeNull();
437
+ });
438
+
153
439
  it('rejects a separator that carries a label', () => {
154
440
  expect(validateMenuItemTemplate({ type: 'separator', label: 'X' })).not.toBeNull();
155
441
  });
@@ -159,4 +445,78 @@ describe('validateMenuItemTemplate', () => {
159
445
  node.submenu = [node];
160
446
  expect(() => validateMenuItemTemplate(node)).toThrow();
161
447
  });
448
+
449
+ it('accepts checked on a checkbox item', () => {
450
+ expect(validateMenuItemTemplate({ id: 'wrap', label: 'Wrap', type: 'checkbox', checked: true })).toBeNull();
451
+ });
452
+
453
+ it('accepts checked on a radio item', () => {
454
+ expect(validateMenuItemTemplate({ id: 'left', label: 'Left', type: 'radio', checked: false })).toBeNull();
455
+ });
456
+
457
+ // The web backend draws its tick from `checked` alone, so a normal item carrying it renders a mark
458
+ // nothing can clear, while native backends tend to drop it — one descriptor, two wrong results.
459
+ it('rejects checked on a normal item', () => {
460
+ expect(validateMenuItemTemplate({ id: 'copy', label: 'Copy', checked: true })).not.toBeNull();
461
+ });
462
+
463
+ it('rejects checked on a submenu item', () => {
464
+ expect(
465
+ validateMenuItemTemplate({ id: 'more', label: 'More', type: 'submenu', checked: true, submenu: [{ id: 'a' }] }),
466
+ ).not.toBeNull();
467
+ });
468
+
469
+ // checked: false is still a claim about checkability, so it is rejected on a non-toggle item too —
470
+ // otherwise the rule would only catch half the mistake.
471
+ it('rejects checked: false on a normal item', () => {
472
+ expect(validateMenuItemTemplate({ id: 'copy', label: 'Copy', checked: false })).not.toBeNull();
473
+ });
474
+
475
+ it('rejects two checked radios in the same group', () => {
476
+ const parent = createMenuItemTemplate({
477
+ id: 'align',
478
+ type: 'submenu',
479
+ submenu: [
480
+ { id: 'left', label: 'Left', type: 'radio', checked: true },
481
+ { id: 'right', label: 'Right', type: 'radio', checked: true },
482
+ ],
483
+ });
484
+ expect(validateMenuItemTemplate(parent)).toContain('radio group');
485
+ });
486
+
487
+ it('accepts one checked radio in a group', () => {
488
+ const parent = createMenuItemTemplate({
489
+ id: 'align',
490
+ type: 'submenu',
491
+ submenu: [
492
+ { id: 'left', label: 'Left', type: 'radio', checked: true },
493
+ { id: 'right', label: 'Right', type: 'radio', checked: false },
494
+ ],
495
+ });
496
+ expect(validateMenuItemTemplate(parent)).toBeNull();
497
+ });
498
+
499
+ // A separator ends the run, so the two checked radios below belong to different groups and are both
500
+ // legitimate — the rule is about adjacency, not about the whole child list.
501
+ it('accepts a checked radio in each of two groups split by a separator', () => {
502
+ const parent = createMenuItemTemplate({
503
+ id: 'view',
504
+ type: 'submenu',
505
+ submenu: [
506
+ { id: 'left', label: 'Left', type: 'radio', checked: true },
507
+ { type: 'separator' },
508
+ { id: 'small', label: 'Small', type: 'radio', checked: true },
509
+ ],
510
+ });
511
+ expect(validateMenuItemTemplate(parent)).toBeNull();
512
+ });
513
+
514
+ it('reports the offending child of a submenu', () => {
515
+ const parent = createMenuItemTemplate({
516
+ id: 'view',
517
+ type: 'submenu',
518
+ submenu: [{ id: 'zoom', label: 'Zoom', checked: true }],
519
+ });
520
+ expect(validateMenuItemTemplate(parent)).toContain('checked');
521
+ });
162
522
  });