@vaadin/rich-text-editor 25.0.0-alpha1 → 25.0.0-alpha10

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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
4
+ *
5
+ * This program is available under Vaadin Commercial License and Service Terms.
6
+ *
7
+ *
8
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
9
+ * license.
10
+ */
11
+ import { css } from 'lit';
12
+ import { overlayStyles } from '@vaadin/overlay/src/styles/vaadin-overlay-base-styles.js';
13
+
14
+ export const richTextEditorPopupOverlay = css`
15
+ [part='overlay'] {
16
+ padding: var(--vaadin-rich-text-editor-overlay-padding, var(--vaadin-padding-container));
17
+ }
18
+
19
+ [part='content'] {
20
+ display: grid;
21
+ gap: var(--vaadin-rich-text-editor-overlay-gap, var(--vaadin-gap-container-inline));
22
+ grid-template-columns: repeat(7, minmax(0, 1fr));
23
+ }
24
+
25
+ [part='content'] ::slotted(button) {
26
+ border: var(--vaadin-rich-text-editor-overlay-color-option-border-width, 1px) solid
27
+ var(--vaadin-rich-text-editor-overlay-color-option-border-color, transparent);
28
+ border-radius: var(--vaadin-rich-text-editor-overlay-color-option-border-radius, 9999px);
29
+ cursor: var(--vaadin-clickable-cursor);
30
+ font-size: var(--vaadin-rich-text-editor-overlay-color-option-font-size, inherit);
31
+ height: var(--vaadin-rich-text-editor-overlay-color-option-height, 1lh);
32
+ line-height: var(--vaadin-rich-text-editor-overlay-color-option-line-height, inherit);
33
+ padding: var(--vaadin-rich-text-editor-overlay-color-option-padding, 0);
34
+ width: var(--vaadin-rich-text-editor-overlay-color-option-width, 1lh);
35
+ }
36
+
37
+ [part='content'] ::slotted(button:focus-visible) {
38
+ outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
39
+ outline-offset: 1px;
40
+ }
41
+ `;
42
+
43
+ export const richTextEditorPopupOverlayStyles = [overlayStyles, richTextEditorPopupOverlay];
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2000 - 2025 Vaadin Ltd.
4
+ *
5
+ * This program is available under Vaadin Commercial License and Service Terms.
6
+ *
7
+ *
8
+ * See https://vaadin.com/commercial-license-and-service-terms for the full
9
+ * license.
10
+ */
11
+ import { overlayStyles } from '@vaadin/overlay/src/styles/vaadin-overlay-core-styles.js';
12
+
13
+ export const richTextEditorPopupOverlayStyles = [overlayStyles];
@@ -10,7 +10,6 @@
10
10
  */
11
11
  import '../vendor/vaadin-quill.js';
12
12
  import { timeOut } from '@vaadin/component-base/src/async.js';
13
- import { isFirefox } from '@vaadin/component-base/src/browser-utils.js';
14
13
  import { Debouncer } from '@vaadin/component-base/src/debounce.js';
15
14
  import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
16
15
 
@@ -177,7 +176,7 @@ export const RichTextEditorMixin = (superClass) =>
177
176
  '#ffffff', '#facccc', '#ffebcc', '#ffffcc', '#cce8cc', '#cce0f5', '#ebd6ff',
178
177
  '#bbbbbb', '#f06666', '#ffc266', '#ffff66', '#66b966', '#66a3e0', '#c285ff',
179
178
  '#888888', '#a10000', '#b26b00', '#b2b200', '#006100', '#0047b2', '#6b24b2',
180
- '#444444', '#5c0000', '#663d00', '#666600', '#003700', '#002966', '#3d1466'
179
+ '#444444', '#5c0000', '#663d00', '#666600', '#003700', '#002966', '#3d1466',
181
180
  ];
182
181
  },
183
182
  },
@@ -352,11 +351,6 @@ export const RichTextEditorMixin = (superClass) =>
352
351
  this.__patchToolbar();
353
352
  this.__patchKeyboard();
354
353
 
355
- /* c8 ignore next 3 */
356
- if (isFirefox) {
357
- this.__patchFirefoxFocus();
358
- }
359
-
360
354
  this.__setDirection(this.__dir);
