@kubex/zinc 1.1.162 → 1.1.164

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.
@@ -1,6 +1,6 @@
1
1
  import '../../../dist/zn.min.js';
2
2
  import {aTimeout, expect, fixture, html, waitUntil} from '@open-wc/testing';
3
- import {EDITOR_ACTIONS} from './actions';
3
+ import {EDITOR_ACTIONS, isApple} from './actions';
4
4
  import {FEATURE_KEYS} from './feature-keys';
5
5
  import {parse as remarkdParse} from 'remarkd-js';
6
6
  import type ZnButton from '../button';
@@ -352,8 +352,10 @@ Second"></zn-remarkd-editor>`);
352
352
  .filter(group => getComputedStyle(group).display === 'none');
353
353
  expect(collapsed.length, 'nothing collapsed at 240px').to.be.greaterThan(0);
354
354
 
355
- // No action is lost: the collapsed groups' actions are all in the menu.
356
- const menuItems = el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar-more zn-menu-item');
355
+ // One top-level entry per collapsed group, each holding that group's actions.
356
+ const groupItems = el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar-more > zn-menu > zn-menu-item');
357
+ expect(groupItems.length).to.equal(collapsed.length);
358
+ const menuItems = el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar-more zn-menu[slot="submenu"] zn-menu-item');
357
359
  expect(menuItems.length).to.be.greaterThan(0);
358
360
 
359
361
  // The trigger itself must stay on-screen — a group that doesn't fit must not force its
@@ -372,7 +374,7 @@ Second"></zn-remarkd-editor>`);
372
374
  .to.equal(expectedCount);
373
375
  });
374
376
 
