@kubex/zinc 1.1.79 → 1.1.80

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.
Files changed (31) hide show
  1. package/dist/custom-elements.json +1095 -76
  2. package/dist/vscode.html-custom-data.json +186 -12
  3. package/dist/web-types.json +418 -24
  4. package/dist/zn.d.ts +384 -0
  5. package/dist/zn.min.js +350 -295
  6. package/docs/pages/components/inline-edit.md +21 -0
  7. package/docs/pages/components/slash-item.md +126 -0
  8. package/docs/pages/components/slash-menu.md +168 -0
  9. package/docs/pages/components/textarea.md +148 -0
  10. package/docs/pages/components/translations.md +34 -0
  11. package/package.json +1 -1
  12. package/src/components/inline-edit/inline-edit.component.ts +31 -0
  13. package/src/components/inline-edit/inline-edit.test.ts +83 -1
  14. package/src/components/slash-item/index.ts +12 -0
  15. package/src/components/slash-item/slash-item.component.ts +76 -0
  16. package/src/components/slash-item/slash-item.scss +5 -0
  17. package/src/components/slash-menu/index.ts +14 -0
  18. package/src/components/slash-menu/slash-menu-controller.ts +361 -0
  19. package/src/components/slash-menu/slash-menu-items.ts +122 -0
  20. package/src/components/slash-menu/slash-menu.component.ts +305 -0
  21. package/src/components/slash-menu/slash-menu.scss +120 -0
  22. package/src/components/slash-menu/slash-menu.test.ts +154 -0
  23. package/src/components/textarea/textarea.component.ts +143 -0
  24. package/src/components/textarea/textarea.test.ts +310 -2
  25. package/src/components/translations/translations.component.ts +31 -0
  26. package/src/components/translations/translations.test.ts +89 -1
  27. package/src/events/events.ts +2 -0
  28. package/src/events/zn-slash-insert.ts +9 -0
  29. package/src/events/zn-slash-select.ts +9 -0
  30. package/src/utilities/caret-position.ts +118 -0
  31. package/src/zinc.ts +3 -0