361
355
 
362
356
  const editorContent = editor.querySelector('.ql-editor');
@@ -422,13 +416,32 @@ export const RichTextEditorMixin = (superClass) =>
422
416
  // Flush pending htmlValue only once the editor is fully initialized
423
417
  this.__flushPendingHtmlValue();
424
418
 
425
- this.$.backgroundPopup.target = this.shadowRoot.querySelector('#btn-background');
426
- this.$.colorPopup.target = this.shadowRoot.querySelector('#btn-color');
419
+ this.querySelector('[slot="color-popup"]').target = this.shadowRoot.querySelector('#btn-color');
420
+ this.querySelector('[slot="background-popup"]').target = this.shadowRoot.querySelector('#btn-background');
421
+
422
+ // Set up tooltip to show when hovering or focusing toolbar buttons
423
+ this._tooltip = document.createElement('vaadin-tooltip');
424
+ this._tooltip.slot = 'tooltip';
425
+ // Create dummy aria target, as toolbar buttons already have aria-label, and also cannot be linked with the
426
+ // tooltip being in the light DOM
427
+ this._tooltip.ariaTarget = document.createElement('div');
428
+ this.append(this._tooltip);
429
+
430
+ const buttons = this.shadowRoot.querySelectorAll('[part~="toolbar-button"]');
431
+ buttons.forEach((button) => {
432
+ button.addEventListener('mouseenter', this.__showTooltip.bind(this));
433
+ button.addEventListener('focusin', this.__showTooltip.bind(this));
434
+ });
435
+ }
427
436
 
