@kubex/zinc 1.1.79 → 1.1.81
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/custom-elements.json +1398 -109
- package/dist/vscode.html-custom-data.json +202 -3
- package/dist/web-types.json +478 -4
- package/dist/zn.d.ts +467 -8
- package/dist/zn.min.js +388 -316
- package/docs/pages/components/inline-edit.md +22 -0
- package/docs/pages/components/input.md +21 -0
- package/docs/pages/components/remarkd-editor.md +43 -0
- package/docs/pages/components/slash-item.md +126 -0
- package/docs/pages/components/slash-menu.md +161 -0
- package/docs/pages/components/textarea.md +148 -0
- package/docs/pages/components/translations.md +34 -0
- package/package.json +1 -1
- package/src/components/inline-edit/inline-edit.component.ts +36 -0
- package/src/components/inline-edit/inline-edit.test.ts +83 -1
- package/src/components/input/input.component.ts +143 -0
- package/src/components/input/input.test.ts +113 -1
- package/src/components/remarkd-editor/remarkd-editor.component.ts +157 -95
- package/src/components/remarkd-editor/remarkd-editor.scss +24 -36
- package/src/components/remarkd-editor/remarkd-editor.test.ts +110 -6
- package/src/components/slash-item/index.ts +12 -0
- package/src/components/slash-item/slash-item.component.ts +76 -0
- package/src/components/slash-item/slash-item.scss +5 -0
- package/src/components/slash-menu/index.ts +14 -0
- package/src/components/slash-menu/slash-menu-controller.ts +361 -0
- package/src/components/slash-menu/slash-menu-items.ts +122 -0
- package/src/components/slash-menu/slash-menu.component.ts +327 -0
- package/src/components/slash-menu/slash-menu.scss +120 -0
- package/src/components/slash-menu/slash-menu.test.ts +154 -0
- package/src/components/textarea/textarea.component.ts +143 -0
- package/src/components/textarea/textarea.test.ts +310 -2
- package/src/components/translations/translations.component.ts +31 -0
- package/src/components/translations/translations.test.ts +89 -1
- package/src/events/events.ts +2 -0
- package/src/events/zn-slash-insert.ts +9 -0
- package/src/events/zn-slash-select.ts +9 -0
- package/src/utilities/caret-position.ts +118 -0
- package/src/zinc.ts +3 -0
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
-
import { expect, fixture, html } from '@open-wc/testing';
|
|
2
|
+
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
|
3
3
|
import type ZnInlineEdit from './inline-edit.component';
|
|
4
4
|
import type ZnSelect from '../select/select.component';
|
|
5
|
+
import type ZnSlashMenu from '../slash-menu/slash-menu.component';
|
|
6
|
+
import type ZnTextarea from '../textarea/textarea.component';
|
|
7
|
+
|
|
8
|
+
// The auto-resizing textarea's ResizeObserver can emit a benign "loop completed with undelivered
|
|
9
|
+
// notifications" warning while the slash menu is positioned. It's not a real error — ignore it so the
|
|
10
|
+
// test runner doesn't treat it as an uncaught exception (capture phase runs before the runner's).
|
|
11
|
+
window.addEventListener('error', (e: ErrorEvent) => {
|
|
12
|
+
if (typeof e.message === 'string' && e.message.includes('ResizeObserver loop')) {
|
|
13
|
+
e.stopImmediatePropagation();
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
}
|
|
16
|
+
}, true);
|
|
5
17
|
|
|
6
18
|
describe('<zn-inline-edit>', () => {
|
|
7
19
|
it('should render a component', async () => {
|
|
@@ -483,4 +495,74 @@ describe('<zn-inline-edit>', () => {
|
|
|
483
495
|
expect(el.shadowRoot!.querySelector('.ai--editing')).to.not.exist;
|
|
484
496
|
expect(el.value).to.equal('real@example.com');
|
|
485
497
|
});
|
|
498
|
+
|
|
499
|
+
// -- Slash menu --
|
|
500
|
+
|
|
501
|
+
describe('slash menu', () => {
|
|
502
|
+
async function openSlashMenu(el: ZnInlineEdit) {
|
|
503
|
+
await el.updateComplete;
|
|
504
|
+
|
|
505
|
+
el.shadowRoot!.querySelector<HTMLElement>('.ai__left')!
|
|
506
|
+
.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true}));
|
|
507
|
+
await el.updateComplete;
|
|
508
|
+
|
|
509
|
+
const textarea = el.shadowRoot!.querySelector<ZnTextarea>('zn-textarea')!;
|
|
510
|
+
await textarea.updateComplete;
|
|
511
|
+
|
|
512
|
+
textarea.focus();
|
|
513
|
+
textarea.input.value = '/';
|
|
514
|
+
textarea.input.setSelectionRange(1, 1);
|
|
515
|
+
textarea.input.dispatchEvent(new InputEvent('input', {bubbles: true, composed: true}));
|
|
516
|
+
|
|
517
|
+
const menuOf = () => textarea.shadowRoot!.querySelector<ZnSlashMenu>('zn-slash-menu');
|
|
518
|
+
await waitUntil(() => menuOf()?.open, 'the slash menu never opened');
|
|
519
|
+
|
|
520
|
+
return {textarea, menu: menuOf()!};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
it('offers its slash items on the inner textarea', async () => {
|
|
524
|
+
const el = await fixture<ZnInlineEdit>(html`
|
|
525
|
+
<zn-inline-edit input-type="textarea"
|
|
526
|
+
slash-items="Brand name={{BRAND_NAME}}, Support email={{SUPPORT_EMAIL}}"></zn-inline-edit>`);
|
|
527
|
+
const {menu} = await openSlashMenu(el);
|
|
528
|
+
|
|
529
|
+
expect(menu.items.map(item => item.label)).to.deep.equal(['Brand name', 'Support email']);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('forwards a custom trigger', async () => {
|
|
533
|
+
const el = await fixture<ZnInlineEdit>(html`
|
|
534
|
+
<zn-inline-edit input-type="textarea" slash-trigger="{{"
|
|
535
|
+
slash-items="Brand name={{BRAND_NAME}}"></zn-inline-edit>`);
|
|
536
|
+
await el.updateComplete;
|
|
537
|
+
|
|
538
|
+
const textarea = el.shadowRoot!.querySelector<ZnTextarea>('zn-textarea')!;
|
|
539
|
+
expect(textarea.slashTrigger).to.equal('{{');
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('does not submit the form when Enter chooses an item', async () => {
|
|
543
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
544
|
+
<form>
|
|
545
|
+
<zn-inline-edit input-type="textarea" slash-items="Brand name={{BRAND_NAME}}"></zn-inline-edit>
|
|
546
|
+
</form>`);
|
|
547
|
+
const el = form.querySelector<ZnInlineEdit>('zn-inline-edit')!;
|
|
548
|
+
|
|
549
|
+
let submits = 0;
|
|
550
|
+
form.addEventListener('submit', event => {
|
|
551
|
+
event.preventDefault();
|
|
552
|
+
submits++;
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const {textarea} = await openSlashMenu(el);
|
|
556
|
+
textarea.input.dispatchEvent(new KeyboardEvent('keydown', {
|
|
557
|
+
key: 'Enter',
|
|
558
|
+
bubbles: true,
|
|
559
|
+
composed: true,
|
|
560
|
+
cancelable: true
|
|
561
|
+
}));
|
|
562
|
+
await el.updateComplete;
|
|
563
|
+
|
|
564
|
+
expect(el.value, 'the token should have replaced the trigger').to.equal('{{BRAND_NAME}}');
|
|
565
|
+
expect(submits, 'Enter belongs to the open menu, not the form').to.equal(0);
|
|
566
|
+
});
|
|
567
|
+
});
|
|
486
568
|
});
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import {classMap} from "lit/directives/class-map.js";
|
|
2
2
|
import {defaultValue} from "../../internal/default-value";
|
|
3
3
|
import {FormControlController} from "../../internal/form";
|
|
4
|
+
import {getSlashMenuPreset, parseSlashItems, type SlashMenuItem} from "../slash-menu/slash-menu-items";
|
|
4
5
|
import {HasSlotController} from "../../internal/slot";
|
|
5
6
|
import {html, unsafeCSS} from 'lit';
|
|
6
7
|
import {ifDefined} from "lit/directives/if-defined.js";
|
|
7
8
|
import {live} from "lit/directives/live.js";
|
|
8
9
|
import {LocalizeController} from "../../utilities/localize";
|
|
9
10
|
import {property, query, state} from 'lit/decorators.js';
|
|
11
|
+
import {SlashMenuController} from "../slash-menu/slash-menu-controller";
|
|
10
12
|
import {watch} from "../../internal/watch";
|
|
11
13
|
import ZincElement from '../../internal/zinc-element';
|
|
12
14
|
import ZnIcon from "../icon";
|
|
15
|
+
import ZnSlashItem from "../slash-item";
|
|
16
|
+
import ZnSlashMenu from "../slash-menu";
|
|
13
17
|
import ZnTooltip from "../tooltip";
|
|
14
18
|
import type {ZincFormControl} from '../../internal/zinc-element';
|
|
15
19
|
|
|
@@ -40,8 +44,20 @@ import styles from './input.scss';
|
|
|
40
44
|
* @slot show-password-icon - An icon to use in lieu of the default show password icon.
|
|
41
45
|
* @slot hide-password-icon - An icon to use in lieu of the default hide password icon.
|
|
42
46
|
* @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
|
|
47
|
+
* @slot slash-items - `<zn-slash-item>` elements describing the insertions offered by the slash menu. Items are
|
|
48
|
+
* picked up wherever they sit inside the input, so this slot name is optional.
|
|
49
|
+
* @slot slash-menu - A `<zn-slash-menu>` to use instead of the built-in one, so its heading, sizing and styling can
|
|
50
|
+
* be set in markup. Any `<zn-slash-item>` children of it become the menu's items.
|
|
51
|
+
*
|
|
52
|
+
* @dependency zn-slash-item
|
|
53
|
+
* @dependency zn-slash-menu
|
|
54
|
+
*
|
|
55
|
+
* @event zn-slash-select - Emitted when a slash menu item is chosen. Cancelable — call `preventDefault()` to
|
|
56
|
+
* suppress the insertion and handle the item yourself.
|
|
57
|
+
* @event zn-slash-insert - Emitted after a slash menu item's value has been inserted.
|
|
43
58
|
*
|
|
44
59
|
* @csspart form-control - The form control that wraps the label, input, and help text.
|
|
60
|
+
* @csspart slash-menu - The slash menu shown at the caret.
|
|
45
61
|
* @csspart form-control-label - The label's wrapper.
|
|
46
62
|
* @csspart form-control-input - The input's wrapper.
|
|
47
63
|
* @csspart form-control-help-text - The help text's wrapper.
|
|
@@ -62,6 +78,8 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
62
78
|
static styles = unsafeCSS(styles);
|
|
63
79
|
static dependencies = {
|
|
64
80
|
'zn-icon': ZnIcon,
|
|
81
|
+
'zn-slash-item': ZnSlashItem,
|
|
82
|
+
'zn-slash-menu': ZnSlashMenu,
|
|
65
83
|
'zn-tooltip': ZnTooltip
|
|
66
84
|
}
|
|
67
85
|
|
|
@@ -71,10 +89,24 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
71
89
|
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label', 'label-tooltip');
|
|
72
90
|
private readonly localize = new LocalizeController(this);
|
|
73
91
|
|
|
92
|
+
private readonly slashController = new SlashMenuController(this, {
|
|
93
|
+
menu: () => this.mountSlashMenu(),
|
|
94
|
+
items: search => this.resolveSlashItems(search),
|
|
95
|
+
trigger: () => this.slashTrigger,
|
|
96
|
+
onSelect: (item, search) => !this.emit('zn-slash-select', {
|
|
97
|
+
detail: {item, query: search}
|
|
98
|
+
}).defaultPrevented,
|
|
99
|
+
onInsert: (item, value) => this.emit('zn-slash-insert', {detail: {item, value}})
|
|
100
|
+
});
|
|
101
|
+
|
|
74
102
|
@query('.input__control') input: HTMLInputElement;
|
|
75
103
|
@query('.input__color-picker') colorPicker: HTMLInputElement;
|
|
104
|
+
@query('zn-slash-menu') slashMenuElement: ZnSlashMenu | null;
|
|
76
105
|
|
|
77
106
|
@state() private hasFocus = false;
|
|
107
|
+
|
|
108
|
+
/** Renders the slash menu only once it has been needed, keeping unused inputs cheap. */
|
|
109
|
+
@state() private hasSlashMenu = false;
|
|
78
110
|
@state() private isUserTyping = false;
|
|
79
111
|
@property() title = "" // make reactive pass through
|
|
80
112
|
|
|
@@ -224,6 +256,35 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
224
256
|
*/
|
|
225
257
|
@property() inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
|
|
226
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Quick insertions offered by the slash menu. Accepts a JSON array of items, or the shorthand
|
|
261
|
+
* `Brand name={{BRAND_NAME}}, Support email={{SUPPORT_EMAIL}}`. Can also be set as an array of
|
|
262
|
+
* `SlashMenuItem` objects in JavaScript.
|
|
263
|
+
*/
|
|
264
|
+
@property({
|
|
265
|
+
attribute: 'slash-items',
|
|
266
|
+
converter: {
|
|
267
|
+
fromAttribute: (value: string) => parseSlashItems(value),
|
|
268
|
+
toAttribute: (value: SlashMenuItem[]) => JSON.stringify(value)
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
slashItems: SlashMenuItem[] = [];
|
|
272
|
+
|
|
273
|
+
/** Names of item sets registered with `registerSlashMenuPreset`, comma separated. */
|
|
274
|
+
@property({attribute: 'slash-preset'}) slashPreset = '';
|
|
275
|
+
|
|
276
|
+
/** The characters that open the slash menu. */
|
|
277
|
+
@property({attribute: 'slash-trigger'}) slashTrigger = '/';
|
|
278
|
+
|
|
279
|
+
/** The heading shown above the slash menu's items. */
|
|
280
|
+
@property({attribute: 'slash-heading'}) slashHeading = 'Insert';
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Resolves additional items each time the menu opens, for lists that come from elsewhere (e.g. an
|
|
284
|
+
* API). Receives the current query and may return a promise. JavaScript only.
|
|
285
|
+
*/
|
|
286
|
+
@property({attribute: false}) slashItemsProvider?: (query: string) => SlashMenuItem[] | Promise<SlashMenuItem[]>;
|
|
287
|
+
|
|
227
288
|
/**
|
|
228
289
|
* When enabled, pressing enter will always submit the surrounding form, even when the form uses
|
|
229
290
|
* enter-navigation to move between fields.
|
|
@@ -565,6 +626,10 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
565
626
|
}
|
|
566
627
|
|
|
567
628
|
private handleKeyDown(event: KeyboardEvent) {
|
|
629
|
+
// The slash menu claims its own keys; while it is open Escape closes it and Enter picks an item,
|
|
630
|
+
// rather than blurring the field or submitting the form
|
|
631
|
+
if (this.slashController.open) return;
|
|
632
|
+
|
|
568
633
|
if (event.key === 'Escape') {
|
|
569
634
|
this.input.blur();
|
|
570
635
|
event.stopPropagation();
|
|
@@ -632,6 +697,78 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
632
697
|
if (this.type === 'range' && this.value === '') {
|
|
633
698
|
this.value = this.input.value;
|
|
634
699
|
}
|
|
700
|
+
|
|
701
|
+
this.slashController.attach(this.input);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** The insertions the slash menu offers, gathered from every source, in the order they were declared. */
|
|
705
|
+
async resolveSlashItems(search = ''): Promise<SlashMenuItem[]> {
|
|
706
|
+
const items = [
|
|
707
|
+
...getSlashMenuPreset(this.slashPreset),
|
|
708
|
+
...this.slashItems,
|
|
709
|
+
...this.slottedSlashItems()
|
|
710
|
+
];
|
|
711
|
+
|
|
712
|
+
const provided = await this.slashItemsProvider?.(search);
|
|
713
|
+
return provided ? [...items, ...provided] : items;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** Opens the slash menu at the caret, inserting the trigger if it isn't already there. */
|
|
717
|
+
async showSlashMenu() {
|
|
718
|
+
this.focus();
|
|
719
|
+
|
|
720
|
+
const caret = this.input.selectionStart ?? this.input.value.length;
|
|
721
|
+
const precedes = this.input.value.slice(caret - this.slashTrigger.length, caret);
|
|
722
|
+
if (precedes !== this.slashTrigger) {
|
|
723
|
+
this.setRangeText(this.slashTrigger, caret, caret, 'end');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
await this.updateComplete;
|
|
727
|
+
this.slashController.requestOpen();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** Closes the slash menu. */
|
|
731
|
+
hideSlashMenu() {
|
|
732
|
+
this.slashController.close();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
private slottedSlashItems(): SlashMenuItem[] {
|
|
736
|
+
const elements = [...this.querySelectorAll<ZnSlashItem>('zn-slash-item')];
|
|
737
|
+
|
|
738
|
+
return elements.map(element => {
|
|
739
|
+
// Elements from a different zinc build may not have upgraded, so fall back to their markup
|
|
740
|
+
if (typeof element.toSlashMenuItem === 'function') return element.toSlashMenuItem();
|
|
741
|
+
|
|
742
|
+
const value = element.getAttribute('value') ?? (element.textContent ?? '').trim();
|
|
743
|
+
const order = element.getAttribute('order');
|
|
744
|
+
const caretOffset = element.getAttribute('caret-offset');
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
label: element.getAttribute('label') ?? value,
|
|
748
|
+
value,
|
|
749
|
+
icon: element.getAttribute('icon') ?? undefined,
|
|
750
|
+
description: element.getAttribute('description') ?? undefined,
|
|
751
|
+
keywords: element.getAttribute('keywords') ?? undefined,
|
|
752
|
+
group: element.getAttribute('group') ?? undefined,
|
|
753
|
+
action: element.getAttribute('action') ?? undefined,
|
|
754
|
+
order: order === null ? undefined : Number(order),
|
|
755
|
+
caretOffset: caretOffset === null ? undefined : Number(caretOffset),
|
|
756
|
+
disabled: element.hasAttribute('disabled')
|
|
757
|
+
};
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private async mountSlashMenu(): Promise<ZnSlashMenu | null> {
|
|
762
|
+
// A slotted menu is the author's own: it keeps its own heading, sizing and styling
|
|
763
|
+
const slotted = this.querySelector<ZnSlashMenu>('zn-slash-menu');
|
|
764
|
+
if (slotted) return slotted;
|
|
765
|
+
|
|
766
|
+
if (!this.hasSlashMenu) {
|
|
767
|
+
this.hasSlashMenu = true;
|
|
768
|
+
await this.updateComplete;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return this.slashMenuElement;
|
|
635
772
|
}
|
|
636
773
|
|
|
637
774
|
@watch('disabled', {waitUntilFirstUpdate: true})
|
|
@@ -958,6 +1095,12 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
958
1095
|
: ''}
|
|
959
1096
|
</span>
|
|
960
1097
|
</div>
|
|
1098
|
+
${this.hasSlashMenu
|
|
1099
|
+
? html`
|
|
1100
|
+
<zn-slash-menu part="slash-menu" heading=${this.slashHeading}></zn-slash-menu>`
|
|
1101
|
+
: ''}
|
|
1102
|
+
<slot name="slash-menu"></slot>
|
|
1103
|
+
<slot name="slash-items" hidden></slot>
|
|
961
1104
|
</div>
|
|
962
1105
|
|
|
963
1106
|
<div
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
-
import {expect, fixture, html} from '@open-wc/testing';
|
|
2
|
+
import {expect, fixture, html, waitUntil} from '@open-wc/testing';
|
|
3
3
|
import {clearFormStoreValues} from '../../utilities/form';
|
|
4
4
|
import type ZnInput from './input.component';
|
|
5
|
+
import type ZnSlashMenu from '../slash-menu/slash-menu.component';
|
|
5
6
|
|
|
6
7
|
describe('<zn-input>', () => {
|
|
7
8
|
it('should render a component', async () => {
|
|
@@ -217,4 +218,115 @@ describe('<zn-input>', () => {
|
|
|
217
218
|
expect(contextNote(el)).to.equal('Custom');
|
|
218
219
|
});
|
|
219
220
|
});
|
|
221
|
+
|
|
222
|
+
describe('slash menu', () => {
|
|
223
|
+
function slashMenuOf(el: ZnInput) {
|
|
224
|
+
return el.shadowRoot!.querySelector<ZnSlashMenu>('zn-slash-menu');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Types `text` into the field the way a user would, leaving the caret at the end. */
|
|
228
|
+
function type(el: ZnInput, text: string) {
|
|
229
|
+
el.input.value = text;
|
|
230
|
+
el.input.setSelectionRange(text.length, text.length);
|
|
231
|
+
el.input.dispatchEvent(new InputEvent('input', {bubbles: true, composed: true}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function openSlashMenu(el: ZnInput, text = '/') {
|
|
235
|
+
await el.updateComplete;
|
|
236
|
+
el.focus();
|
|
237
|
+
type(el, text);
|
|
238
|
+
await waitUntil(() => slashMenuOf(el)?.open, 'the slash menu never opened');
|
|
239
|
+
|
|
240
|
+
return slashMenuOf(el)!;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
it('stays closed when no items are configured', async () => {
|
|
244
|
+
const el = await fixture<ZnInput>(html`<zn-input></zn-input>`);
|
|
245
|
+
await el.updateComplete;
|
|
246
|
+
|
|
247
|
+
el.focus();
|
|
248
|
+
type(el, '/');
|
|
249
|
+
await el.updateComplete;
|
|
250
|
+
await el.updateComplete;
|
|
251
|
+
|
|
252
|
+
expect(slashMenuOf(el)?.open ?? false, 'nothing to insert, so nothing to show').to.be.false;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('opens on the trigger with items from the slash-items attribute', async () => {
|
|
256
|
+
const el = await fixture<ZnInput>(html`
|
|
257
|
+
<zn-input slash-items="Brand name={{BRAND_NAME}}, Support email={{SUPPORT_EMAIL}}"></zn-input>`);
|
|
258
|
+
const menu = await openSlashMenu(el);
|
|
259
|
+
|
|
260
|
+
expect(menu.items.map(item => item.label)).to.deep.equal(['Brand name', 'Support email']);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('collects items from slotted zn-slash-item elements', async () => {
|
|
264
|
+
const el = await fixture<ZnInput>(html`
|
|
265
|
+
<zn-input>
|
|
266
|
+
<zn-slash-item label="Brand name" value="{{BRAND_NAME}}"></zn-slash-item>
|
|
267
|
+
</zn-input>`);
|
|
268
|
+
const menu = await openSlashMenu(el);
|
|
269
|
+
|
|
270
|
+
expect(menu.items.map(item => item.label)).to.deep.equal(['Brand name']);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('inserts the active item on Enter, replacing the trigger and query', async () => {
|
|
274
|
+
const el = await fixture<ZnInput>(html`
|
|
275
|
+
<zn-input slash-items="Brand name={{BRAND_NAME}}"></zn-input>`);
|
|
276
|
+
await openSlashMenu(el, 'Welcome to /bra');
|
|
277
|
+
|
|
278
|
+
el.input.dispatchEvent(new KeyboardEvent('keydown', {
|
|
279
|
+
key: 'Enter',
|
|
280
|
+
bubbles: true,
|
|
281
|
+
composed: true,
|
|
282
|
+
cancelable: true
|
|
283
|
+
}));
|
|
284
|
+
await el.updateComplete;
|
|
285
|
+
|
|
286
|
+
expect(el.value).to.equal('Welcome to {{BRAND_NAME}}');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('does not submit the form when Enter chooses an item', async () => {
|
|
290
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
291
|
+
<form>
|
|
292
|
+
<zn-input slash-items="Brand name={{BRAND_NAME}}"></zn-input>
|
|
293
|
+
</form>`);
|
|
294
|
+
const el = form.querySelector<ZnInput>('zn-input')!;
|
|
295
|
+
|
|
296
|
+
let submits = 0;
|
|
297
|
+
form.addEventListener('submit', event => {
|
|
298
|
+
event.preventDefault();
|
|
299
|
+
submits++;
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await openSlashMenu(el);
|
|
303
|
+
el.input.dispatchEvent(new KeyboardEvent('keydown', {
|
|
304
|
+
key: 'Enter',
|
|
305
|
+
bubbles: true,
|
|
306
|
+
composed: true,
|
|
307
|
+
cancelable: true
|
|
308
|
+
}));
|
|
309
|
+
await el.updateComplete;
|
|
310
|
+
|
|
311
|
+
expect(el.value, 'the token should have replaced the trigger').to.equal('{{BRAND_NAME}}');
|
|
312
|
+
expect(submits, 'Enter belongs to the open menu, not the form').to.equal(0);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('closes on Escape without blurring the field', async () => {
|
|
316
|
+
const el = await fixture<ZnInput>(html`
|
|
317
|
+
<zn-input slash-items="Brand name={{BRAND_NAME}}"></zn-input>`);
|
|
318
|
+
const menu = await openSlashMenu(el);
|
|
319
|
+
|
|
320
|
+
el.input.dispatchEvent(new KeyboardEvent('keydown', {
|
|
321
|
+
key: 'Escape',
|
|
322
|
+
bubbles: true,
|
|
323
|
+
composed: true,
|
|
324
|
+
cancelable: true
|
|
325
|
+
}));
|
|
326
|
+
await el.updateComplete;
|
|
327
|
+
|
|
328
|
+
expect(menu.open, 'Escape should close the menu').to.be.false;
|
|
329
|
+
expect(el.shadowRoot!.activeElement, 'the field should keep focus').to.equal(el.input);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
220
332
|
});
|