@domternal/angular 0.4.1 → 0.5.0

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/README.md CHANGED
@@ -24,7 +24,7 @@ See <u>[Packages & Bundle Size](https://domternal.dev/v1/packages)</u> for a ful
24
24
  - **Tree-shakeable** - import only what you use, your bundler strips the rest
25
25
  - **~38 KB gzipped** (own code), <u>[~108 KB total](https://domternal.dev/v1/packages)</u> with ProseMirror
26
26
  - **TypeScript first** - 100% typed, zero `any`
27
- - **7,500+ tests** - 3,936 unit tests and 3,652 E2E tests across 76 Playwright specs
27
+ - **6,400+ tests** - 2,677 unit tests and 3,767 E2E tests across 78 Playwright specs
28
28
  - **Light and dark theme** - 70+ CSS custom properties for full visual control
29
29
  - **Inline styles export** - `getHTML({ styled: true })` produces inline CSS ready for email clients, CMS, and Google Docs
30
30
  - **SSR helpers** - `generateHTML`, `generateJSON`, `generateText` for server-side rendering
package/dist/README.md CHANGED
@@ -24,7 +24,7 @@ See <u>[Packages & Bundle Size](https://domternal.dev/v1/packages)</u> for a ful
24
24
  - **Tree-shakeable** - import only what you use, your bundler strips the rest
25
25
  - **~38 KB gzipped** (own code), <u>[~108 KB total](https://domternal.dev/v1/packages)</u> with ProseMirror
26
26
  - **TypeScript first** - 100% typed, zero `any`
27
- - **7,500+ tests** - 3,936 unit tests and 3,652 E2E tests across 76 Playwright specs
27
+ - **6,400+ tests** - 2,677 unit tests and 3,767 E2E tests across 78 Playwright specs
28
28
  - **Light and dark theme** - 70+ CSS custom properties for full visual control
29
29
  - **Inline styles export** - `getHTML({ styled: true })` produces inline CSS ready for email clients, CMS, and Google Docs
30
30
  - **SSR helpers** - `generateHTML`, `generateJSON`, `generateText` for server-side rendering
@@ -470,6 +470,28 @@ class DomternalToolbarComponent {
470
470
  this.controller.navigateLast();
471
471
  this.focusCurrentButton();
472
472
  break;
473
+ case 'ArrowDown': {
474
+ event.preventDefault();
475
+ if (this.openDropdown()) {
476
+ this.focusDropdownItem(1);
477
+ }
478
+ else {
479
+ const buttons = this.elRef.nativeElement.querySelectorAll('.dm-toolbar-button');
480
+ const btn = buttons[this.controller?.focusedIndex ?? 0];
481
+ if (btn?.getAttribute('aria-haspopup')) {
482
+ btn.click();
483
+ requestAnimationFrame(() => this.focusDropdownItem(0, true));
484
+ }
485
+ }
486
+ break;
487
+ }
488
+ case 'ArrowUp': {
489
+ event.preventDefault();
490
+ if (this.openDropdown()) {
491
+ this.focusDropdownItem(-1);
492
+ }
493
+ break;
494
+ }
473
495
  case 'Escape':
474
496
  if (this.openDropdown()) {
475
497
  event.preventDefault();
@@ -483,6 +505,24 @@ class DomternalToolbarComponent {
483
505
  }
484
506
  }
485
507
  // === Private ===
508
+ focusDropdownItem(direction, first) {
509
+ const panel = this.elRef.nativeElement.querySelector('.dm-toolbar-dropdown-panel');
510
+ if (!panel)
511
+ return;
512
+ const items = Array.from(panel.querySelectorAll('[role="menuitem"]'));
513
+ if (!items.length)
514
+ return;
515
+ if (first) {
516
+ items[0]?.focus();
517
+ return;
518
+ }
519
+ const current = document.activeElement;
520
+ const idx = items.indexOf(current);
521
+ const next = idx === -1
522
+ ? (direction > 0 ? 0 : items.length - 1)
523
+ : (idx + direction + items.length) % items.length;
524
+ items[next]?.focus();
525
+ }
486
526
  resolveIconSvg(name) {
487
527
  const customIcons = this.icons();
488
528
  if (customIcons) {
@@ -642,6 +682,7 @@ class DomternalToolbarComponent {
642
682
  class="dm-color-swatch"
643
683
  [class.dm-color-swatch--active]="isActive(sub.name)"
644
684
  role="menuitem"
685
+ [attr.tabindex]="-1"
645
686
  [attr.aria-label]="sub.label"
646
687
  [title]="sub.label"
647
688
  [style.background-color]="sub.color"
@@ -653,8 +694,10 @@ class DomternalToolbarComponent {
653
694
  type="button"
654
695
  class="dm-color-palette-reset"
655
696
  role="menuitem"
697
+ [attr.tabindex]="-1"
656
698
  [attr.aria-label]="sub.label"
657
699
  [innerHTML]="getCachedItemContent(sub.icon, sub.label)"
700
+ [attr.tabindex]="-1"
658
701
  (mousedown)="$event.preventDefault()"
659
702
  (click)="onDropdownItemClick(sub)"
660
703
  ></button>
@@ -670,6 +713,7 @@ class DomternalToolbarComponent {
670
713
  class="dm-toolbar-dropdown-item"
671
714
  [class.dm-toolbar-dropdown-item--active]="isActive(sub.name)"
672
715
  role="menuitem"
716
+ [attr.tabindex]="-1"
673
717
  [attr.aria-label]="sub.label"
674
718
  [attr.style]="sub.style ?? null"
675
719
  [innerHTML]="getCachedItemContent(sub.icon, sub.label, asDropdown(item).displayMode)"
@@ -752,6 +796,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
752
796
  class="dm-color-swatch"
753
797
  [class.dm-color-swatch--active]="isActive(sub.name)"
754
798
  role="menuitem"
799
+ [attr.tabindex]="-1"
755
800
  [attr.aria-label]="sub.label"
756
801
  [title]="sub.label"
757
802
  [style.background-color]="sub.color"
@@ -763,8 +808,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
763
808
  type="button"
764
809
  class="dm-color-palette-reset"
765
810
  role="menuitem"
811
+ [attr.tabindex]="-1"
766
812
  [attr.aria-label]="sub.label"
767
813
  [innerHTML]="getCachedItemContent(sub.icon, sub.label)"
814
+ [attr.tabindex]="-1"
768
815
  (mousedown)="$event.preventDefault()"
769
816
  (click)="onDropdownItemClick(sub)"
770
817
  ></button>
@@ -780,6 +827,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
780
827
  class="dm-toolbar-dropdown-item"
781
828
  [class.dm-toolbar-dropdown-item--active]="isActive(sub.name)"
782
829
  role="menuitem"
830
+ [attr.tabindex]="-1"
783
831
  [attr.aria-label]="sub.label"
784
832
  [attr.style]="sub.style ?? null"
785
833
  [innerHTML]="getCachedItemContent(sub.icon, sub.label, asDropdown(item).displayMode)"
@@ -1115,13 +1163,14 @@ class DomternalBubbleMenuComponent {
1115
1163
  }
1116
1164
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalBubbleMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1117
1165
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: DomternalBubbleMenuComponent, isStandalone: true, selector: "domternal-bubble-menu", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, shouldShow: { classPropertyName: "shouldShow", publicName: "shouldShow", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, updateDelay: { classPropertyName: "updateDelay", publicName: "updateDelay", isSignal: true, isRequired: false, transformFunction: null }, items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, contexts: { classPropertyName: "contexts", publicName: "contexts", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "menuEl", first: true, predicate: ["menuEl"], descendants: true, isSignal: true }], ngImport: i0, template: `
1118
- <div #menuEl class="dm-bubble-menu">
1166
+ <div #menuEl class="dm-bubble-menu" role="toolbar" aria-label="Text formatting">
1119
1167
  @for (item of resolvedItems(); track item.name) {
1120
1168
  @if (item.type === 'separator') {
1121
- <span class="dm-toolbar-separator"></span>
1169
+ <span class="dm-toolbar-separator" role="separator"></span>
1122
1170
  } @else {
1123
1171
  <button type="button" class="dm-toolbar-button"
1124
1172
  [class.dm-toolbar-button--active]="isItemActive(item)"
1173
+ [attr.aria-pressed]="isItemActive(item)"
1125
1174
  [disabled]="isItemDisabled(item)"
1126
1175
  [title]="item.label"
1127
1176
  [attr.aria-label]="item.label"
@@ -1137,13 +1186,14 @@ class DomternalBubbleMenuComponent {
1137
1186
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: DomternalBubbleMenuComponent, decorators: [{
1138
1187
  type: Component,
1139
1188
  args: [{ selector: 'domternal-bubble-menu', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
1140
- <div #menuEl class="dm-bubble-menu">
1189
+ <div #menuEl class="dm-bubble-menu" role="toolbar" aria-label="Text formatting">
1141
1190
  @for (item of resolvedItems(); track item.name) {
1142
1191
  @if (item.type === 'separator') {
1143
- <span class="dm-toolbar-separator"></span>
1192
+ <span class="dm-toolbar-separator" role="separator"></span>
1144
1193
  } @else {
1145
1194
  <button type="button" class="dm-toolbar-button"
1146
1195
  [class.dm-toolbar-button--active]="isItemActive(item)"
1196
+ [attr.aria-pressed]="isItemActive(item)"
1147
1197
  [disabled]="isItemDisabled(item)"
1148
1198
  [title]="item.label"
1149
1199
  [attr.aria-label]="item.label"
@@ -1301,6 +1351,45 @@ class DomternalEmojiPickerComponent {
1301
1351
  }
1302
1352
  });
1303
1353
  }
1354
+ onGridKeydown(event) {
1355
+ const grid = this.elRef.nativeElement.querySelector('.dm-emoji-picker-grid');
1356
+ if (!grid)
1357
+ return;
1358
+ const swatches = Array.from(grid.querySelectorAll('.dm-emoji-swatch'));
1359
+ if (!swatches.length)
1360
+ return;
1361
+ const current = document.activeElement;
1362
+ const idx = swatches.indexOf(current);
1363
+ if (idx === -1)
1364
+ return;
1365
+ const cols = 8;
1366
+ let next = idx;
1367
+ switch (event.key) {
1368
+ case 'ArrowRight':
1369
+ event.preventDefault();
1370
+ next = Math.min(idx + 1, swatches.length - 1);
1371
+ break;
1372
+ case 'ArrowLeft':
1373
+ event.preventDefault();
1374
+ next = Math.max(idx - 1, 0);
1375
+ break;
1376
+ case 'ArrowDown':
1377
+ event.preventDefault();
1378
+ next = Math.min(idx + cols, swatches.length - 1);
1379
+ break;
1380
+ case 'ArrowUp':
1381
+ event.preventDefault();
1382
+ next = Math.max(idx - cols, 0);
1383
+ break;
1384
+ case 'Enter':
1385
+ case ' ':
1386
+ event.preventDefault();
1387
+ swatches[idx]?.click();
1388
+ return;
1389
+ default: return;
1390
+ }
1391
+ swatches[next]?.focus();
1392
+ }
1304
1393
  onGridScroll() {
1305
1394
  const grid = this.elRef.nativeElement.querySelector('.dm-emoji-picker-grid');
1306
1395
  if (!grid || this.searchQuery())
@@ -1423,6 +1512,7 @@ class DomternalEmojiPickerComponent {
1423
1512
  placeholder="Search emoji..."
1424
1513
  [value]="searchQuery()"
1425
1514
  (input)="onSearch($event)"
1515
+ aria-label="Search emoji"
1426
1516
  (keydown.escape)="close()"
1427
1517
  />
1428
1518
  </div>
@@ -1433,6 +1523,8 @@ class DomternalEmojiPickerComponent {
1433
1523
  type="button"
1434
1524
  class="dm-emoji-picker-tab"
1435
1525
  [class.dm-emoji-picker-tab--active]="activeCategory() === cat"
1526
+ role="tab"
1527
+ [attr.aria-selected]="activeCategory() === cat"
1436
1528
  [title]="cat"
1437
1529
  [attr.aria-label]="cat"
1438
1530
  (mousedown)="$event.preventDefault()"
@@ -1441,12 +1533,13 @@ class DomternalEmojiPickerComponent {
1441
1533
  }
1442
1534
  </div>
1443
1535
 
1444
- <div class="dm-emoji-picker-grid" #grid (scroll)="onGridScroll()">
1536
+ <div class="dm-emoji-picker-grid" #grid (scroll)="onGridScroll()" (keydown)="onGridKeydown($event)">
1445
1537
  @if (searchQuery()) {
1446
1538
  @for (item of filteredEmojis(); track item.name) {
1447
1539
  <button
1448
1540
  type="button"
1449
1541
  class="dm-emoji-swatch"
1542
+ [attr.tabindex]="-1"
1450
1543
  [title]="formatName(item.name)"
1451
1544
  [attr.aria-label]="formatName(item.name)"
1452
1545
  (mousedown)="$event.preventDefault()"
@@ -1463,6 +1556,7 @@ class DomternalEmojiPickerComponent {
1463
1556
  <button
1464
1557
  type="button"
1465
1558
  class="dm-emoji-swatch"
1559
+ [attr.tabindex]="-1"
1466
1560
  [title]="formatName(item.name)"
1467
1561
  [attr.aria-label]="formatName(item.name)"
1468
1562
  (mousedown)="$event.preventDefault()"
@@ -1476,6 +1570,7 @@ class DomternalEmojiPickerComponent {
1476
1570
  <button
1477
1571
  type="button"
1478
1572
  class="dm-emoji-swatch"
1573
+ [attr.tabindex]="-1"
1479
1574
  [title]="formatName(item.name)"
1480
1575
  [attr.aria-label]="formatName(item.name)"
1481
1576
  (mousedown)="$event.preventDefault()"
@@ -1506,6 +1601,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1506
1601
  placeholder="Search emoji..."
1507
1602
  [value]="searchQuery()"
1508
1603
  (input)="onSearch($event)"
1604
+ aria-label="Search emoji"
1509
1605
  (keydown.escape)="close()"
1510
1606
  />
1511
1607
  </div>
@@ -1516,6 +1612,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1516
1612
  type="button"
1517
1613
  class="dm-emoji-picker-tab"
1518
1614
  [class.dm-emoji-picker-tab--active]="activeCategory() === cat"
1615
+ role="tab"
1616
+ [attr.aria-selected]="activeCategory() === cat"
1519
1617
  [title]="cat"
1520
1618
  [attr.aria-label]="cat"
1521
1619
  (mousedown)="$event.preventDefault()"
@@ -1524,12 +1622,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1524
1622
  }
1525
1623
  </div>
1526
1624
 
1527
- <div class="dm-emoji-picker-grid" #grid (scroll)="onGridScroll()">
1625
+ <div class="dm-emoji-picker-grid" #grid (scroll)="onGridScroll()" (keydown)="onGridKeydown($event)">
1528
1626
  @if (searchQuery()) {
1529
1627
  @for (item of filteredEmojis(); track item.name) {
1530
1628
  <button
1531
1629
  type="button"
1532
1630
  class="dm-emoji-swatch"
1631
+ [attr.tabindex]="-1"
1533
1632
  [title]="formatName(item.name)"
1534
1633
  [attr.aria-label]="formatName(item.name)"
1535
1634
  (mousedown)="$event.preventDefault()"
@@ -1546,6 +1645,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1546
1645
  <button
1547
1646
  type="button"
1548
1647
  class="dm-emoji-swatch"
1648
+ [attr.tabindex]="-1"
1549
1649
  [title]="formatName(item.name)"
1550
1650
  [attr.aria-label]="formatName(item.name)"
1551
1651
  (mousedown)="$event.preventDefault()"
@@ -1559,6 +1659,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1559
1659
  <button
1560
1660
  type="button"
1561
1661
  class="dm-emoji-swatch"
1662
+ [attr.tabindex]="-1"
1562
1663
  [title]="formatName(item.name)"
1563
1664
  [attr.aria-label]="formatName(item.name)"
1564
1665
  (mousedown)="$event.preventDefault()"