@@ -0,0 +1,305 @@
1
+ import {autoUpdate, computePosition, flip, offset, shift, size} from '@floating-ui/dom';
2
+ import {classMap} from 'lit/directives/class-map.js';
3
+ import {html, unsafeCSS} from 'lit';
4
+ import {property, query, state} from 'lit/decorators.js';
5
+ import ZincElement from '../../internal/zinc-element';
6
+ import ZnIcon from '../icon';
7
+ import type {CSSResultGroup, PropertyValues} from 'lit';
8
+ import type {Placement, VirtualElement} from '@floating-ui/dom';
9
+ import type {SlashMenuItem} from './slash-menu-items';
10
+
11
+ import styles from './slash-menu.scss';
12
+
13
+ export const SLASH_ITEM_SELECT = 'zn-slash-item-select';
14
+
15
+ function sameItems(a: SlashMenuItem[] | undefined, b: SlashMenuItem[]): boolean {
16
+ return !!a && a.length === b.length && a.every((item, i) =>
17
+ item.label === b[i].label && item.value === b[i].value && item.disabled === b[i].disabled);
18
+ }
19
+
20
+ /**
21
+ * @summary A keyboard-driven list of insertions, anchored to the caret of the field that opened it.
22
+ * @documentation https://zinc.style/components/slash-menu
23
+ * @status experimental
24
+ * @since 1.1
25
+ *
26
+ * @dependency zn-icon
27
+ *
28
+ * @event zn-slash-item-select - Emitted when an item is chosen. Does not cross shadow boundaries; the
29
+ * component driving the menu (e.g. `zn-textarea`) re-emits it as `zn-slash-select`.
30
+ *
31
+ * @csspart panel - The floating panel that holds the list.
32
+ * @csspart heading - The panel's heading.
33
+ * @csspart item - An item in the list.
34
+ * @csspart group-heading - A group heading between items.
35
+ * @csspart footer - The truncation footer, shown when not every match fits.
36
+ *
37
+ * @cssproperty --slash-menu-width - The width of the panel.
38
+ * @cssproperty --slash-menu-max-height - The maximum height of the panel before it scrolls.
39
+ */
40
+ export default class ZnSlashMenu extends ZincElement {
41
+ static styles: CSSResultGroup = unsafeCSS(styles);
42
+ static dependencies = {
43
+ 'zn-icon': ZnIcon
44
+ };
45
+
46
+ @query('.slash-menu__panel') private panel: HTMLElement;
47
+
48
+ private stopAutoUpdate?: () => void;
49
+
50
+ /** Whether the menu is showing. */
51
+ @property({type: Boolean, reflect: true}) open = false;
52
+
53
+ /** The items to list. Already filtered — the menu displays what it is given. */
54
+ @property({type: Array}) items: SlashMenuItem[] = [];
55
+
56
+ /** The query the items were matched against, shown in the heading. */
57
+ @property() query = '';
58
+
59
+ /** The heading shown when there is no query. */
60
+ @property() heading = 'Insert';
61
+
62
+ /** Shown in place of the list when there are no items. */
63
+ @property({attribute: 'empty-text'}) emptyText = 'No matches';
64
+
65
+ /** The most items to render at once. Remaining matches are reported in the footer. */
66
+ @property({attribute: 'max-items', type: Number}) maxItems = 25;
67
+
68
+ /** The element or caret rect the panel is positioned against. */
69
+ @property({attribute: false}) anchor: Element | VirtualElement | null = null;
70
+
71
+ /** The preferred placement of the panel. */
72
+ @property() placement: Placement = 'bottom-start';
73
+
74
+ /** The gap between the caret and the panel. */
75
+ @property({type: Number}) distance = 4;
76
+
77
+ @state() private activeIndex = 0;
78
+
79
+ private get visibleItems(): SlashMenuItem[] {
80
+ return this.maxItems > 0 ? this.items.slice(0, this.maxItems) : this.items;
81
+ }
82
+
83
+ /** The item that Enter would insert. */
84
+ get activeItem(): SlashMenuItem | undefined {
85
+ return this.visibleItems[this.activeIndex];
86
+ }
87
+
88
+ show() {
89
+ this.open = true;
90
+ }
91
+
92
+ hide() {
93
+ this.open = false;
94
+ }
95
+
96
+ /** Sets the active item by index, wrapping at both ends and skipping disabled items. */
97
+ setActiveIndex(index: number) {
98
+ const items = this.visibleItems;
99
+ const selectable = items.filter(item => !item.disabled).length;
100
+ if (!selectable) {
101
+ this.activeIndex = -1;
102
+ return;
103
+ }
104
+
105
+ let next = (index + items.length) % items.length;
106
+ // Walk in the direction of travel until a selectable item is found
107
+ const step = index < this.activeIndex ? -1 : 1;
108
+ while (items[next]?.disabled) {
109
+ next = (next + step + items.length) % items.length;
110
+ }
111
+
112
+ this.activeIndex = next;
113
+ void this.updateComplete.then(() => this.scrollActiveIntoView());
114
+ }
115
+
116
+ /** Moves the active item by `delta` places. */
117
+ moveActive(delta: number) {
118
+ this.setActiveIndex(this.activeIndex + delta);
119
+ }
120
+
121
+ /** Chooses the active item, as pressing Enter would. */
122
+ selectActive() {
123
+ const item = this.activeItem;
124
+ if (item) this.selectItem(item);
125
+ }
126
+
127
+ /** Recalculates the panel's position against its anchor. */
128
+ reposition() {
129
+ void this.position();
130
+ }
131
+
132
+ connectedCallback() {
133
+ super.connectedCallback();
134
+ if (this.open) void this.updateComplete.then(() => this.startPositioner());
135
+ }
136
+
137
+ disconnectedCallback() {
138
+ super.disconnectedCallback();
139
+ this.stopPositioner();
140
+ }
141
+
142
+ private startPositioner() {
143
+ this.stopPositioner();
144
+ if (!this.anchor || !this.panel) return;
145
+
146
+ // autoUpdate keeps the panel on the caret through scrolling and resizes
147
+ this.stopAutoUpdate = autoUpdate(this.anchor, this.panel, () => void this.position());
148
+ }
149
+
150
+ private stopPositioner() {
151
+ this.stopAutoUpdate?.();
152
+ this.stopAutoUpdate = undefined;
153
+ }
154
+
155
+ private async position() {
156
+ const {anchor, panel} = this;
157
+ if (!this.open || !anchor || !panel) return;
158
+
159
+ const {x, y} = await computePosition(anchor, panel, {
160
+ placement: this.placement,
161
+ strategy: 'fixed',
162
+ middleware: [
163
+ offset(this.distance),
164
+ flip({padding: 8}),
165
+ shift({padding: 8}),
166
+ size({
167
+ padding: 8,
168
+ apply: ({availableHeight}) => {
169
+ panel.style.setProperty('--auto-size-available-height', `${Math.max(availableHeight, 0)}px`);
170
+ }
171
+ })
172
+ ]
173
+ });
174
+
175
+ Object.assign(panel.style, {left: `${x}px`, top: `${y}px`});
176
+ }
177
+
178
+ private selectItem(item: SlashMenuItem) {
179
+ if (item.disabled) return;
180
+
181
+ this.dispatchEvent(new CustomEvent(SLASH_ITEM_SELECT, {
182
+ bubbles: true,
183
+ cancelable: true,
184
+ composed: false,
185
+ detail: {item, query: this.query}
186
+ }));
187
+ }
188
+
189
+ private scrollActiveIntoView() {
190
+ const active = this.renderRoot.querySelector<HTMLElement>('[data-slash-item][aria-selected="true"]');
191
+ active?.scrollIntoView({block: 'nearest'});
192
+ }
193
+
194
+ // mousedown, not click: preventDefault keeps focus (and the caret) in the field
195
+ private readonly handleItemMouseDown = (event: MouseEvent) => {
196
+ event.preventDefault();
197
+
198
+ const index = Number((event.currentTarget as HTMLElement).dataset.index);
199
+ const item = this.visibleItems[index];
200
+ if (item) {
201
+ this.activeIndex = index;
202
+ this.selectItem(item);
203
+ }
204
+ };
205
+
206
+ protected willUpdate(changed: PropertyValues) {
207
+ super.willUpdate(changed);
208
+
209
+ // A genuinely new result set starts on its first selectable item. Re-resolving the same query
210
+ // hands over an equal-but-new array, which must not move the user's place in the list.
211
+ if (changed.has('items') && !sameItems(changed.get('items') as SlashMenuItem[] | undefined, this.items)) {
212
+ this.activeIndex = this.visibleItems.findIndex(item => !item.disabled);
213
+ }
214
+ }
215
+
216
+ protected updated(changed: PropertyValues) {
217
+ super.updated(changed);
218
+
219
+ if (changed.has('open') || changed.has('anchor')) {
220
+ if (this.open) {
221
+ this.startPositioner();
222
+ } else {
223
+ this.stopPositioner();
224
+ }
225
+ }
226
+
227
+ if (this.open) void this.position();
228
+ }
229
+
230
+ private renderItem(item: SlashMenuItem, index: number, showIcons: boolean) {
231
+ const isActive = index === this.activeIndex;
232
+ const token = item.value && item.value !== item.label && !item.value.includes('\n') ? item.value : '';
233
+
234
+ return html`
235
+ <button
236
+ type="button"
237
+ part="item"
238
+ class=${classMap({
239
+ 'slash-menu__item': true,
240
+ 'slash-menu__item--active': isActive,
241
+ 'slash-menu__item--disabled': !!item.disabled
242
+ })}
243
+ role="option"
244
+ aria-selected=${isActive ? 'true' : 'false'}
245
+ aria-disabled=${item.disabled ? 'true' : 'false'}
246
+ data-slash-item
247
+ data-index=${index}
248
+ tabindex="-1"
249
+ @mousedown=${this.handleItemMouseDown}>
250
+ ${showIcons
251
+ ? html`
252
+ <span class="slash-menu__icon">
253
+ ${item.icon ? html`
254
+ <zn-icon src=${item.icon} size="16"></zn-icon>` : ''}
255
+ </span>`
256
+ : ''}
257
+ <span class="slash-menu__text">
258
+ <span class="slash-menu__label">${item.label}</span>
259
+ ${item.description ? html`
260
+ <span class="slash-menu__description">${item.description}</span>` : ''}
261
+ </span>
262
+ ${token ? html`
263
+ <code class="slash-menu__token">${token}</code>` : ''}
264
+ </button>`;
265
+ }
266
+
267
+ private renderItems() {
268
+ const items = this.visibleItems;
269
+ const showIcons = items.some(item => item.icon);
270
+ let lastGroup: string | undefined;
271
+
272
+ return items.map((item, index) => {
273
+ const group = item.group;
274
+ const heading = group && group !== lastGroup
275
+ ? html`
276
+ <div part="group-heading" class="slash-menu__group-heading">${group}</div>`
277
+ : '';
278
+ lastGroup = group;
279
+
280
+ return html`${heading}${this.renderItem(item, index, showIcons)}`;
281
+ });
282
+ }
283
+
284
+ render() {
285
+ const hidden = this.items.length - this.visibleItems.length;
286
+
287
+ return html`
288
+ <div
289
+ part="panel"
290
+ class="slash-menu__panel"
291
+ role="listbox"
292
+ aria-hidden=${this.open ? 'false' : 'true'}
293
+ aria-label=${this.query ? `Matches for ${this.query}` : this.heading}>
294
+ <div part="heading" class="slash-menu__heading">
295
+ ${this.query ? html`Matching <strong>${this.query}</strong>` : this.heading}
296
+ </div>
297
+ ${this.items.length
298
+ ? this.renderItems()
299
+ : html`
300
+ <div class="slash-menu__empty">${this.emptyText}</div>`}
301
+ ${hidden > 0 ? html`
302
+ <div part="footer" class="slash-menu__footer">${hidden} more — keep typing to narrow</div>` : ''}
303
+ </div>`;
304
+ }
305
+ }
@@ -0,0 +1,120 @@
1
+ @use "../../wc";
2
+
3
+ :host {
4
+ --slash-menu-width: 320px;
5
+ --slash-menu-max-height: 260px;
6
+
7
+ display: contents;
8
+ }
9
+
10
+ :host(:not([open])) .slash-menu__panel {
11
+ display: none;
12
+ }
13
+
14
+ .slash-menu__panel {
15
+ display: flex;
16
+ flex-direction: column;
17
+ position: fixed;
18
+ top: 0;
19
+ left: 0;
20
+ width: var(--slash-menu-width);
21
+ max-width: var(--auto-size-available-width, none);
22
+ max-height: min(var(--slash-menu-max-height), var(--auto-size-available-height, 100vh));
23
+ overflow-y: auto;
24
+ overscroll-behavior: none;
25
+ z-index: var(--zn-z-index-dropdown);
26
+ padding: var(--zn-spacing-2x-small, 4px) 0;
27
+ background: var(--zn-panel-background-color);
28
+ border: solid var(--zn-panel-border-width, 1px) var(--zn-panel-border-color);
29
+ border-radius: var(--zn-border-radius, 6px);
30
+ box-shadow: var(--zn-shadow-medium);
31
+ font-family: var(--zn-font-sans);
32
+ font-size: var(--zn-font-size-medium);
33
+ color: rgb(var(--zn-text));
34
+ }
35
+
36
+ .slash-menu__heading,
37
+ .slash-menu__group-heading,
38
+ .slash-menu__footer,
39
+ .slash-menu__empty {
40
+ padding: var(--zn-spacing-2x-small, 4px) var(--zn-spacing-small, 12px);
41
+ font-size: var(--zn-font-size-x-small);
42
+ color: var(--zn-color-neutral-500);
43
+ user-select: none;
44
+ -webkit-user-select: none;
45
+ }
46
+
47
+ .slash-menu__group-heading {
48
+ font-weight: var(--zn-font-weight-semibold);
49
+ text-transform: uppercase;
50
+ letter-spacing: 0.04em;
51
+ }
52
+
53
+ .slash-menu__footer {
54
+ border-top: solid var(--zn-panel-border-width, 1px) var(--zn-panel-border-color);
55
+ margin-top: var(--zn-spacing-2x-small, 4px);
56
+ }
57
+
58
+ .slash-menu__item {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: var(--zn-spacing-x-small, 8px);
62
+ width: 100%;
63
+ padding: var(--zn-spacing-x-small, 8px) var(--zn-spacing-small, 12px);
64
+ border: none;
65
+ background: transparent;
66
+ color: inherit;
67
+ font: inherit;
68
+ text-align: left;
69
+ cursor: pointer;
70
+
71
+ &--active,
72
+ &:hover:not(&--disabled) {
73
+ background: var(--zn-input-background-color-hover);
74
+ }
75
+
76
+ &--disabled {
77
+ opacity: 0.5;
78
+ cursor: not-allowed;
79
+ }
80
+ }
81
+
82
+ .slash-menu__icon {
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ flex: 0 0 16px;
87
+ color: var(--zn-color-neutral-500);
88
+ }
89
+
90
+ .slash-menu__text {
91
+ display: flex;
92
+ flex-direction: column;
93
+ min-width: 0;
94
+ flex: 1 1 auto;
95
+ }
96
+
97
+ .slash-menu__label {
98
+ overflow: hidden;
99
+ text-overflow: ellipsis;
100
+ white-space: nowrap;
101
+ }
102
+
103
+ .slash-menu__description {
104
+ font-size: var(--zn-font-size-x-small);
105
+ color: var(--zn-color-neutral-500);
106
+ overflow: hidden;
107
+ text-overflow: ellipsis;
108
+ white-space: nowrap;
109
+ }
110
+
111
+ .slash-menu__token {
112
+ flex: 0 1 auto;
113
+ max-width: 45%;
114
+ overflow: hidden;
115
+ text-overflow: ellipsis;
116
+ white-space: nowrap;
117
+ font-family: var(--zn-font-mono);
118
+ font-size: var(--zn-font-size-x-small);
119
+ color: var(--zn-color-neutral-600);
120
+ }
@@ -0,0 +1,154 @@
1
+ import '../../../dist/zn.min.js';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+ import {
4
+ filterSlashItems,
5
+ getSlashMenuPreset,
6
+ parseSlashItems,
7
+ registerSlashMenuPreset,
8
+ unregisterSlashMenuPreset
9
+ } from './slash-menu-items';
10
+ import type ZnSlashMenu from './slash-menu.component';
11
+
12
+ describe('<zn-slash-menu>', () => {
13
+ it('should render a component', async () => {
14
+ const el = await fixture(html`
15
+ <zn-slash-menu></zn-slash-menu>`);
16
+
17
+ expect(el).to.exist;
18
+ });
19
+
20
+ it('lists the items it is given and starts on the first one', async () => {
21
+ const el = await fixture<ZnSlashMenu>(html`
22
+ <zn-slash-menu></zn-slash-menu>`);
23
+ el.items = [{label: 'Brand name', value: '{{BRAND_NAME}}'}, {label: 'Legal entity', value: '{{LEGAL_ENTITY}}'}];
24
+ el.show();
25
+ await el.updateComplete;
26
+
27
+ const items = el.shadowRoot!.querySelectorAll('[data-slash-item]');
28
+ expect(items.length).to.equal(2);
29
+ expect(el.activeItem?.label).to.equal('Brand name');
30
+ });
31
+
32
+ it('wraps when moving past either end and skips disabled items', async () => {
33
+ const el = await fixture<ZnSlashMenu>(html`
34
+ <zn-slash-menu></zn-slash-menu>`);
35
+ el.items = [
36
+ {label: 'One', value: '1'},
37
+ {label: 'Two', value: '2', disabled: true},
38
+ {label: 'Three', value: '3'}
39
+ ];
40
+ await el.updateComplete;
41
+
42
+ el.moveActive(1);
43
+ expect(el.activeItem?.label, 'skips the disabled item').to.equal('Three');
44
+
45
+ el.moveActive(1);
46
+ expect(el.activeItem?.label, 'wraps to the start').to.equal('One');
47
+
48
+ el.moveActive(-1);
49
+ expect(el.activeItem?.label, 'wraps to the end').to.equal('Three');
50
+ });
51
+
52
+ it('emits zn-slash-item-select when an item is clicked, without stealing focus', async () => {
53
+ const el = await fixture<ZnSlashMenu>(html`
54
+ <zn-slash-menu></zn-slash-menu>`);
55
+ el.items = [{label: 'Brand name', value: '{{BRAND_NAME}}'}];
56
+ el.show();
57
+ await el.updateComplete;
58
+
59
+ const selected: string[] = [];
60
+ el.addEventListener('zn-slash-item-select', (event: Event) => {
61
+ selected.push((event as CustomEvent<{item: {label: string}}>).detail.item.label);
62
+ });
63
+
64
+ const item = el.shadowRoot!.querySelector<HTMLButtonElement>('[data-slash-item]')!;
65
+ const event = new MouseEvent('mousedown', {bubbles: true, cancelable: true, composed: true});
66
+ item.dispatchEvent(event);
67
+
68
+ expect(selected).to.deep.equal(['Brand name']);
69
+ expect(event.defaultPrevented, 'mousedown is prevented so the field keeps focus').to.be.true;
70
+ });
71
+
72
+ it('reports how many matches were not rendered', async () => {
73
+ const el = await fixture<ZnSlashMenu>(html`
74
+ <zn-slash-menu max-items="2"></zn-slash-menu>`);
75
+ el.items = [{label: 'One'}, {label: 'Two'}, {label: 'Three'}];
76
+ el.show();
77
+ await el.updateComplete;
78
+
79
+ expect(el.shadowRoot!.querySelectorAll('[data-slash-item]').length).to.equal(2);
80
+ expect(el.shadowRoot!.querySelector('[part="footer"]')?.textContent).to.contain('1 more');
81
+ });
82
+ });
83
+
84
+ describe('slash menu items', () => {
85
+ describe('parseSlashItems', () => {
86
+ it('parses a JSON array', () => {
87
+ const items = parseSlashItems('[{"label":"Brand name","value":"{{BRAND_NAME}}"}]');
88
+
89
+ expect(items).to.deep.equal([{label: 'Brand name', value: '{{BRAND_NAME}}'}]);
90
+ });
91
+
92
+ it('parses the label=value shorthand', () => {
93
+ const items = parseSlashItems('Brand name={{BRAND_NAME}}, {{SUPPORT_EMAIL}}');
94
+
95
+ expect(items).to.deep.equal([
96
+ {label: 'Brand name', value: '{{BRAND_NAME}}'},
97
+ {label: '{{SUPPORT_EMAIL}}', value: '{{SUPPORT_EMAIL}}'}
98
+ ]);
99
+ });
100
+
101
+ it('returns nothing for empty or unparsable input', () => {
102
+ expect(parseSlashItems('')).to.deep.equal([]);
103
+ expect(parseSlashItems(null)).to.deep.equal([]);
104
+ expect(parseSlashItems('[{"nope":true}')).to.deep.equal([]);
105
+ });
106
+ });
107
+
108
+ describe('filterSlashItems', () => {
109
+ const items = [
110
+ {label: 'Support email', value: '{{SUPPORT_EMAIL}}', keywords: 'contact'},
111
+ {label: 'Brand name', value: '{{BRAND_NAME}}', description: 'Trading name'},
112
+ {label: 'Jurisdiction', value: '{{BRAND_JURISDICTION}}'}
113
+ ];
114
+
115
+ it('keeps declaration order for an empty query', () => {
116
+ expect(filterSlashItems(items, '').map(item => item.label))
117
+ .to.deep.equal(['Support email', 'Brand name', 'Jurisdiction']);
118
+ });
119
+
120
+ it('ranks label matches above value and keyword matches', () => {
121
+ expect(filterSlashItems(items, 'brand').map(item => item.label))
122
+ .to.deep.equal(['Brand name', 'Jurisdiction']);
123
+ });
124
+
125
+ it('matches keywords and descriptions', () => {
126
+ expect(filterSlashItems(items, 'contact').map(item => item.label)).to.deep.equal(['Support email']);
127
+ expect(filterSlashItems(items, 'trading').map(item => item.label)).to.deep.equal(['Brand name']);
128
+ });
129
+
130
+ it('honours order over declaration order', () => {
131
+ const ordered = filterSlashItems([{label: 'Second', order: 2}, {label: 'First', order: 1}], '');
132
+
133
+ expect(ordered.map(item => item.label)).to.deep.equal(['First', 'Second']);
134
+ });
135
+
136
+ it('drops items that match nothing', () => {
137
+ expect(filterSlashItems(items, 'nothing here')).to.deep.equal([]);
138
+ });
139
+ });
140
+
141
+ describe('presets', () => {
142
+ it('resolves registered presets by name', () => {
143
+ registerSlashMenuPreset('test-legal', [{label: 'Brand name', value: '{{BRAND_NAME}}'}]);
144
+ registerSlashMenuPreset('test-support', [{label: 'Support email', value: '{{SUPPORT_EMAIL}}'}]);
145
+
146
+ expect(getSlashMenuPreset('test-legal, test-support').map(item => item.label))
147
+ .to.deep.equal(['Brand name', 'Support email']);
148
+
149
+ unregisterSlashMenuPreset('test-legal');
150
+ unregisterSlashMenuPreset('test-support');
151
+ expect(getSlashMenuPreset('test-legal')).to.deep.equal([]);
152
+ });
153
+ });
154
+ });