375
- it('should scroll the overflow menu instead of running off the viewport', async () => {
377
+ it('should cap each overflow submenu so it scrolls instead of running off the viewport', async () => {
376
378
  const el = await fixture<ZnRemarkdEditor>(html`
377
379
  <zn-remarkd-editor style="width: 240px"></zn-remarkd-editor>`);
378
380
  await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__toolbar-more'),
@@ -384,14 +386,14 @@ Second"></zn-remarkd-editor>`);
384
386
  // wait used for the same reason in expanding-action's drop-panel overflow tests.
385
387
  await new Promise(resolve => requestAnimationFrame(resolve));
386
388
 
387
- // At 240px, every action lands in the menu — enough zn-menu-items to overflow the
388
- // max-height. The scroll container is zn-menu's own inner `.menu` div (set via the
389
- // --zn-menu-max-height custom property), not the zn-menu host itself: a constraint on
390
- // the host leaves the host with room to spare, so a wheel over an item finds `.menu`
391
- // has nothing to scroll and never chains out to the host.
392
- const menu = dropdown.querySelector('zn-menu')!;
389
+ // Inline is the longest group, so its submenu is the one that can outgrow the viewport.
390
+ // The cap lands on zn-menu's inner `.menu` div (via --zn-menu-max-height), not the host:
391
+ // a constraint on the host leaves `.menu` with nothing to scroll.
392
+ const inlineItem = [...dropdown.querySelectorAll(':scope > zn-menu > zn-menu-item')]
393
+ .find(item => item.textContent?.includes('Strong'))!;
394
+ const menu = inlineItem.querySelector('zn-menu[slot="submenu"]')!;
393
395
  const inner = menu.shadowRoot!.querySelector<HTMLElement>('.menu')!;
394
- expect(inner.scrollHeight, 'the menu needs room to scroll').to.be.greaterThan(inner.clientHeight);
396
+ expect(getComputedStyle(inner).maxHeight).to.not.equal('none');
395
397
  expect(getComputedStyle(inner).overflow).to.equal('auto');
396
398
  });
397
399
 
@@ -416,7 +418,7 @@ Second"></zn-remarkd-editor>`);
416
418
  // trigger wouldn't actually move focus in a test DOM, so the blur is reproduced directly.
417
419
  input.dispatchEvent(new Event('blur'));
418
420
 
419
- const strong = [...dropdown.querySelectorAll('zn-menu-item')]
421
+ const strong = [...dropdown.querySelectorAll('zn-menu[slot="submenu"] zn-menu-item')]
420
422
  .find(item => item.textContent?.includes('Strong'));
421
423
  expect(strong, 'Strong should be reachable through the overflow menu').to.exist;
422
424
  strong!.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
@@ -482,7 +484,7 @@ Second"></zn-remarkd-editor>`);
482
484
  const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
483
485
  input.setSelectionRange(0, 5);
484
486
  const strong = [...el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button')]
485
- .find(b => b.getAttribute('tooltip') === 'Strong')!;
487
+ .find(b => b.getAttribute('label') === 'Strong')!;
486
488
  strong.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
487
489
  await el.updateComplete;
488
490
 
@@ -492,6 +494,122 @@ Second"></zn-remarkd-editor>`);
492
494
  expect([result.selectionStart, result.selectionEnd]).to.eql([2, 7]);
493
495
  });
494
496
 
497
+ describe('keyboard shortcuts', () => {
498
+ const apple = isApple();
499
+ const press = (input: HTMLTextAreaElement, key: string, init: KeyboardEventInit = {}) => {
500
+ const event = new KeyboardEvent('keydown', {key, bubbles: true, composed: true, cancelable: true, ...init});
501
+ input.dispatchEvent(event);
502
+ return event;
503
+ };
504
+ const mod = (init: KeyboardEventInit = {}): KeyboardEventInit =>
505
+ apple ? {metaKey: true, ...init} : {ctrlKey: true, ...init};
506
+
507
+ async function editing(value: string, start: number, end: number) {
508
+ const el = await fixture<ZnRemarkdEditor>(html`
509
+ <zn-remarkd-editor value=${value}></zn-remarkd-editor>`);
510
+ el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
511
+ await el.updateComplete;
512
+ const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
513
+ input.setSelectionRange(start, end);
514
+ return {el, input};
515
+ }
516
+
517
+ it('should bold the selection with the platform modifier + B', async () => {
518
+ const {el, input} = await editing('hello world', 0, 5);
519
+ const event = press(input, 'b', mod());
520
+ await el.updateComplete;
521
+ expect(event.defaultPrevented).to.be.true;
522
+ expect(input.value).to.equal('**hello** world');
523
+ });
524
+
525
+ it('should toggle the mark back off on a second press', async () => {
526
+ const {el, input} = await editing('hello world', 0, 5);
527
+ press(input, 'b', mod());
528
+ await el.updateComplete;
529
+ press(input, 'b', mod());
530
+ await el.updateComplete;
531
+ expect(input.value).to.equal('hello world');
532
+ });
533
+
534
+ it('should map shift shortcuts, e.g. strikethrough', async () => {
535
+ const {el, input} = await editing('hello world', 0, 5);
536
+ press(input, 'X', mod({shiftKey: true}));
537
+ await el.updateComplete;
538
+ expect(input.value).to.equal('~~hello~~ world');
539
+ });
540
+
541
+ it('should ignore the other platform\'s modifier', async () => {
542
+ const {el, input} = await editing('hello world', 0, 5);
543
+ const event = press(input, 'b', apple ? {ctrlKey: true} : {metaKey: true});
544
+ await el.updateComplete;
545
+ expect(event.defaultPrevented).to.be.false;
546
+ expect(input.value).to.equal('hello world');
547
+ });
548
+
549
+ it('should ignore AltGr (Ctrl+Alt) combinations', async () => {
550
+ const {el, input} = await editing('hello world', 0, 5);
551
+ press(input, 'b', mod({altKey: true}));
552
+ await el.updateComplete;
553
+ expect(input.value).to.equal('hello world');
554
+ });
555
+
556
+ it('should show the shortcut in the toolbar tooltip', async () => {
557
+ const el = await fixture<ZnRemarkdEditor>(html`<zn-remarkd-editor></zn-remarkd-editor>`);
558
+ const strong = [...el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button')]
559
+ .find(b => b.getAttribute('label') === 'Strong')!;
560
+ expect(strong.getAttribute('tooltip')).to.equal(apple ? 'Strong (⌘B)' : 'Strong (Ctrl+B)');
561
+ });
562
+ });
563
+
564
+ describe('list continuation', () => {
565
+ const enter = (input: HTMLTextAreaElement) => {
566
+ const event = new KeyboardEvent('keydown', {key: 'Enter', bubbles: true, composed: true, cancelable: true});
567
+ input.dispatchEvent(event);
568
+ return event;
569
+ };
570
+
571
+ it('should continue a numbered list on Enter in the open block', async () => {
572
+ const el = await fixture<ZnRemarkdEditor>(html`
573
+ <zn-remarkd-editor value="1. hello"></zn-remarkd-editor>`);
574
+ el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
575
+ await el.updateComplete;
576
+ const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
577
+ input.setSelectionRange(8, 8);
578
+ let inputs = 0;
579
+ el.addEventListener('zn-input', () => inputs++);
580
+
581
+ expect(enter(input).defaultPrevented).to.be.true;
582
+ await el.updateComplete;
583
+ expect(input.value).to.equal('1. hello\n2. ');
584
+ expect(input.selectionStart).to.equal(12);
585
+ expect(inputs).to.equal(1);
586
+ });
587
+
588
+ it('should leave Enter as a plain newline outside a list', async () => {
589
+ const el = await fixture<ZnRemarkdEditor>(html`
590
+ <zn-remarkd-editor value="hello"></zn-remarkd-editor>`);
591
+ el.shadowRoot!.querySelector<HTMLElement>('.remarkd-editor__rendered')!.click();
592
+ await el.updateComplete;
593
+ const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
594
+ input.setSelectionRange(5, 5);
595
+ expect(enter(input).defaultPrevented).to.be.false;
596
+ });
597
+
598
+ it('should continue a list on Enter in the raw source view', async () => {
599
+ const el = await fixture<ZnRemarkdEditor>(html`
600
+ <zn-remarkd-editor allow-raw value="- [ ] first"></zn-remarkd-editor>`);
601
+ el.shadowRoot!.querySelector('.remarkd-editor__raw-toggle')!
602
+ .dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
603
+ await el.updateComplete;
604
+ const raw = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__raw')!;
605
+ raw.setSelectionRange(11, 11);
606
+
607
+ expect(enter(raw).defaultPrevented).to.be.true;
608
+ await el.updateComplete;
609
+ expect(el.value).to.equal('- [ ] first\n- [ ] ');
610
+ });
611
+ });
612
+
495
613
  it('should insert a placeholder when nothing is selected', async () => {
496
614
  const el = await fixture<ZnRemarkdEditor>(html`
497
615
  <zn-remarkd-editor value="hello"></zn-remarkd-editor>`);
@@ -501,7 +619,7 @@ Second"></zn-remarkd-editor>`);
501
619
  input.setSelectionRange(5, 5);
502
620
 
503
621
  [...el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button')]
504
- .find(b => b.getAttribute('tooltip') === 'Strong')!
622
+ .find(b => b.getAttribute('label') === 'Strong')!
505
623
  .dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
506
624
  await el.updateComplete;
507
625
 
@@ -519,7 +637,7 @@ Second"></zn-remarkd-editor>`);
519
637
  const input = el.shadowRoot!.querySelector<HTMLTextAreaElement>('.remarkd-editor__input')!;
520
638
  input.setSelectionRange(2, 7); // hello, inside the marks
521
639
  [...el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button')]
522
- .find(b => b.getAttribute('tooltip') === 'Strong')!
640
+ .find(b => b.getAttribute('label') === 'Strong')!
523
641
  .dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
524
642
  await el.updateComplete;
525
643
 
@@ -536,7 +654,7 @@ Second"></zn-remarkd-editor>`);
536
654
  input.setSelectionRange(2, 7); // hello, inside the strike marks
537
655
 
538
656
  [...el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button')]
539
- .find(b => b.getAttribute('tooltip') === 'Subscript')!
657
+ .find(b => b.getAttribute('label') === 'Subscript')!
540
658
  .dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
541
659
  await el.updateComplete;
542
660
 
@@ -549,11 +667,11 @@ Second"></zn-remarkd-editor>`);
549
667
  const el = await fixture<ZnRemarkdEditor>(html`
550
668
  <zn-remarkd-editor value="hello" link-url="/links"></zn-remarkd-editor>`);
551
669
  const strong = [...el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button')]
552
- .find(b => b.getAttribute('tooltip') === 'Strong')!;
670
+ .find(b => b.getAttribute('label') === 'Strong')!;
553
671
  expect(strong.hasAttribute('disabled')).to.be.true;
554
672
 
555
673
  const link = [...el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button')]
556
- .find(b => b.getAttribute('tooltip') === 'Link to article')!;
674
+ .find(b => b.getAttribute('label') === 'Link to article')!;
557
675
  expect(link.hasAttribute('disabled')).to.be.true;
558
676
  });
559
677
 
@@ -566,7 +684,7 @@ Second"></zn-remarkd-editor>`);
566
684
  input.setSelectionRange(2, 7); // Label, inside the link markup
567
685
 
568
686
  [...el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button')]
569
- .find(b => b.getAttribute('tooltip') === 'Link')!
687
+ .find(b => b.getAttribute('label') === 'Link')!
570
688
  .dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
571
689
  await el.updateComplete;
572
690
 
@@ -587,7 +705,7 @@ Second"></zn-remarkd-editor>`);
587
705
  input.setSelectionRange(1, 6); // Label, inside the real link markup
588
706
 
589
707
  [...el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button')]
590
- .find(b => b.getAttribute('tooltip') === 'Link')!
708
+ .find(b => b.getAttribute('label') === 'Link')!
591
709
  .dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
592
710
  await el.updateComplete;
593
711
 
@@ -601,7 +719,7 @@ Second"></zn-remarkd-editor>`);
601
719
  // Two actions share the "Code" tooltip (inline-code in the inline group, code-fence in
602
720
  // the blocks group) — disambiguate by the group the button actually lives in.
603
721
  const codeButton = [...el.shadowRoot!.querySelectorAll<HTMLElement>('.remarkd-editor__toolbar zn-button')]
604
- .find(b => b.getAttribute('tooltip') === 'Code'
722
+ .find(b => b.getAttribute('label') === 'Code'
605
723
  && b.closest('.toolbar__group')?.getAttribute('data-group') === 'blocks')!;
606
724
  codeButton.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
607
725
  await el.updateComplete;
@@ -619,7 +737,7 @@ Second"></zn-remarkd-editor>`);
619
737
  await el.updateComplete;
620
738
 
621
739
  const strong = [...el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button')]
622
- .find(b => b.getAttribute('tooltip') === 'Strong')!;
740
+ .find(b => b.getAttribute('label') === 'Strong')!;
623
741
  const mousedown = new MouseEvent('mousedown', {bubbles: true, composed: true, cancelable: true});
624
742
  strong.dispatchEvent(mousedown);
625
743
 
@@ -900,7 +1018,7 @@ Second"></zn-remarkd-editor>`);
900
1018
  it('should show an inline zn-file picker from the toolbar image button', async () => {
901
1019
  const el = await fixture<ZnRemarkdEditor>(html`
902
1020
  <zn-remarkd-editor attachment-url="/upload"></zn-remarkd-editor>`);
903
- const imageButton = el.shadowRoot!.querySelector('.remarkd-editor__toolbar zn-button[tooltip="Image"]')!;
1021
+ const imageButton = el.shadowRoot!.querySelector('.remarkd-editor__toolbar zn-button[label="Image"]')!;
904
1022
  imageButton.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
905
1023
  await el.updateComplete;
906
1024
 
@@ -1139,7 +1257,7 @@ include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
1139
1257
  <zn-remarkd-editor include-url="/options" value="# Title"></zn-remarkd-editor>`);
1140
1258
 
1141
1259
  const includeButton = el.shadowRoot!
1142
- .querySelector<HTMLElement>('.remarkd-editor__toolbar zn-button[tooltip="Include"]')!;
1260
+ .querySelector<HTMLElement>('.remarkd-editor__toolbar zn-button[label="Include"]')!;
1143
1261
  includeButton.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
1144
1262
 
1145
1263
  await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__include-option'),
@@ -1176,7 +1294,7 @@ include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
1176
1294
  const el = await fixture<ZnRemarkdEditor>(html`
1177
1295
  <zn-remarkd-editor include-url="/options" value="# Title"></zn-remarkd-editor>`);
1178
1296
  const includeButton = el.shadowRoot!
1179
- .querySelector<HTMLElement>('.remarkd-editor__toolbar zn-button[tooltip="Include"]')!;
1297
+ .querySelector<HTMLElement>('.remarkd-editor__toolbar zn-button[label="Include"]')!;
1180
1298
  includeButton.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
1181
1299
  await waitUntil(() => el.shadowRoot!.querySelectorAll('.remarkd-editor__include-option').length === 2,
1182
1300
  'the include picker never listed both');
@@ -1195,7 +1313,7 @@ include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
1195
1313
  const el = await fixture<ZnRemarkdEditor>(html`
1196
1314
  <zn-remarkd-editor value="# Title"></zn-remarkd-editor>`);
1197
1315
  const tooltips = Array.from(el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button'))
1198
- .map(button => button.getAttribute('tooltip'));
1316
+ .map(button => button.getAttribute('label'));
1199
1317
  expect(tooltips).to.not.contain('Image');
1200
1318
  });
1201
1319
 
@@ -1203,7 +1321,7 @@ include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
1203
1321
  const el = await fixture<ZnRemarkdEditor>(html`
1204
1322
  <zn-remarkd-editor value="# Title" attachment-url="/attachments"></zn-remarkd-editor>`);
1205
1323
  const tooltips = Array.from(el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button'))
1206
- .map(button => button.getAttribute('tooltip'));
1324
+ .map(button => button.getAttribute('label'));
1207
1325
  expect(tooltips).to.contain('Image');
1208
1326
  });
1209
1327
 
@@ -1211,7 +1329,7 @@ include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
1211
1329
  const el = await fixture<ZnRemarkdEditor>(html`
1212
1330
  <zn-remarkd-editor value="# Title"></zn-remarkd-editor>`);
1213
1331
  const tooltips = Array.from(el.shadowRoot!.querySelectorAll('.remarkd-editor__toolbar zn-button'))
1214
- .map(button => button.getAttribute('tooltip'));
1332
+ .map(button => button.getAttribute('label'));
1215
1333
  expect(tooltips).to.not.contain('Include');
1216
1334
  });
1217
1335
  // A list that never arrived is not evidence an embed is broken: flagging every
@@ -1242,7 +1360,7 @@ include::inc-1[Payment Terms]"></zn-remarkd-editor>`);
1242
1360
  const el = await fixture<ZnRemarkdEditor>(html`
1243
1361
  <zn-remarkd-editor include-url="/options" value="# Title"></zn-remarkd-editor>`);
1244
1362
  const includeButton = el.shadowRoot!
1245
- .querySelector<HTMLElement>('.remarkd-editor__toolbar zn-button[tooltip="Include"]')!;
1363
+ .querySelector<HTMLElement>('.remarkd-editor__toolbar zn-button[label="Include"]')!;
1246
1364
  includeButton.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
1247
1365
 
1248
1366
  await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__include-picker-empty')?.textContent
@@ -1461,7 +1579,7 @@ const x = {product};
1461
1579
  await dropdown.show();
1462
1580
  await new Promise(resolve => requestAnimationFrame(resolve));
1463
1581
 
1464
- const linkItem = [...dropdown.querySelectorAll('zn-menu-item')]
1582
+ const linkItem = [...dropdown.querySelectorAll('zn-menu[slot="submenu"] zn-menu-item')]
1465
1583
  .find(item => item.textContent?.includes('Link to article'));
1466
1584
  expect(linkItem, 'the article-link action should be reachable through the overflow menu').to.exist;
1467
1585
  // ZnButton overrides click() without dispatching a DOM event, so the menu item's
@@ -1584,7 +1702,7 @@ const x = {product};
1584
1702
  await el.updateComplete;
1585
1703
  const input = typeInBlock(el, text);
1586
1704
  input.setSelectionRange(from, to);
1587
- el.shadowRoot!.querySelector<ZnButton>('zn-button[tooltip="Link to article"]')!
1705
+ el.shadowRoot!.querySelector<ZnButton>('zn-button[label="Link to article"]')!
1588
1706
  .dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
1589
1707
  await waitUntil(() => el.shadowRoot!.querySelector('.remarkd-editor__link-picker'),
1590
1708
  'the link picker never opened');
@@ -21,3 +21,4 @@ export type {ZnThemeChangeEvent} from './zn-theme-change';
21
21
  export type {ZnThemeSubmitEvent} from './zn-theme-submit';
22
22
  export type {ZnSlashSelectEvent} from './zn-slash-select';
23
23
  export type {ZnSlashInsertEvent} from './zn-slash-insert';
24
+ export type {ZnRepositionEvent} from './zn-reposition';
@@ -0,0 +1,7 @@
1
+ export type ZnRepositionEvent = CustomEvent<Record<PropertyKey, never>>;
2
+
3
+ declare global {
4
+ interface GlobalEventHandlersEventMap {
5
+ 'zn-reposition': ZnRepositionEvent;
6
+ }
7
+ }