@kubex/zinc 1.1.140 → 1.1.142

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.
@@ -12,6 +12,21 @@ describe('<zn-page-builder>', () => {
12
12
  expect(el.shadowRoot?.querySelector('[part="canvas"]')).to.exist;
13
13
  });
14
14
 
15
+ it('should fill the height it is given inside a growing panel', async () => {
16
+ const parent = await fixture(html`
17
+ <div style="display: flex; flex-direction: column; height: 700px; width: 900px;">
18
+ <zn-sp grow flush>
19
+ <form>
20
+ <zn-page-builder name="config"></zn-page-builder>
21
+ </form>
22
+ </zn-sp>
23
+ </div>`);
24
+ const el = parent.querySelector<ZnPageBuilder>('zn-page-builder')!;
25
+ await el.updateComplete;
26
+
27
+ expect(el.getBoundingClientRect().height).to.equal(700);
28
+ });
29
+
15
30
  it('should build the palette from slotted config templates', async () => {
16
31
  const el = await fixture<ZnPageBuilder>(html`
17
32
  <zn-page-builder>
@@ -312,6 +327,201 @@ describe('<zn-page-builder>', () => {
312
327
  expect(unnamed?.querySelector('.inspector-head__type'), 'no duplicate type line').to.not.exist;
313
328
  });
314
329
 
330
+ it('should resize the inspector by dragging its edge handle', async () => {
331
+ const el = await fixture<ZnPageBuilder>(html`
332
+ <zn-page-builder style="width: 1200px; height: 600px" config='{"sections":[{"id":"s1","type":"hero","data":{}}]}'>
333
+ <template type="hero" slot="config" label="Hero">
334
+ <zn-input name="title" label="Title"></zn-input>
335
+ </template>
336
+ </zn-page-builder>`);
337
+ await el.updateComplete;
338
+
339
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
340
+ await el.updateComplete;
341
+
342
+ const builder = el.shadowRoot!.querySelector<HTMLElement>('.builder')!;
343
+ const handle = el.shadowRoot!.querySelector<HTMLElement>('.inspector-resize');
344
+ expect(handle, 'resize handle').to.exist;
345
+
346
+ const before = el.shadowRoot!.querySelector('.inspector')!.getBoundingClientRect().width;
347
+ handle!.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true, clientX: 800, pointerId: 1}));
348
+ window.dispatchEvent(new PointerEvent('pointermove', {bubbles: true, clientX: 600, pointerId: 1}));
349
+ window.dispatchEvent(new PointerEvent('pointerup', {bubbles: true, pointerId: 1}));
350
+ await el.updateComplete;
351
+
352
+ expect(builder.style.getPropertyValue('--inspector-col')).to.equal(`${Math.round(before + 200)}px`);
353
+ });
354
+
355
+ it('should hold the dragged inspector above its minimum width', async () => {
356
+ const el = await fixture<ZnPageBuilder>(html`
357
+ <zn-page-builder style="width: 1200px; height: 600px" config='{"sections":[{"id":"s1","type":"hero","data":{}}]}'>
358
+ <template type="hero" slot="config" label="Hero"></template>
359
+ </zn-page-builder>`);
360
+ await el.updateComplete;
361
+
362
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
363
+ await el.updateComplete;
364
+
365
+ const builder = el.shadowRoot!.querySelector<HTMLElement>('.builder')!;
366
+ const handle = el.shadowRoot!.querySelector<HTMLElement>('.inspector-resize')!;
367
+ handle.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true, clientX: 800, pointerId: 1}));
368
+ window.dispatchEvent(new PointerEvent('pointermove', {bubbles: true, clientX: 5000, pointerId: 1}));
369
+ window.dispatchEvent(new PointerEvent('pointerup', {bubbles: true, pointerId: 1}));
370
+ await el.updateComplete;
371
+
372
+ expect(parseInt(builder.style.getPropertyValue('--inspector-col'), 10)).to.equal(300);
373
+ });
374
+
375
+ it('should resize the inspector from the keyboard', async () => {
376
+ const el = await fixture<ZnPageBuilder>(html`
377
+ <zn-page-builder style="width: 1200px; height: 600px" config='{"sections":[{"id":"s1","type":"hero","data":{}}]}'>
378
+ <template type="hero" slot="config" label="Hero"></template>
379
+ </zn-page-builder>`);
380
+ await el.updateComplete;
381
+
382
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
383
+ await el.updateComplete;
384
+
385
+ const builder = el.shadowRoot!.querySelector<HTMLElement>('.builder')!;
386
+ const handle = el.shadowRoot!.querySelector<HTMLElement>('.inspector-resize')!;
387
+ expect(handle.getAttribute('role')).to.equal('separator');
388
+
389
+ handle.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft', bubbles: true}));
390
+ await el.updateComplete;
391
+ expect(parseInt(builder.style.getPropertyValue('--inspector-col'), 10)).to.equal(363);
392
+
393
+ handle.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true}));
394
+ await el.updateComplete;
395
+ expect(parseInt(builder.style.getPropertyValue('--inspector-col'), 10)).to.equal(343);
396
+ });
397
+
398
+ it('should still tuck a resized inspector fully away', async () => {
399
+ const el = await fixture<ZnPageBuilder>(html`
400
+ <zn-page-builder style="width: 1200px; height: 600px" config='{"sections":[{"id":"s1","type":"hero","data":{}}]}'>
401
+ <template type="hero" slot="config" label="Hero"></template>
402
+ </zn-page-builder>`);
403
+ await el.updateComplete;
404
+
405
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
406
+ await el.updateComplete;
407
+
408
+ const handle = el.shadowRoot!.querySelector<HTMLElement>('.inspector-resize')!;
409
+ handle.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true, clientX: 800, pointerId: 1}));
410
+ window.dispatchEvent(new PointerEvent('pointermove', {bubbles: true, clientX: 600, pointerId: 1}));
411
+ window.dispatchEvent(new PointerEvent('pointerup', {bubbles: true, pointerId: 1}));
412
+ await el.updateComplete;
413
+
414
+ const builder = el.shadowRoot!.querySelector<HTMLElement>('.builder')!;
415
+ expect(parseInt(builder.style.getPropertyValue('--inspector-col'), 10), 'resized').to.be.greaterThan(343);
416
+
417
+ // An inline width outranks the collapsed rule's 0px, so it has to step aside.
418
+ el.inspectorCollapsed = true;
419
+ await el.updateComplete;
420
+ expect(getComputedStyle(builder).getPropertyValue('--inspector-col').trim()).to.equal('0px');
421
+ });
422
+
423
+ const richBuilder = (height: number) => fixture<ZnPageBuilder>(html`
424
+ <zn-page-builder style="width: 1200px; height: ${height}px" config='{"sections":[{"id":"s1","type":"rich","data":{}}]}'>
425
+ <template type="rich" slot="config" label="Rich Text">
426
+ <zn-translations name="content" label="Content" input-type="remarkd"></zn-translations>
427
+ </template>
428
+ </zn-page-builder>`);
429
+
430
+ it('should widen the inspector by default for a remarkd editor', async () => {
431
+ const el = await richBuilder(600);
432
+ await el.updateComplete;
433
+
434
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
435
+ await el.updateComplete;
436
+
437
+ const builder = el.shadowRoot!.querySelector<HTMLElement>('.builder')!;
438
+ expect(builder.classList.contains('builder--inspector-wide'), 'wide').to.be.true;
439
+ expect(getComputedStyle(builder).getPropertyValue('--inspector-col').trim()).to.equal('520px');
440
+ });
441
+
442
+ it('should leave the inspector at its usual width without a remarkd editor', async () => {
443
+ const el = await fixture<ZnPageBuilder>(html`
444
+ <zn-page-builder style="width: 1200px; height: 600px" config='{"sections":[{"id":"s1","type":"hero","data":{}}]}'>
445
+ <template type="hero" slot="config" label="Hero">
446
+ <zn-input name="title" label="Title"></zn-input>
447
+ </template>
448
+ </zn-page-builder>`);
449
+ await el.updateComplete;
450
+
451
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
452
+ await el.updateComplete;
453
+
454
+ const builder = el.shadowRoot!.querySelector<HTMLElement>('.builder')!;
455
+ expect(builder.classList.contains('builder--inspector-wide')).to.be.false;
456
+ expect(getComputedStyle(builder).getPropertyValue('--inspector-col').trim()).to.equal('343px');
457
+ });
458
+
459
+ it('should prefill translations stamped inside a translation group', async () => {
460
+ const state = JSON.stringify({
461
+ sections: [{
462
+ id: 's1', type: 'tile', data: {
463
+ title: JSON.stringify({en: 'Billing'}),
464
+ subtitle: JSON.stringify({en: 'How to pay'})
465
+ }
466
+ }]
467
+ });
468
+ const el = await fixture<ZnPageBuilder>(html`
469
+ <zn-page-builder .config="${state}">
470
+ <template type="tile" slot="config" label="Tile">
471
+ <zn-translation-group inline languages='{"en":"English","fr":"French"}'>
472
+ <zn-translations name="title" label="Title"></zn-translations>
473
+ <zn-translations name="subtitle" label="Subtitle"></zn-translations>
474
+ </zn-translation-group>
475
+ </template>
476
+ </zn-page-builder>`);
477
+ await el.updateComplete;
478
+
479
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
480
+ await el.updateComplete;
481
+
482
+ const fields = [...el.shadowRoot!.querySelectorAll<HTMLElement & {
483
+ values: Record<string, string>; languages: Record<string, string>; grouped: boolean;
484
+ updateComplete: Promise<unknown>;
485
+ }>('.inspector__form zn-translations')];
486
+ expect(fields.length, 'both fields stamped').to.equal(2);
487
+ await Promise.all(fields.map(f => f.updateComplete));
488
+
489
+ expect(fields[0].values.en).to.equal('Billing');
490
+ expect(fields[1].values.en).to.equal('How to pay');
491
+ // The group owns the select and the language list, so the children drop theirs.
492
+ fields.forEach(field => {
493
+ expect(field.grouped, 'grouped').to.be.true;
494
+ expect(field.languages).to.have.property('fr');
495
+ expect(field.shadowRoot!.querySelector('[part="language-select"]'), 'child select').to.not.exist;
496
+ });
497
+ expect(el.shadowRoot!.querySelectorAll('.inspector__form [part="language-select"]').length).to.equal(0);
498
+ });
499
+
500
+ it('should not strand the group language select above its fields', async () => {
501
+ const el = await fixture<ZnPageBuilder>(html`
502
+ <zn-page-builder style="width: 1200px; height: 600px" config='{"sections":[{"id":"s1","type":"tile","data":{}}]}'>
503
+ <template type="tile" slot="config" label="Tile">
504
+ <zn-translation-group inline languages='{"en":"English","fr":"French"}'>
505
+ <zn-translations name="title" label="Title" flush></zn-translations>
506
+ <zn-translations name="subtitle" label="Subtitle" flush></zn-translations>
507
+ </zn-translation-group>
508
+ </template>
509
+ </zn-page-builder>`);
510
+ await el.updateComplete;
511
+
512
+ el.shadowRoot?.querySelector('zn-page-section-card')?.dispatchEvent(new Event('click'));
513
+ await el.updateComplete;
514
+
515
+ const group = el.shadowRoot!.querySelector<HTMLElement & { updateComplete: Promise<unknown> }>(
516
+ '.inspector__form zn-translation-group')!;
517
+ await group.updateComplete;
518
+ const select = group.shadowRoot!.querySelector<HTMLElement>('[part="language-field"]')!;
519
+ const first = group.querySelector<HTMLElement>('zn-translations')!;
520
+
521
+ const gap = first.getBoundingClientRect().top - select.getBoundingClientRect().bottom;
522
+ expect(gap, 'gap under the language select').to.be.at.most(24);
523
+ });
524
+
315
525
  it('should clear the selection from the inspector close button', async () => {
316
526
  const el = await fixture<ZnPageBuilder>(html`
317
527
  <zn-page-builder config='{"sections":[{"id":"s1","type":"hero","data":{}}]}'>
@@ -1778,12 +1778,14 @@ export default class ZnRemarkdEditor extends ZincElement implements ZincFormCont
1778
1778
 
1779
1779
  /** A picker action with no endpoint configured is not offered at all. */
1780
1780
  private actionAvailable(action: EditorAction): boolean {
1781
+ if (action.opens === 'image') return !!this.attachmentUrl;
1781
1782
  if (action.opens === 'include') return !!this.includeUrl;
1782
1783
  if (action.opens === 'link') return !!this.linkUrl;
1783
1784
  return true;
1784
1785
  }
1785
1786
 
1786
1787
  private slashItemAvailable(item: SlashMenuItem): boolean {
1788
+ if (item.action === 'image') return !!this.attachmentUrl;
1787
1789
  if (item.action === 'include') return !!this.includeUrl;
1788
1790
  if (item.action === 'link') return !!this.linkUrl;
1789
1791
  return true;
@@ -331,7 +331,7 @@ Second"></zn-remarkd-editor>`);
331
331
 
332
332
  it('should group the toolbar buttons', async () => {
333
333
  const el = await fixture<ZnRemarkdEditor>(html`
334
- <zn-remarkd-editor include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
334
+ <zn-remarkd-editor attachment-url="/attachments" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
335
335
  const groups = el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar .toolbar__group');
336
336
 
337
337
  expect(groups.length).to.be.greaterThan(5);
@@ -343,7 +343,7 @@ Second"></zn-remarkd-editor>`);
343
343
 
344
344
  it('should move toolbar groups into an overflow menu when narrow', async () => {
345
345
  const el = await fixture<ZnRemarkdEditor>(html`
346
- <zn-remarkd-editor style="width: 240px"></zn-remarkd-editor>`);
346
+ <zn-remarkd-editor attachment-url="/attachments" style="width: 240px"></zn-remarkd-editor>`);
347
347
  await el.updateComplete;
348
348
  await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__toolbar-more'),
349
349
  'the overflow trigger never appeared');
@@ -748,7 +748,7 @@ Second"></zn-remarkd-editor>`);
748
748
 
749
749
  it('should list every action in the slash menu under its group', async () => {
750
750
  const el = await fixture<ZnRemarkdEditor>(html`
751
- <zn-remarkd-editor value="Hello" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
751
+ <zn-remarkd-editor value="Hello" attachment-url="/attachments" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
752
752
  el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
753
753
  await el.updateComplete;
754
754
  typeInBlock(el, '/');
@@ -761,20 +761,32 @@ Second"></zn-remarkd-editor>`);
761
761
 
762
762
  it('should omit the Include action from the slash menu without an include-url', async () => {
763
763
  const el = await fixture<ZnRemarkdEditor>(html`
764
- <zn-remarkd-editor value="Hello"></zn-remarkd-editor>`);
764
+ <zn-remarkd-editor value="Hello" attachment-url="/attachments" link-url="/links"></zn-remarkd-editor>`);
765
765
  el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
766
766
  await el.updateComplete;
767
767
  typeInBlock(el, '/');
768
768
  await waitUntil(() => el.shadowRoot!.querySelector('zn-slash-menu[open]'), 'the slash menu never opened');
769
769
 
770
770
  const menu = el.shadowRoot!.querySelector<ZnSlashMenu>('zn-slash-menu')!;
771
- expect(menu.items.length).to.equal(EDITOR_ACTIONS.length - 2);
771
+ expect(menu.items.length).to.equal(EDITOR_ACTIONS.length - 1);
772
772
  expect(menu.items.every(item => item.action !== 'include')).to.be.true;
773
773
  });
774
774
 
775
+ it('should omit the Image action from the slash menu without an attachment-url', async () => {
776
+ const el = await fixture<ZnRemarkdEditor>(html`
777
+ <zn-remarkd-editor value="Hello" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
778
+ el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
779
+ await el.updateComplete;
780
+ typeInBlock(el, '/');
781
+ await waitUntil(() => el.shadowRoot!.querySelector('zn-slash-menu[open]'), 'the slash menu never opened');
782
+
783
+ const menu = el.shadowRoot!.querySelector<ZnSlashMenu>('zn-slash-menu')!;
784
+ expect(menu.items.every(item => item.action !== 'image')).to.be.true;
785
+ });
786
+
775
787
  it('should omit the article link action without a link-url', async () => {
776
788
  const el = await fixture<ZnRemarkdEditor>(html`
777
- <zn-remarkd-editor value="Hello" include-url="/includes"></zn-remarkd-editor>`);
789
+ <zn-remarkd-editor value="Hello" attachment-url="/attachments" include-url="/includes"></zn-remarkd-editor>`);
778
790
  el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
779
791
  await el.updateComplete;
780
792
  typeInBlock(el, '/');
@@ -787,7 +799,7 @@ Second"></zn-remarkd-editor>`);
787
799
 
788
800
  it('should offer the article link action with a link-url', async () => {
789
801
  const el = await fixture<ZnRemarkdEditor>(html`
790
- <zn-remarkd-editor value="Hello" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
802
+ <zn-remarkd-editor value="Hello" attachment-url="/attachments" include-url="/includes" link-url="/links"></zn-remarkd-editor>`);
791
803
  el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
792
804
  await el.updateComplete;
793
805
  typeInBlock(el, '/');
@@ -1179,6 +1191,22 @@ include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
1179
1191
  expect(options[0].textContent).to.contain('Refund Policy');
1180
1192
  });
1181
1193
 
1194
+ it('should offer no image entry points without an attachment-url', async () => {
1195
+ const el = await fixture<ZnRemarkdEditor>(html`
1196
+ <zn-remarkd-editor value="# Title"></zn-remarkd-editor>`);
1197
+ const tooltips = Array.from(el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button'))
1198
+ .map(button => button.getAttribute('tooltip'));
1199
+ expect(tooltips).to.not.contain('Image');
1200
+ });
1201
+
1202
+ it('should offer the Image action with an attachment-url', async () => {
1203
+ const el = await fixture<ZnRemarkdEditor>(html`
1204
+ <zn-remarkd-editor value="# Title" attachment-url="/attachments"></zn-remarkd-editor>`);
1205
+ const tooltips = Array.from(el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button'))
1206
+ .map(button => button.getAttribute('tooltip'));
1207
+ expect(tooltips).to.contain('Image');
1208
+ });
1209
+
1182
1210
  it('should offer no include entry points without an include-url', async () => {
1183
1211
  const el = await fixture<ZnRemarkdEditor>(html`
1184
1212
  <zn-remarkd-editor value="# Title"></zn-remarkd-editor>`);
@@ -845,6 +845,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
845
845
  this.getAllOptGroups().forEach(group => {
846
846
  group.hidden = false;
847
847
  });
848
+ this.updateOptGroupSeparators();
848
849
  this._noResultsVisible = false;
849
850
  return;
850
851
  }
@@ -872,6 +873,7 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
872
873
 
873
874
  // Update opt-group visibility based on whether any child options are visible
874
875
  this.getAllOptGroups().forEach(group => group.updateVisibility());
876
+ this.updateOptGroupSeparators();
875
877
 
876
878
  // Show empty state when no non-selected options match (selected-but-hidden items are value anchors, not results)
877
879
  this._noResultsVisible = allOptions.filter(o => !o.selected).every(option => option.hidden);
@@ -907,6 +909,12 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
907
909
  this.getAllOptGroups().forEach(group => {
908
910
  group.hidden = false;
909
911
  });
912
+ this.updateOptGroupSeparators();
913
+ }
914
+
915
+ // Runs after every visibility change so group separators reflect which groups are actually showing
916
+ private updateOptGroupSeparators() {
917
+ this.getAllOptGroups().forEach(group => group.updateSeparator());
910
918
  }
911
919
 
912
920
  /**
@@ -1099,6 +1107,8 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
1099
1107
  // Select only the options that match the new value (re-query in case options were materialised)
1100
1108
  this.setSelectedOptions(this.getAllOptions().filter(el => value.includes(el.value)));
1101
1109
 
1110
+ this.updateOptGroupSeparators();
1111
+
1102
1112
  // of check if an option has selected attribute set initially
1103
1113
  if (!this.valueHasChanged) {
1104
1114
  const initiallySelectedOptions = allOptions.filter(el => el.hasAttribute('selected'));
@@ -1330,6 +1340,11 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
1330
1340
  this.formControlController.emitInvalidEvent(event);
1331
1341
  }
1332
1342
 
1343
+ protected updated(_changedProperties: PropertyValues) {
1344
+ super.updated(_changedProperties);
1345
+ this.updateOptGroupSeparators();
1346
+ }
1347
+
1333
1348
  protected firstUpdated(_changedProperties: PropertyValues) {
1334
1349
  super.firstUpdated(_changedProperties);
1335
1350
 
@@ -220,6 +220,16 @@ describe('<zn-select>', () => {
220
220
 
221
221
  expect(visibleValues()).to.deep.equal(['lemon']);
222
222
  });
223
+
224
+ it('drops the separator on the first visible group when earlier groups are filtered out', async () => {
225
+ const groups = el.querySelectorAll('zn-opt-group');
226
+
227
+ await type('berr');
228
+ expect(groups[1].hasAttribute('data-first-visible'), 'Berries group while alone').to.be.true;
229
+
230
+ await type('');
231
+ expect(groups[1].hasAttribute('data-first-visible'), 'Berries group with Citrus restored').to.be.false;
232
+ });
223
233
  });
224
234
 
225
235
  describe('Escape key', () => {
@@ -15,6 +15,12 @@
15
15
  height: 100%;
16
16
  }
17
17
 
18
+ // A form wrapping the whole panel is there to submit it, not to lay it out —
19
+ // leaving it in the box tree floors the panel at the form's content height.
20
+ :host([grow]) ::slotted(form:only-child) {
21
+ display: contents;
22
+ }
23
+
18
24
  :host([max-full]) {
19
25
  max-height: 100%;
20
26
  overflow: hidden;
@@ -8,6 +8,37 @@ describe('<zn-sp>', () => {
8
8
  expect(el).to.exist;
9
9
  });
10
10
 
11
+ it('should lay out through a form that wraps the whole panel', async () => {
12
+ const parent = await fixture(html`
13
+ <div style="display: flex; flex-direction: column; height: 400px; width: 600px;">
14
+ <zn-sp grow flush>
15
+ <form>
16
+ <div id="panel" style="height: 100%;">Content</div>
17
+ </form>
18
+ </zn-sp>
19
+ </div>
20
+ `);
21
+
22
+ const form = parent.querySelector('form')!;
23
+ expect(getComputedStyle(form).display).to.equal('contents');
24
+ expect(parent.querySelector<HTMLElement>('#panel')!.getBoundingClientRect().height).to.equal(400);
25
+ });
26
+
27
+ it('should leave a form that shares the panel with siblings alone', async () => {
28
+ const parent = await fixture(html`
29
+ <div style="display: flex; flex-direction: column; height: 400px;">
30
+ <zn-sp grow>
31
+ <h2>Heading</h2>
32
+ <form>
33
+ <div>Content</div>
34
+ </form>
35
+ </zn-sp>
36
+ </div>
37
+ `);
38
+
39
+ expect(getComputedStyle(parent.querySelector('form')!).display).to.equal('block');
40
+ });
41
+
11
42
  it('should fill its parent container height', async () => {
12
43
  const parent = await fixture(html`
13
44
  <div style="height: 300px;">
@@ -13,6 +13,7 @@ import ZnChip from '../chip';
13
13
  import ZnInlineEdit from '../inline-edit';
14
14
  import ZnInput from '../input';
15
15
  import ZnOption from '../option';
16
+ import ZnRemarkdEditor from '../remarkd-editor';
16
17
  import ZnSelect from '../select';
17
18
  import ZnTextarea from '../textarea';
18
19
  import type {PropertyValues} from 'lit';
@@ -64,6 +65,7 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
64
65
  'zn-inline-edit': ZnInlineEdit,
65
66
  'zn-input': ZnInput,
66
67
  'zn-option': ZnOption,
68
+ 'zn-remarkd-editor': ZnRemarkdEditor,
67
69
  'zn-select': ZnSelect,
68
70
  'zn-textarea': ZnTextarea
69
71
  };
@@ -96,11 +98,23 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
96
98
  @property({type: Boolean, reflect: true}) flush = false;
97
99
 
98
100
  /** The control each translation is edited through. */
99
- @property({attribute: "input-type"}) inputType: 'text' | 'number' | 'textarea' = 'text';
101
+ @property({attribute: "input-type"}) inputType: 'text' | 'number' | 'textarea' | 'remarkd' = 'text';
100
102
 
101
103
  /** Rows of the textarea, when `input-type` is `textarea`. */
102
104
  @property({attribute: "textarea-rows", type: Number}) textareaRows: number | undefined;
103
105
 
106
+ /** Allows raw HTML blocks, when `input-type` is `remarkd`. */
107
+ @property({type: Boolean, attribute: 'allow-raw'}) allowRaw = false;
108
+
109
+ /** Where the remarkd editor uploads images, when `input-type` is `remarkd`. */
110
+ @property({attribute: 'attachment-url'}) attachmentUrl = '';
111
+
112
+ /** Where the remarkd editor lists embeddable includes, when `input-type` is `remarkd`. */
113
+ @property({attribute: 'include-url'}) includeUrl = '';
114
+
115
+ /** Where the remarkd editor searches link targets, when `input-type` is `remarkd`. */
116
+ @property({attribute: 'link-url'}) linkUrl = '';
117
+
104
118
  /**
105
119
  * Edits the translation through a `zn-inline-edit` — the value reads as text until it is clicked — rather than a
106
120
  * plain input or textarea.
@@ -325,7 +339,7 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
325
339
  };
326
340
 
327
341
  private handleValueUpdate = (e: CustomEvent) => {
328
- const target = e.target as (ZnInput | ZnInlineEdit | ZnTextarea);
342
+ const target = e.target as (ZnInput | ZnInlineEdit | ZnRemarkdEditor | ZnTextarea);
329
343
  if (this._activeLanguage) {
330
344
  const newValue: string = target.value as string;
331
345
  if (newValue !== this.values[this._activeLanguage]) {
@@ -387,6 +401,23 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
387
401
  ></zn-inline-edit>`;
388
402
  }
389
403
 
404
+ if (this.inputType === 'remarkd') {
405
+ return html`
406
+ <zn-remarkd-editor
407
+ .value=${live(value)}
408
+ name="${this.name}"
409
+ placeholder="${placeholder}"
410
+ dir="${dir}"
411
+ ?disabled="${this.disabled}"
412
+ ?allow-raw="${this.allowRaw}"
413
+ attachment-url="${this.attachmentUrl}"
414
+ include-url="${this.includeUrl}"
415
+ link-url="${this.linkUrl}"
416
+ @zn-change="${this.handleValueUpdate}"
417
+ @zn-input="${this.handleValueUpdate}"
418
+ ></zn-remarkd-editor>`;
419
+ }
420
+
390
421
  if (this.inputType === 'textarea') {
391
422
  return html`
392
423
  <zn-textarea
@@ -219,6 +219,47 @@ describe('<zn-translations>', () => {
219
219
  expect(el.shadowRoot!.querySelector('zn-input')).to.be.null;
220
220
  });
221
221
 
222
+ it('renders a remarkd editor for input-type="remarkd"', async () => {
223
+ const el = await fixture<ZnTranslations>(html`
224
+ <zn-translations input-type="remarkd" values='{"en":"= Hello"}'></zn-translations>`);
225
+ await el.updateComplete;
226
+
227
+ const editor = el.shadowRoot!.querySelector<HTMLElement & { value: string }>('zn-remarkd-editor');
228
+ expect(editor).to.exist;
229
+ expect(editor!.value).to.equal('= Hello');
230
+ expect(el.shadowRoot!.querySelector('zn-input')).to.be.null;
231
+ expect(el.shadowRoot!.querySelector('zn-textarea')).to.be.null;
232
+ });
233
+
234
+ it('passes its editor endpoints through to the remarkd editor', async () => {
235
+ const el = await fixture<ZnTranslations>(html`
236
+ <zn-translations input-type="remarkd"
237
+ allow-raw
238
+ attachment-url="/a"
239
+ include-url="/i"
240
+ link-url="/l"></zn-translations>`);
241
+ await el.updateComplete;
242
+
243
+ const editor = el.shadowRoot!.querySelector('zn-remarkd-editor')!;
244
+ expect(editor.getAttribute('attachment-url')).to.equal('/a');
245
+ expect(editor.getAttribute('include-url')).to.equal('/i');
246
+ expect(editor.getAttribute('link-url')).to.equal('/l');
247
+ expect(editor.hasAttribute('allow-raw')).to.be.true;
248
+ });
249
+
250
+ it('writes a remarkd edit back to the active language', async () => {
251
+ const el = await fixture<ZnTranslations>(html`
252
+ <zn-translations input-type="remarkd" values='{"en":"one"}'></zn-translations>`);
253
+ await el.updateComplete;
254
+
255
+ const editor = el.shadowRoot!.querySelector<HTMLElement & { value: string }>('zn-remarkd-editor')!;
256
+ editor.value = 'two';
257
+ editor.dispatchEvent(new CustomEvent('zn-change', {bubbles: true, composed: true}));
258
+ await el.updateComplete;
259
+
260
+ expect(el.values.en).to.equal('two');
261
+ });
262
+
222
263
  it('renders an inline edit when asked for one', async () => {
223
264
  const el = await fixture<ZnTranslations>(html`
224
265
  <zn-translations inline-edit values='{"en":"Hello"}'></zn-translations>`);