@kubex/zinc 1.1.87 → 1.1.89

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.87",
3
+ "version": "1.1.89",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -9,9 +9,9 @@ interface QuillLike {
9
9
  root: HTMLElement;
10
10
  }
11
11
 
12
- interface ContextMenuLike extends HTMLElement {
12
+ interface SlashMenuLike extends HTMLElement {
13
13
  open: boolean;
14
- results: { label: string }[];
14
+ items: { label: string }[];
15
15
  setActiveIndex: (index: number) => void;
16
16
  }
17
17
 
@@ -19,9 +19,13 @@ interface EditorDialogLike extends HTMLElement {
19
19
  dialogEl: HTMLDialogElement;
20
20
  }
21
21
 
22
+ interface DropdownLike extends HTMLElement {
23
+ open: boolean;
24
+ }
25
+
22
26
  describe('<zn-editor> context menu tools', () => {
23
27
  afterEach(() => {
24
- document.querySelectorAll('zn-context-menu, zn-editor-dialog').forEach(el => el.remove());
28
+ document.querySelectorAll('zn-slash-menu, zn-editor-dialog').forEach(el => el.remove());
25
29
  });
26
30
 
27
31
  const editorWithTool = () => fixture<ZnEditor>(html`
@@ -43,8 +47,8 @@ describe('<zn-editor> context menu tools', () => {
43
47
  return quill;
44
48
  };
45
49
 
46
- const contextMenu = (): ContextMenuLike =>
47
- document.querySelector('zn-context-menu') as unknown as ContextMenuLike;
50
+ const contextMenu = (): SlashMenuLike =>
51
+ document.querySelector('zn-slash-menu') as unknown as SlashMenuLike;
48
52
 
49
53
  it('lists a context-menu flagged tool in the slash menu', async () => {
50
54
  const el = await editorWithTool();
@@ -54,7 +58,7 @@ describe('<zn-editor> context menu tools', () => {
54
58
  expect(menu, 'context menu should exist').to.exist;
55
59
  expect(menu.open, 'context menu should be open').to.be.true;
56
60
 
57
- const labels = menu.results.map(r => r.label);
61
+ const labels = menu.items.map(item => item.label);
58
62
  expect(labels).to.include('Canned Responses');
59
63
  });
60
64
 
@@ -63,7 +67,7 @@ describe('<zn-editor> context menu tools', () => {
63
67
  const quill = await openContextMenu(el);
64
68
 
65
69
  const menu = contextMenu();
66
- const index = menu.results.findIndex(r => r.label === 'Canned Responses');
70
+ const index = menu.items.findIndex(item => item.label === 'Canned Responses');
67
71
  expect(index).to.be.greaterThan(-1);
68
72
  menu.setActiveIndex(index);
69
73
 
@@ -80,4 +84,29 @@ describe('<zn-editor> context menu tools', () => {
80
84
  expect(dialog.dialogEl.open, 'dialog should be open').to.be.true;
81
85
  expect(dialog.innerHTML).to.contain('/canned');
82
86
  });
87
+
88
+ it('keeps the date picker open when Date is chosen with the mouse', async () => {
89
+ const el = await fixture<ZnEditor>(html`
90
+ <zn-editor id="test-editor" dates></zn-editor>`);
91
+ const quill = await openContextMenu(el);
92
+ quill.insertText(1, 'date', 'user');
93
+ quill.setSelection(5, 0, 'user');
94
+ await aTimeout(50);
95
+
96
+ const menu = contextMenu();
97
+ const index = menu.items.findIndex(item => item.label === 'Date');
98
+ expect(index, 'Date should be listed').to.be.greaterThan(-1);
99
+
100
+ const button = menu.shadowRoot!.querySelectorAll<HTMLButtonElement>('[data-slash-item]')[index];
101
+ expect(button, 'Date item should be rendered').to.exist;
102
+
103
+ const toolbar = el.shadowRoot!.getElementById('toolbar');
104
+ const dateDropdown = toolbar?.shadowRoot?.querySelector('zn-dropdown.toolbar__date-dropdown') as DropdownLike | null;
105
+ const overflowDropdown = toolbar?.shadowRoot?.querySelector('zn-dropdown.toolbar__overflow') as DropdownLike | null;
106
+
107
+ button.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true, composed: true}));
108
+ await aTimeout(100);
109
+
110
+ expect(dateDropdown?.open || overflowDropdown?.open, 'a dropdown holding the date picker should be open').to.be.true;
111
+ });
83
112
  });
@@ -1,23 +1,38 @@
1
- import './context-menu-component';
2
- import {html} from "lit";
3
- import {litToHTML} from "../../../../utilities/lit-to-html";
4
- import {type ResultItem} from "./context-menu-component";
1
+ import {filterSlashItems, SLASH_ITEM_SELECT} from "../../../slash-menu";
5
2
  import Delta from "quill-delta";
6
3
  import Quill from "quill";
7
4
  import ZnEditorQuickAction from "./quick-action";
8
5
  import ZnEditorTool from "../toolbar/tool";
9
6
  import type {EditorFeatureConfig} from "../../editor.component";
10
- import type ContextMenuComponent from "./context-menu-component";
7
+ import type {SlashMenuItem} from "../../../slash-menu";
8
+ import type {VirtualElement} from "@floating-ui/dom";
11
9
  import type Toolbar from "../toolbar/toolbar";
10
+ import type ZnSlashMenu from "../../../slash-menu";
11
+
12
+ /**
13
+ * A slash menu entry plus what choosing it does in the editor: trigger a toolbar tool by `key`,
14
+ * or apply `format` with `formatValue`. `SlashMenuItem.value` is left unset so the menu never
15
+ * renders format values as insertion tokens.
16
+ */
17
+ interface ContextMenuItem extends SlashMenuItem {
18
+ format?: string;
19
+ key?: string;
20
+ formatValue?: string | boolean;
21
+ }
12
22
 
13
23
  class ContextMenu {
14
24
  private _quill: Quill;
15
25
  private readonly _toolbarModule: Toolbar;
16
- private _component: ContextMenuComponent;
26
+ private _menu: ZnSlashMenu;
17
27
  private _startIndex = -1;
28
+ /** Trigger position the user dismissed with Escape; the menu stays shut until they move off it. */
29
+ private _dismissedIndex = -1;
18
30
  private _keydownHandler = (e: KeyboardEvent) => this.onKeydown(e);
19
31
  private _docClickHandler = (e: MouseEvent) => this.onDocumentClick(e);
20
32
  private _featureConfig: EditorFeatureConfig = {};
33
+ private readonly _caretAnchor: VirtualElement = {
34
+ getBoundingClientRect: () => this.caretRect()
35
+ };
21
36
 
22
37
  constructor(quill: Quill, options: { config: EditorFeatureConfig }) {
23
38
  this._quill = quill;
@@ -29,69 +44,56 @@ class ContextMenu {
29
44
  }
30
45
 
31
46
  private initComponent() {
32
- this._component = this.createComponent()!;
33
- this._quill.container.ownerDocument.body.appendChild(this._component);
47
+ const doc = this._quill.container.ownerDocument;
48
+ // The named import above pulls in the slash-menu module, which registers the element
49
+ this._menu = doc.createElement('zn-slash-menu');
50
+ this._menu.heading = 'Options';
51
+ this._caretAnchor.contextElement = this._quill.root;
52
+ this._menu.anchor = this._caretAnchor;
53
+ doc.body.appendChild(this._menu);
34
54
  }
35
55
 
36
56
  private attachEvents() {
37
- this._quill.on(Quill.events.TEXT_CHANGE, () => this.updateFromEditor());
38
57
  this._quill.on(Quill.events.EDITOR_CHANGE, () => this.updateFromEditor());
39
58
  this._quill.root.addEventListener('keydown', this._keydownHandler);
40
- this._component.addEventListener('zn-format-select', (e: Event) => this.onToolbarSelect(e as CustomEvent<ResultItem>));
41
- this._quill.on('editor-change', () => this.positionComponent());
59
+ this._menu.addEventListener(SLASH_ITEM_SELECT, (e: Event) => this.onItemSelect(e as CustomEvent<{ item: ContextMenuItem }>));
42
60
  this._quill.focus();
43
61
  }
44
62
 
45
- private createComponent() {
46
- const tpl = html`
47
- <zn-context-menu></zn-context-menu>`;
48
- return litToHTML<ContextMenuComponent>(tpl);
49
- }
50
-
51
63
  private onDocumentClick(e: MouseEvent) {
52
- const target = e.composedPath ? e.composedPath()[0] as Node : (e.target as Node);
53
- if (!target) return;
54
-
55
- if (!this._component.contains(target) && !this._quill.root.contains(target)) {
64
+ const path = e.composedPath();
65
+ if (!path.includes(this._menu) && !path.includes(this._quill.root)) {
56
66
  this.hide();
57
67
  }
58
68
  }
59
69
 
60
70
  private updateFromEditor() {
61
71
  const info = this.getToolbarQuery();
62
- if (!info) {
72
+ if (!info || info.start === this._dismissedIndex) {
63
73
  this.hide();
64
74
  return;
65
75
  }
66
76
 
67
77
  const {start, formatQuery} = info;
68
- this._startIndex = start;
69
-
70
- try {
71
- const q = formatQuery.toLowerCase();
72
- this._component.results = this._getOptions().filter(it => !q || it.label.toLowerCase().includes(q) || it?.format?.toLowerCase().includes(q));
73
- } catch {
74
- this._component.results = [];
78
+ const matches = filterSlashItems(this._getOptions(), formatQuery);
79
+ if (!matches.length) {
80
+ this.hide();
81
+ return;
75
82
  }
76
83
 
77
- this._component.query = formatQuery;
84
+ this._startIndex = start;
85
+ this._menu.query = formatQuery;
86
+ this._menu.items = matches;
78
87
  this.show();
79
- this.positionComponent();
80
88
  }
81
89
 
82
- private positionComponent() {
83
- if (!this._component || !this._component.open) return;
84
-
85
- const range = this._quill.getSelection();
86
- if (!range) return;
87
-
88
- const bounds = this._quill.getBounds(range.index);
89
- if (!bounds) return;
90
+ private caretRect(): DOMRect {
91
+ const index = this._startIndex >= 0 ? this._startIndex : (this._quill.getSelection()?.index ?? 0);
92
+ const container = this._quill.container.getBoundingClientRect();
93
+ const bounds = this._quill.getBounds(index);
94
+ if (!bounds) return new DOMRect(container.left, container.top, 0, 0);
90
95
 
91
- const editorBounds = this._quill.container.getBoundingClientRect();
92
- const left = Math.max(0, editorBounds.left + bounds.left);
93
- const top = editorBounds.top + bounds.bottom + 4;
94
- this._component.setPosition(left, top);
96
+ return new DOMRect(container.left + bounds.left, container.top + bounds.top, 0, bounds.height);
95
97
  }
96
98
 
97
99
  private getToolbarQuery(): { start: number; formatQuery: string } | null {
@@ -120,61 +122,50 @@ class ContextMenu {
120
122
  }
121
123
 
122
124
  private onKeydown(e: KeyboardEvent) {
123
- if (!this._component?.open) return;
125
+ if (!this._menu.open) return;
124
126
 
125
- if (e.key === 'Escape') {
126
- e.preventDefault();
127
- e.stopPropagation();
128
- this.hide();
129
- return;
130
- }
131
-
132
- if (e.key === 'ArrowDown') {
133
- e.preventDefault();
134
- e.stopPropagation();
135
- const next = (this._component.getActiveIndex?.() ?? -1) + 1;
136
- this._component.setActiveIndex?.(next);
137
- return;
138
- }
127
+ switch (e.key) {
128
+ case 'Escape':
129
+ e.preventDefault();
130
+ e.stopPropagation();
131
+ this._dismissedIndex = this._startIndex;
132
+ this.hide();
133
+ return;
139
134
 
140
- if (e.key === 'ArrowUp') {
141
- e.preventDefault();
142
- e.stopPropagation();
143
- const prev = (this._component.getActiveIndex?.() ?? 0) - 1;
144
- this._component.setActiveIndex?.(prev);
145
- return;
146
- }
135
+ case 'ArrowDown':
136
+ e.preventDefault();
137
+ e.stopPropagation();
138
+ this._menu.moveActive(1);
139
+ return;
147
140
 
148
- if (e.key === 'Enter') {
149
- const idx = this._component.getActiveIndex?.();
150
- const results = (this._component.results as ResultItem[]) || [];
151
- const item = (typeof idx === 'number' && idx >= 0 && idx < results.length) ? results[idx] : undefined;
152
- if (item?.format === 'toolbar' && item?.key) {
141
+ case 'ArrowUp':
153
142
  e.preventDefault();
154
143
  e.stopPropagation();
155
- this._clickToolbarItem(item.key);
144
+ this._menu.moveActive(-1);
156
145
  return;
157
- }
158
- if (item?.format) {
146
+
147
+ case 'Enter': {
148
+ if (!this._menu.activeItem) return;
149
+
159
150
  e.preventDefault();
160
151
  e.stopPropagation();
161
- this._applySelectedFormat(item.format, item.value);
152
+ this._menu.selectActive();
162
153
  }
163
154
  }
164
155
  }
165
156
 
166
- private onToolbarSelect(e: CustomEvent<ResultItem>) {
167
- const key: string | undefined = e.detail?.key as (string | undefined);
168
- if (key) {
169
- this._clickToolbarItem(key);
157
+ private onItemSelect(e: CustomEvent<{ item: ContextMenuItem }>) {
158
+ const item = e.detail?.item;
159
+ if (!item) return;
160
+
161
+ if (item.key) {
162
+ this._clickToolbarItem(item.key);
170
163
  return;
171
164
  }
172
165
 
173
- const format: string = e.detail?.format || '';
174
- const value: string | boolean | undefined = e.detail?.value as (string | boolean | undefined);
175
- if (!format) return;
176
-
177
- this._applySelectedFormat(format, value);
166
+ if (item.format) {
167
+ this._applySelectedFormat(item.format, item.formatValue);
168
+ }
178
169
  }
179
170
 
180
171
  private _clickToolbarItem(key: string) {
@@ -228,8 +219,8 @@ class ContextMenu {
228
219
  }
229
220
  }
230
221
 
231
- private _getOptions(): ResultItem[] {
232
- const options: ResultItem[] = [];
222
+ private _getOptions(): ContextMenuItem[] {
223
+ const options: ContextMenuItem[] = [];
233
224
  let orderCounter = 0;
234
225
 
235
226
  // 1) Quick Actions
@@ -251,11 +242,11 @@ class ContextMenu {
251
242
 
252
243
  if (label && icon) {
253
244
  if (key) {
254
- options.push({icon, label, format: 'toolbar', key: key, order});
245
+ options.push({icon, label, key: key, order});
255
246
  } else if (uri) {
256
- options.push({icon, label, format: 'dialog', value: uri, order});
247
+ options.push({icon, label, format: 'dialog', formatValue: uri, order});
257
248
  } else if (content) {
258
- options.push({icon, label, format: 'insert', value: content, order});
249
+ options.push({icon, label, format: 'insert', formatValue: content, order});
259
250
  }
260
251
  }
261
252
  });
@@ -269,73 +260,72 @@ class ContextMenu {
269
260
  if (!label || !icon || !key) return;
270
261
 
271
262
  const order = typeof tool.order === 'number' ? tool.order : orderCounter++;
272
- options.push({icon, label, format: 'toolbar', key: key, order});
263
+ options.push({icon, label, key: key, order});
273
264
  });
274
265
  }
275
266
 
276
267
  // 2) Built-in Actions (In order they should appear)
277
268
  options.push(
278
- {icon: 'bold@lu', label: 'Bold', format: 'bold', order: orderCounter++},
279
- {icon: 'italic@lu', label: 'Italic', format: 'italic', order: orderCounter++},
280
- {icon: 'underline@lu', label: 'Underline', format: 'underline', order: orderCounter++},
281
- {icon: 'strikethrough@lu', label: 'Strikethrough', format: 'strike', order: orderCounter++},
282
- {icon: 'text-quote@lu', label: 'Blockquote', format: 'blockquote', order: orderCounter++},
269
+ {icon: 'bold@lu', label: 'Bold', format: 'bold', keywords: 'bold', order: orderCounter++},
270
+ {icon: 'italic@lu', label: 'Italic', format: 'italic', keywords: 'italic', order: orderCounter++},
271
+ {icon: 'underline@lu', label: 'Underline', format: 'underline', keywords: 'underline', order: orderCounter++},
272
+ {icon: 'strikethrough@lu', label: 'Strikethrough', format: 'strike', keywords: 'strike', order: orderCounter++},
273
+ {icon: 'text-quote@lu', label: 'Blockquote', format: 'blockquote', keywords: 'blockquote', order: orderCounter++},
283
274
  );
284
275
  if (this._featureConfig.codeEnabled !== false) {
285
- options.push({icon: 'code@lu', label: 'Inline Code', format: 'code', order: orderCounter++});
276
+ options.push({icon: 'code@lu', label: 'Inline Code', format: 'code', keywords: 'code', order: orderCounter++});
286
277
  }
287
278
  if (this._featureConfig.codeBlocksEnabled !== false) {
288
- options.push({icon: 'square-code@lu', label: 'Code Block', format: 'code-block', order: orderCounter++});
279
+ options.push({icon: 'square-code@lu', label: 'Code Block', format: 'code-block', keywords: 'code-block', order: orderCounter++});
289
280
  }
290
281
  options.push(
291
- {icon: 'heading-1@lu', label: 'Heading 1', format: 'header', value: '1', order: orderCounter++},
292
- {icon: 'heading-2@lu', label: 'Heading 2', format: 'header', value: '2', order: orderCounter++},
293
- {icon: 'case-sensitive@lu', label: 'Normal Text', format: 'header', value: '', order: orderCounter++},
294
- {icon: 'list@lu', label: 'Bulleted List', format: 'list', value: 'bullet', order: orderCounter++},
295
- {icon: 'list-ordered@lu', label: 'Numbered List', format: 'list', value: 'ordered', order: orderCounter++},
296
- {icon: 'list-todo@lu', label: 'Checklist', format: 'list', value: 'checked', order: orderCounter++}
282
+ {icon: 'heading-1@lu', label: 'Heading 1', format: 'header', formatValue: '1', keywords: 'header,h1', order: orderCounter++},
283
+ {icon: 'heading-2@lu', label: 'Heading 2', format: 'header', formatValue: '2', keywords: 'header,h2', order: orderCounter++},
284
+ {icon: 'case-sensitive@lu', label: 'Normal Text', format: 'header', formatValue: '', keywords: 'header,paragraph', order: orderCounter++},
285
+ {icon: 'list@lu', label: 'Bulleted List', format: 'list', formatValue: 'bullet', keywords: 'list,bullet', order: orderCounter++},
286
+ {icon: 'list-ordered@lu', label: 'Numbered List', format: 'list', formatValue: 'ordered', keywords: 'list,ordered', order: orderCounter++},
287
+ {icon: 'list-todo@lu', label: 'Checklist', format: 'list', formatValue: 'checked', keywords: 'list,checked,todo', order: orderCounter++}
297
288
  );
298
289
  if (this._featureConfig.linksEnabled !== false) {
299
- options.push({icon: 'link@lu', label: 'Link', format: 'link', value: true, order: orderCounter++});
290
+ options.push({icon: 'link@lu', label: 'Link', format: 'link', formatValue: true, keywords: 'link', order: orderCounter++});
300
291
  }
301
292
  if (this._featureConfig.dividersEnabled !== false) {
302
- options.push({icon: 'minus@lu', label: 'Divider', format: 'divider', order: orderCounter++});
293
+ options.push({icon: 'minus@lu', label: 'Divider', format: 'divider', keywords: 'divider,hr', order: orderCounter++});
303
294
  }
304
295
  if (this._featureConfig.attachmentsEnabled !== false) {
305
- options.push({icon: 'paperclip@lu', label: 'Attachment', format: 'attachment', order: orderCounter++});
296
+ options.push({icon: 'paperclip@lu', label: 'Attachment', format: 'attachment', keywords: 'attachment', order: orderCounter++});
306
297
  }
307
298
  if (this._featureConfig.imagesEnabled !== false) {
308
- options.push({icon: 'image@lu', label: 'Image', format: 'image', order: orderCounter++});
299
+ options.push({icon: 'image@lu', label: 'Image', format: 'image', keywords: 'image', order: orderCounter++});
309
300
  }
310
301
  if (this._featureConfig.videosEnabled !== false) {
311
- options.push({icon: 'video@lu', label: 'Video', format: 'video', order: orderCounter++});
302
+ options.push({icon: 'video@lu', label: 'Video', format: 'video', keywords: 'video', order: orderCounter++});
312
303
  }
313
304
  if (this._featureConfig.datesEnabled !== false) {
314
- options.push({icon: 'calendar@lu', label: 'Date', format: 'date', order: orderCounter++});
305
+ options.push({icon: 'calendar@lu', label: 'Date', format: 'date', keywords: 'date', order: orderCounter++});
315
306
  }
316
- options.push({icon: 'remove-formatting@lu', label: 'Clear Formatting', format: 'clean', order: orderCounter++});
317
-
318
- // Sort options by order property
319
- options.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
307
+ options.push({icon: 'remove-formatting@lu', label: 'Clear Formatting', format: 'clean', keywords: 'clean', order: orderCounter++});
320
308
 
321
309
  return options;
322
310
  }
323
311
 
324
312
  private show() {
325
- if (!this._component.open) {
326
- this._component.show();
327
- this._quill.container.ownerDocument.addEventListener('click', this._docClickHandler);
328
- }
313
+ if (this._menu.open) return;
314
+
315
+ this._menu.show();
316
+ this._quill.container.ownerDocument.addEventListener('click', this._docClickHandler);
329
317
  }
330
318
 
331
319
  private hide() {
332
- this._component.hide();
333
320
  this._startIndex = -1;
321
+ if (!this._menu.open) return;
322
+
323
+ this._menu.hide();
334
324
  this._quill.container.ownerDocument.removeEventListener('click', this._docClickHandler);
335
325
  }
336
326
 
337
327
  public isOpen() {
338
- return this._component?.open;
328
+ return this._menu?.open ?? false;
339
329
  }
340
330
  }
341
331
 
@@ -331,7 +331,10 @@ class Toolbar extends QuillToolbar {
331
331
  const isVisible = (el: HTMLElement | null) => (el?.offsetParent !== null);
332
332
 
333
333
  if (isVisible(button)) {
334
- button!.click();
334
+ // zn-button overrides click() without dispatching a DOM event, so the
335
+ // dropdown's trigger listener never hears it — open the dropdown directly
336
+ const dateDropdown = shadow.querySelector('zn-dropdown.toolbar__date-dropdown') as ZnDropdown | null;
337
+ dateDropdown?.show().then();
335
338
  return;
336
339
  }
337
340
 
@@ -228,6 +228,7 @@ export default class ZnSlashMenu extends ZincElement {
228
228
  // mousedown, not click: preventDefault keeps focus (and the caret) in the field
229
229
  private readonly handleItemMouseDown = (event: MouseEvent) => {
230
230
  event.preventDefault();
231
+ event.stopPropagation();
231
232
 
232
233
  const index = Number((event.currentTarget as HTMLElement).dataset.index);
233
234
  const item = this.visibleItems[index];
@@ -63,10 +63,22 @@ describe('<zn-slash-menu>', () => {
63
63
 
64
64
  const item = el.shadowRoot!.querySelector<HTMLButtonElement>('[data-slash-item]')!;
65
65
  const event = new MouseEvent('mousedown', {bubbles: true, cancelable: true, composed: true});
66
- item.dispatchEvent(event);
66
+
67
+ let reachedDocument = false;
68
+ const documentListener = () => {
69
+ reachedDocument = true;
70
+ };
71
+ document.addEventListener('mousedown', documentListener);
72
+ try {
73
+ item.dispatchEvent(event);
74
+ } finally {
75
+ document.removeEventListener('mousedown', documentListener);
76
+ }
67
77
 
68
78
  expect(selected).to.deep.equal(['Brand name']);
69
79
  expect(event.defaultPrevented, 'mousedown is prevented so the field keeps focus').to.be.true;
80
+ // A selection may open another overlay; the mousedown must not leak to its outside-click dismisser
81
+ expect(reachedDocument, 'mousedown does not bubble to document').to.be.false;
70
82
  });
71
83
 
72
84
  it('reports how many matches were not rendered', async () => {
@@ -888,7 +888,6 @@ export default class ZnThemeEditor extends ZincElement {
888
888
  ${this._sourcesSafe().length > 0 ? html`
889
889
  <zn-select
890
890
  class="editor__sources"
891
- size="small"
892
891
  label="Preview source"
893
892
  hoist
894
893
  .value="${String(this._sourceIndex)}"
@@ -262,7 +262,6 @@
262
262
 
263
263
  .editor__sources {
264
264
  width: auto;
265
- max-width: 160px;
266
265
 
267
266
  // Accessible name only - the label text stays out of the compact toolbar row.
268
267
  &::part(form-control-label) {
@@ -1,116 +0,0 @@
1
- import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
2
- import {property, state} from 'lit/decorators.js';
3
- import ZincElement from "../../../../internal/zinc-element";
4
-
5
- import styles from './context-menu.scss';
6
-
7
- export interface ResultItem {
8
- icon: string;
9
- label: string;
10
- format?: string;
11
- key?: string;
12
- value?: string | boolean;
13
- order?: number;
14
- }
15
-
16
- export default class ContextMenuComponent extends ZincElement {
17
- static styles: CSSResultGroup = unsafeCSS(styles);
18
-
19
- @property({type: Boolean, reflect: true}) open = false;
20
- @property({type: String}) query = '';
21
- @property({type: Array}) results: ResultItem[] = [];
22
-
23
- @state() private _activeIndex = -1;
24
-
25
- public show() {
26
- this.open = true;
27
- }
28
-
29
- public hide() {
30
- this.open = false;
31
- this._activeIndex = -1;
32
- }
33
-
34
- setPosition(left: number, top: number) {
35
- this.style.left = `${Math.max(0, left)}px`;
36
- this.style.top = `${top}px`;
37
- }
38
-
39
- setActiveIndex(index: number) {
40
- const len = this.results?.length ?? 0;
41
- if (!len) {
42
- this._activeIndex = -1;
43
- return;
44
- }
45
-
46
- if (index < 0) {
47
- index = len - 1;
48
- }
49
- if (index >= len) {
50
- index = 0;
51
- }
52
- this._activeIndex = index;
53
- this.requestUpdate();
54
-
55
- requestAnimationFrame(() => {
56
- const items = Array.from(this.renderRoot.querySelectorAll<HTMLButtonElement>('[data-toolbar-option]'));
57
- const active = items[this._activeIndex];
58
- active?.scrollIntoView?.({block: 'nearest'});
59
- });
60
- }
61
-
62
- getActiveIndex() {
63
- return this._activeIndex;
64
- }
65
-
66
- private _onClickItem = (e: MouseEvent) => {
67
- const target = e.currentTarget as HTMLElement | null;
68
- if (!target) return;
69
-
70
- const idx = parseInt(target.dataset.index || '-1', 10);
71
- if (Number.isNaN(idx)) return;
72
-
73
- this.setActiveIndex(idx);
74
-
75
- const item = this.results?.[idx];
76
- if (!item) return;
77
-
78
- this.dispatchEvent(new CustomEvent('zn-format-select', {
79
- bubbles: true,
80
- composed: true,
81
- detail: {icon: item.icon, label: item.label, format: item.format, value: item.value, key: item.key}
82
- }));
83
- }
84
-
85
- protected willUpdate(changed: PropertyValues) {
86
- if (changed.has('results')) {
87
- // Reset active index when results change
88
- this._activeIndex = this.results?.length ? 0 : -1;
89
- }
90
- }
91
-
92
- render() {
93
- return html`
94
- <div class="header">${this.query ? `Search: ${this.query}` : 'Options'}</div>
95
- ${Array.isArray(this.results) && this.results.length > 0 ? (
96
- this.results.slice(0, 20).map((res, i) => html`
97
- <button
98
- type="button"
99
- class="item"
100
- role="option"
101
- aria-selected="${String(i === this._activeIndex)}"
102
- data-toolbar-option
103
- data-index="${i}"
104
- @click="${this._onClickItem}"
105
- >
106
- <zn-icon src="${res.icon}" size="16"></zn-icon>
107
- <span class="label">${res.label}</span>
108
- </button>
109
- `)
110
- ) : html`
111
- <div class="empty">${this.query ? 'No results' : 'Type something'}</div>`}
112
- `;
113
- }
114
- }
115
-
116
- ContextMenuComponent.define('zn-context-menu');