428
- requestAnimationFrame(() => {
429
- this.$.linkDialog.$.dialog.$.overlay.addEventListener('vaadin-overlay-open', () => {
430
- this.$.linkUrl.focus();
431
- });
437
+ /** @private */
438
+ __showTooltip(event) {
439
+ const target = event.target;
440
+ this._tooltip.target = target;
441
+ this._tooltip.text = target.ariaLabel;
442
+ this._tooltip._stateController.open({
443
+ focus: event.type === 'focusin',
444
+ hover: event.type === 'mouseenter',
432
445
  });
433
446
  }
434
447
 
@@ -512,69 +525,6 @@ export const RichTextEditorMixin = (superClass) =>
512
525
  this._toolbarState = STATE.DEFAULT;
513
526
  }
514
527
 
515
- /** @private */
516
- __createFakeFocusTarget() {
517
- const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
518
- const elem = document.createElement('textarea');
519
- // Reset box model
520
- elem.style.border = '0';
521
- elem.style.padding = '0';
522
- elem.style.margin = '0';
523
- // Move element out of screen horizontally
524
- elem.style.position = 'absolute';
525
- elem.style[isRTL ? 'right' : 'left'] = '-9999px';
526
- // Move element to the same position vertically
527
- const yPosition = window.pageYOffset || document.documentElement.scrollTop;
528
- elem.style.top = `${yPosition}px`;
529
- return elem;
530
- }
531
-
532
- /** @private */
533
- __patchFirefoxFocus() {
534
- // In Firefox 63+ with native Shadow DOM, when moving focus out of
535
- // contenteditable and back again within same shadow root, cursor
536
- // disappears. See https://bugzilla.mozilla.org/show_bug.cgi?id=1496769
537
- const editorContent = this.shadowRoot.querySelector('.ql-editor');
538
- let isFake = false;
539
-
540
- const focusFake = () => {
541
- isFake = true;
542
- this.__fakeTarget = this.__createFakeFocusTarget();
543
- document.body.appendChild(this.__fakeTarget);
544
- // Let the focus step out of shadow root!
545
- this.__fakeTarget.focus();
546
- return new Promise((resolve) => {
547
- setTimeout(resolve);
548
- });
549
- };
550
-
551
- const focusBack = (offsetNode, offset) => {
552
- this._editor.focus();
553
- if (offsetNode) {
554
- this._editor.selection.setNativeRange(offsetNode, offset);
555
- }
556
- document.body.removeChild(this.__fakeTarget);
557
- delete this.__fakeTarget;
558
- isFake = false;
559
- };
560
-
561
- editorContent.addEventListener('mousedown', (e) => {
562
- if (!this._editor.hasFocus()) {
563
- const { x, y } = e;
564
- const { offset, offsetNode } = document.caretPositionFromPoint(x, y);
565
- focusFake().then(() => {
566
- focusBack(offsetNode, offset);
567
- });
568
- }
569
- });
570
-
571
- editorContent.addEventListener('focusin', () => {
572
- if (isFake === false) {
573
- focusFake().then(() => focusBack());
574
- }
575
- });
576
- }
577
-
578
528
  /** @private */
579
529
  __patchToolbar() {
580
530
  const toolbar = this._editor.getModule('toolbar');
@@ -721,7 +671,8 @@ export const RichTextEditorMixin = (superClass) =>
721
671
  if (e.keyCode === 13) {
722
672
  e.preventDefault();
723
673
  e.stopPropagation();
724
- this.$.confirmLink.click();
674
+ this._onLinkEditConfirm();
675
+ this._closeLinkDialog();
725
676
  }
726
677
  }
727
678
 
@@ -868,10 +819,7 @@ export const RichTextEditorMixin = (superClass) =>
868
819
  timeOut.after(timeout),
869
820
  () => {
870
821
  const formatting = Array.from(this.shadowRoot.querySelectorAll('[part="toolbar"] .ql-active'))
871
- .map((button) => {
872
- const tooltip = this.shadowRoot.querySelector(`[for="${button.id}"]`);
873
- return tooltip.text;
874
- })
822
+ .map((button) => button.getAttribute('aria-label'))
875
823
  .join(', ');
876
824
  announcer.textContent = formatting;
877
825
  },
@@ -8,51 +8,83 @@
8
8
  * See https://vaadin.com/commercial-license-and-service-terms for the full
9
9
  * license.
10
10
  */
11
- import { css, html, LitElement } from 'lit';
11
+ import { css, html, LitElement, render } from 'lit';
12
12
  import { defineCustomElement } from '@vaadin/component-base/src/define.js';
13
13
  import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
14
14
  import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
15
15
  import { OverlayMixin } from '@vaadin/overlay/src/vaadin-overlay-mixin.js';
16
16
  import { PositionMixin } from '@vaadin/overlay/src/vaadin-overlay-position-mixin.js';
17
- import { overlayStyles } from '@vaadin/overlay/src/vaadin-overlay-styles.js';
17
+ import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
18
18
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
19
- import { RichTextEditorPopupMixin } from './vaadin-rich-text-editor-popup-mixin.js';
19
+ import { richTextEditorPopupOverlayStyles } from './styles/vaadin-rich-text-editor-popup-overlay-core-styles.js';
20
20
 
21
21
  /**
22
22
  * An element used internally by `<vaadin-rich-text-editor>`. Not intended to be used separately.
23
23
  *
24
24
  * @customElement
25
25
  * @extends HTMLElement
26
- * @mixes RichTextEditorPopupMixin
27
26
  * @private
28
27
  */
29
- class RichTextEditorPopup extends RichTextEditorPopupMixin(PolylitMixin(LitElement)) {
28
+ class RichTextEditorPopup extends PolylitMixin(LitElement) {
30
29
  static get is() {
31
30
  return 'vaadin-rich-text-editor-popup';
32
31
  }
33
32
 
34
33
  static get styles() {
35
34
  return css`
36
- :host {
37
- display: none;
35
+ :host([opened]),
36
+ :host([opening]),
37
+ :host([closing]) {
38
+ display: contents !important;
39
+ }
40
+
41
+ :host,
42
+ :host([hidden]) {
43
+ display: none !important;
38
44
  }
39
45
  `;
40
46
  }
41
47
 
48
+ static get properties() {
49
+ return {
50
+ target: {
51
+ type: Object,
52
+ },
53
+
54
+ opened: {
55
+ type: Boolean,
56
+ reflectToAttribute: true,
57
+ notify: true,
58
+ },
59
+
60
+ colors: {
61
+ type: Array,
62
+ },
63
+ };
64
+ }
65
+
66
+ static get observers() {
67
+ return ['__openedOrTargetChanged(opened, target)', '__colorsChanged(colors)'];
68
+ }
69
+
42
70
  /** @protected */
43
71
  render() {
44
72
  return html`
45
73
  <vaadin-rich-text-editor-popup-overlay
46
- .renderer="${this.renderer}"
74
+ id="overlay"
75
+ .owner="${this}"
47
76
  .opened="${this.opened}"
48
77
  .positionTarget="${this.target}"
49
78
  no-vertical-overlap
50
79
  horizontal-align="start"
51
80
  vertical-align="top"
52
81
  focus-trap
82
+ exportparts="overlay, content"
53
83
  @opened-changed="${this._onOpenedChanged}"
54
84
  @vaadin-overlay-escape-press="${this._onOverlayEscapePress}"
55
- ></vaadin-rich-text-editor-popup-overlay>
85
+ >
86
+ <slot></slot>
87
+ </vaadin-rich-text-editor-popup-overlay>
56
88
  `;
57
89
  }
58
90
 
@@ -60,6 +92,39 @@ class RichTextEditorPopup extends RichTextEditorPopupMixin(PolylitMixin(LitEleme
60
92
  _onOpenedChanged(event) {
61
93
  this.opened = event.detail.value;
62
94
  }
95
+
96
+ /** @private */
97
+ _onOverlayEscapePress() {
98
+ this.target.focus();
99
+ }
100
+
101
+ /** @private */
102
+ _onColorClick(e) {
103
+ const { color } = e.target.dataset;
104
+ this.dispatchEvent(new CustomEvent('color-selected', { detail: { color } }));
105
+ }
106
+
107
+ /** @private */
108
+ __colorsChanged(colors) {
109
+ render(
110
+ html`
111
+ ${colors.map(
112
+ (color) => html`
113
+ <button data-color="${color}" style="background: ${color}" @click="${this._onColorClick}"></button>
114
+ `,
115
+ )}
116
+ `,
117
+ this,
118
+ { host: this },
119
+ );
120
+ }
121
+
122
+ /** @private */
123
+ __openedOrTargetChanged(opened, target) {
124
+ if (target) {
125
+ target.setAttribute('aria-expanded', opened ? 'true' : 'false');
126
+ }
127
+ }
63
128
  }
64
129
 
65
130
  defineCustomElement(RichTextEditorPopup);
@@ -78,25 +143,44 @@ export { RichTextEditorPopup };
78
143
  * @private
79
144
  */
80
145
  class RichTextEditorPopupOverlay extends PositionMixin(
81
- OverlayMixin(DirMixin(ThemableMixin(PolylitMixin(LitElement)))),
146
+ OverlayMixin(DirMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(LitElement))))),
82
147
  ) {
83
148
  static get is() {
84
149
  return 'vaadin-rich-text-editor-popup-overlay';
85
150
  }
86
151
 
87
152
  static get styles() {
88
- return overlayStyles;
153
+ return richTextEditorPopupOverlayStyles;
89
154
  }
90
155
 
91
156
  /** @protected */
92
157
  render() {
93
158
  return html`
94
- <div id="backdrop" part="backdrop" hidden></div>
95
159
  <div part="overlay" id="overlay">
96
- <div part="content" id="content"><slot></slot></div>
160
+ <div part="content" id="content">
161
+ <slot></slot>
162
+ </div>
97
163
  </div>
98
164
  `;
99
165
  }
166
+
167
+ /**
168
+ * Override method from OverlayFocusMixin to use owner as content root
169
+ * @protected
170
+ * @override
171
+ */
172
+ get _contentRoot() {
173
+ return this.owner;
174
+ }
175
+
176
+ /**
177
+ * Override method from OverlayFocusMixin to use owner as modal root
178
+ * @protected
179
+ * @override
180
+ */
181
+ get _modalRoot() {
182
+ return this.owner;
183
+ }
100
184
  }
101
185
 
102
186
  defineCustomElement(RichTextEditorPopupOverlay);
@@ -29,7 +29,7 @@ export interface RichTextEditorEventMap extends HTMLElementEventMap, RichTextEdi
29
29
  * It provides a set of toolbar controls to apply formatting on the content,
30
30
  * which is stored and can be accessed as HTML5 or JSON string.
31
31
  *
32
- * ```
32
+ * ```html
33
33
  * <vaadin-rich-text-editor></vaadin-rich-text-editor>
34
34
  * ```
35
35
  *