@kubex/zinc 1.1.80 → 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 +392 -122
- package/dist/vscode.html-custom-data.json +40 -15
- package/dist/web-types.json +109 -29
- package/dist/zn.d.ts +372 -297
- package/dist/zn.min.js +282 -265
- package/docs/pages/components/inline-edit.md +3 -2
- package/docs/pages/components/input.md +21 -0
- package/docs/pages/components/remarkd-editor.md +43 -0
- package/docs/pages/components/slash-menu.md +7 -14
- package/docs/pages/components/translations.md +3 -3
- package/package.json +1 -1
- package/src/components/inline-edit/inline-edit.component.ts +7 -2
- 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-menu/slash-menu.component.ts +23 -1
- package/src/components/translations/translations.component.ts +1 -1
|
@@ -122,8 +122,9 @@ Use `input-type="textarea"` for multi-line text content.
|
|
|
122
122
|
{% raw %}
|
|
123
123
|
|
|
124
124
|
`slash-items`, `slash-preset`, `slash-trigger` and `slash-heading` are forwarded to the inner
|
|
125
|
-
[`zn-textarea`](/components/textarea#slash-menu-quick-insertions)
|
|
126
|
-
|
|
125
|
+
[`zn-textarea`](/components/textarea#slash-menu-quick-insertions) or
|
|
126
|
+
[`zn-input`](/components/input#slash-menu-quick-insertions), so typing `/` offers replacement strings at the caret on
|
|
127
|
+
both. The menu claims `Enter` and `Escape` while it is open, so choosing an item neither submits nor cancels the edit.
|
|
127
128
|
|
|
128
129
|
```html:preview
|
|
129
130
|
<zn-inline-edit
|
|
@@ -464,6 +464,27 @@ Add the `trigger-submit` attribute to make pressing Enter in the input always su
|
|
|
464
464
|
</script>
|
|
465
465
|
```
|
|
466
466
|
|
|
467
|
+
### Slash Menu Quick Insertions
|
|
468
|
+
|
|
469
|
+
{% raw %}
|
|
470
|
+
|
|
471
|
+
Typing `/` opens a menu at the caret, exactly as it does on
|
|
472
|
+
[`zn-textarea`](/components/textarea#slash-menu-quick-insertions) — useful for short single-line copy that carries
|
|
473
|
+
replacement strings, such as a headline or a button label. The same `slash-items`, `slash-preset`, `slash-trigger` and
|
|
474
|
+
`slash-heading` attributes apply, and `zn-slash-item` children work too.
|
|
475
|
+
|
|
476
|
+
```html:preview
|
|
477
|
+
<zn-input
|
|
478
|
+
label="Confirmation headline"
|
|
479
|
+
help-text="Type / to insert a replacement string"
|
|
480
|
+
slash-items="Brand name={{BRAND_NAME}}, Customer email={{CUSTOMER_EMAIL}}"></zn-input>
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
While the menu is open it claims `↑`, `↓`, `Enter`, `Tab` and `Escape`, so choosing an item does not submit the
|
|
484
|
+
surrounding form the way `Enter` otherwise would.
|
|
485
|
+
|
|
486
|
+
{% endraw %}
|
|
487
|
+
|
|
467
488
|
### Customizing Label Position
|
|
468
489
|
|
|
469
490
|
Use [CSS parts](#css-parts) to customize the way form controls are drawn. This example uses CSS grid to position the
|
|
@@ -42,6 +42,28 @@ block committed empty is removed.
|
|
|
42
42
|
></zn-remarkd-editor>
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
### Slash Menu
|
|
46
|
+
|
|
47
|
+
Typing `/` in an empty block opens a [slash menu](/components/slash-menu) of
|
|
48
|
+
block types at the caret. Keep typing to narrow it, `↑`/`↓` to move, `Enter` or
|
|
49
|
+
`Tab` to insert, `Escape` to dismiss. The menu only appears in a block that is
|
|
50
|
+
nothing but the slash command — a prefix like `## ` is not valid remarkd
|
|
51
|
+
part-way through a line — so `/` in the middle of a sentence stays literal
|
|
52
|
+
text.
|
|
53
|
+
|
|
54
|
+
Choosing a block type replaces the command with its remarkd prefix and leaves
|
|
55
|
+
the caret ready to type: `Heading 2` gives you `## `, `Code` opens a fence with
|
|
56
|
+
the caret inside it, and `Image` opens the upload picker instead of inserting
|
|
57
|
+
text. The same block types are on the toolbar, which inserts them as new blocks
|
|
58
|
+
at the end of the document.
|
|
59
|
+
|
|
60
|
+
```html:preview
|
|
61
|
+
<zn-remarkd-editor
|
|
62
|
+
name="content"
|
|
63
|
+
value="Click the empty space below this block, then type / to see the menu."
|
|
64
|
+
></zn-remarkd-editor>
|
|
65
|
+
```
|
|
66
|
+
|
|
45
67
|
### Images
|
|
46
68
|
|
|
47
69
|
Adding an image shows a `zn-file` drop area inline, at the point in the
|
|
@@ -92,6 +114,27 @@ with a blank line inside
|
|
|
92
114
|
></zn-remarkd-editor>
|
|
93
115
|
````
|
|
94
116
|
|
|
117
|
+
### Raw Source
|
|
118
|
+
|
|
119
|
+
Add `allow-raw` for a toolbar toggle that swaps the block view for the whole
|
|
120
|
+
document as editable remarkd source. Useful for pasting in a finished document
|
|
121
|
+
or fixing syntax the block view makes awkward. Toggling back re-splits the
|
|
122
|
+
source into blocks, normalising the spacing between them to a single blank
|
|
123
|
+
line. The toggle is only offered on editable editors — `readonly` and
|
|
124
|
+
`disabled` instances hide the toolbar entirely.
|
|
125
|
+
|
|
126
|
+
```html:preview
|
|
127
|
+
<zn-remarkd-editor
|
|
128
|
+
name="content"
|
|
129
|
+
allow-raw
|
|
130
|
+
value="# Raw Source
|
|
131
|
+
|
|
132
|
+
Hit the toggle at the right of the toolbar to edit this whole document at once.
|
|
133
|
+
|
|
134
|
+
NOTE: The value is the same remarkd source either way."
|
|
135
|
+
></zn-remarkd-editor>
|
|
136
|
+
```
|
|
137
|
+
|
|
95
138
|
### Form Integration
|
|
96
139
|
|
|
97
140
|
`zn-remarkd-editor` is a [form control](/getting-started/form-controls); its
|
|
@@ -113,20 +113,13 @@ The menu doesn't listen for keys itself — whatever owns the field decides whic
|
|
|
113
113
|
scrolls when its content exceeds `--slash-menu-max-height`.
|
|
114
114
|
|
|
115
115
|
```html:preview
|
|
116
|
-
<zn-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
await customElements.whenDefined('zn-slash-menu');
|
|
124
|
-
|
|
125
|
-
menu.items = Array.from({length: 9}, (_, i) => ({label: `Replacement ${i + 1}`, value: `{{TOKEN_${i + 1}}}`}));
|
|
126
|
-
menu.anchor = toggle;
|
|
127
|
-
|
|
128
|
-
toggle.addEventListener('click', () => menu.open ? menu.hide() : menu.show());
|
|
129
|
-
</script>
|
|
116
|
+
<zn-textarea label="Terms and conditions" rows="5" help-text="Type / to see 3 of 9, then keep typing to narrow"
|
|
117
|
+
slash-items="Brand name={{BRAND_NAME}}, Legal entity={{LEGAL_ENTITY}}, Jurisdiction={{JURISDICTION}},
|
|
118
|
+
Customer name={{CUSTOMER_NAME}}, Customer email={{CUSTOMER_EMAIL}},
|
|
119
|
+
Invoice number={{INVOICE_NUMBER}}, Invoice date={{INVOICE_DATE}},
|
|
120
|
+
Support email={{SUPPORT_EMAIL}}, Support phone={{SUPPORT_PHONE}}">
|
|
121
|
+
<zn-slash-menu slot="slash-menu" heading="Replacement strings" max-items="3"></zn-slash-menu>
|
|
122
|
+
</zn-textarea>
|
|
130
123
|
```
|
|
131
124
|
|
|
132
125
|
### Attaching It To Your Own Field
|
|
@@ -328,8 +328,8 @@ A complete example showing product content management with translations.
|
|
|
328
328
|
{% raw %}
|
|
329
329
|
|
|
330
330
|
Copy that carries replacement strings — `{{BRAND_NAME}}` and the like — needs the same tokens in every language. Set
|
|
331
|
-
`slash-items`
|
|
332
|
-
|
|
331
|
+
`slash-items` and typing `/` offers them at the caret, in whichever language is being edited. This works on both the
|
|
332
|
+
single-line default and `input-type="textarea"`. The attribute takes the same shorthand and JSON that
|
|
333
333
|
[`zn-textarea`](/components/textarea#slash-menu-quick-insertions) accepts, and `slash-preset`, `slash-trigger` and
|
|
334
334
|
`slash-heading` are forwarded too.
|
|
335
335
|
|
|
@@ -365,7 +365,7 @@ way `Enter` otherwise would.
|
|
|
365
365
|
| `flush` | `boolean` | `false` | Removes padding for compact layout |
|
|
366
366
|
| `languages` | `Record<string, string>` | `{en: "EN"}` | Object mapping language codes to display names |
|
|
367
367
|
| `values` | `Record<string, string>` | `{}` | Object mapping language codes to translation text |
|
|
368
|
-
| `slash-items` | `SlashMenuItem[]` | `[]` | Quick insertions offered by the slash menu
|
|
368
|
+
| `slash-items` | `SlashMenuItem[]` | `[]` | Quick insertions offered by the slash menu |
|
|
369
369
|
| `slash-preset` | `string` | `''` | Registered item sets to offer, comma separated |
|
|
370
370
|
| `slash-trigger` | `string` | `'/'` | The characters that open the slash menu |
|
|
371
371
|
| `slash-heading` | `string` | `'Insert'` | Heading shown above the slash menu's items |
|
package/package.json
CHANGED
|
@@ -99,8 +99,8 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
99
99
|
@property({ attribute: "textarea-rows", type: Number }) textareaRows: 1;
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* Quick insertions offered by the slash menu of a `textarea` input. Accepts a JSON array of items, or
|
|
103
|
-
* shorthand `Brand name={{BRAND_NAME}}, Support email={{SUPPORT_EMAIL}}`.
|
|
102
|
+
* Quick insertions offered by the slash menu of a `text` or `textarea` input. Accepts a JSON array of items, or
|
|
103
|
+
* the shorthand `Brand name={{BRAND_NAME}}, Support email={{SUPPORT_EMAIL}}`.
|
|
104
104
|
*/
|
|
105
105
|
@property({
|
|
106
106
|
attribute: 'slash-items',
|
|
@@ -482,6 +482,11 @@ export default class ZnInlineEdit extends ZincElement implements ZincFormControl
|
|
|
482
482
|
autocomplete=${ifDefined(this.autocomplete)}
|
|
483
483
|
required=${ifDefined(this.required)}
|
|
484
484
|
dir="${this.dir}"
|
|
485
|
+
slash-trigger="${this.slashTrigger}"
|
|
486
|
+
slash-heading="${this.slashHeading}"
|
|
487
|
+
slash-preset="${this.slashPreset}"
|
|
488
|
+
.slashItems="${this.slashItems}"
|
|
489
|
+
.slashItemsProvider="${this.slashItemsProvider}"
|
|
485
490
|
@zn-input="${this.handleInput}"
|
|
486
491
|
@zn-blur="${this.handleBlur}">
|
|
487
492
|
</zn-input>`;
|
|
@@ -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
|
});
|