From 52218018e569a24a6816bc6bc0098cf64df7a996 Mon Sep 17 00:00:00 2001 From: web-padawan Date: Thu, 13 Nov 2025 11:22:16 +0200 Subject: [PATCH 01/20] refactor: move RTE background and border to the container --- .../vaadin-rich-text-editor-base-styles.js | 10 +++++----- .../rich-text-editor/test/auto-grow.test.js | 6 +++++- .../rich-text-editor/baseline/basic.png | Bin 7576 -> 7427 bytes .../rich-text-editor/baseline/max-height.png | Bin 24816 -> 24792 bytes .../src/components/rich-text-editor.css | 9 ++++++--- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/rich-text-editor/src/styles/vaadin-rich-text-editor-base-styles.js b/packages/rich-text-editor/src/styles/vaadin-rich-text-editor-base-styles.js index 96787728ff4..fbd4b4c2fa5 100644 --- a/packages/rich-text-editor/src/styles/vaadin-rich-text-editor-base-styles.js +++ b/packages/rich-text-editor/src/styles/vaadin-rich-text-editor-base-styles.js @@ -13,10 +13,6 @@ import { icons } from './vaadin-rich-text-editor-base-icons.js'; const base = css` :host { - background: var(--vaadin-rich-text-editor-background, var(--vaadin-background-color)); - border: var(--vaadin-input-field-border-width, 1px) solid - var(--vaadin-input-field-border-color, var(--vaadin-border-color)); - border-radius: var(--vaadin-input-field-border-radius, var(--vaadin-radius-m)); box-sizing: border-box; display: flex; flex-direction: column; @@ -41,7 +37,11 @@ const base = css` flex-direction: column; max-height: inherit; min-height: inherit; - border-radius: inherit; + background: var(--vaadin-rich-text-editor-background, var(--vaadin-background-color)); + border: var(--vaadin-input-field-border-width, 1px) solid + var(--vaadin-input-field-border-color, var(--vaadin-border-color)); + border-radius: var(--vaadin-input-field-border-radius, var(--vaadin-radius-m)); + outline-offset: calc(var(--vaadin-focus-ring-width) * -1); contain: paint; } diff --git a/packages/rich-text-editor/test/auto-grow.test.js b/packages/rich-text-editor/test/auto-grow.test.js index 104bada99c2..995718fc10a 100644 --- a/packages/rich-text-editor/test/auto-grow.test.js +++ b/packages/rich-text-editor/test/auto-grow.test.js @@ -42,7 +42,11 @@ describe('rich text editor', () => { ['height', 'min-height', 'max-height'].forEach((definedValue, key) => { describe(`defined ${definedValue}`, () => { beforeEach(async () => { - rte = fixtureSync(``); + rte = fixtureSync(` + + `); await nextRender(); editorContainer = rte.shadowRoot.querySelector('.vaadin-rich-text-editor-container'); editorContentContainer = rte.shadowRoot.querySelector('.ql-container'); diff --git a/packages/rich-text-editor/test/visual/base/screenshots/rich-text-editor/baseline/basic.png b/packages/rich-text-editor/test/visual/base/screenshots/rich-text-editor/baseline/basic.png index ab843073263f535ce71e9d3c4c5d9ba0b6044553..7e5f8ea9811903189b69fd25410e7b5efeb9d9b6 100644 GIT binary patch literal 7427 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 JM@Y;4{{TPjT?zmI literal 7576 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 diff --git a/packages/rich-text-editor/test/visual/base/screenshots/rich-text-editor/baseline/max-height.png b/packages/rich-text-editor/test/visual/base/screenshots/rich-text-editor/baseline/max-height.png index 472865afc949c577786616a90988432bece89b8f..0a244e47c43d9b46e1aac3dda1f7dae14a3bcffe 100644 GIT binary patch literal 24792 zcmeHwcUTkMw{Gkx_5w&16;TlQl-?C7N>wS58j#+5FX5A>R1xV-K{_NL(jh@9k=}dn zEp!4TkmT<8`<;8vz4zSn-1Yx0-81D0_~t<)k2pcE%;}7378VQ#H3F?3iZ~gK^FNrs>GLHx*Q` zR~25<(^Zsf=~qxq%j?f*UjKc}`R9Pa1L2dx4lmBB3tNVu?@!&1q`m%piDo2p*6b

5~M<|7vJU5%=uR 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 literal 24816 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 Date: Mon, 27 Oct 2025 08:06:43 +0200 Subject: [PATCH 02/20] proto: implement rich text editor label helper validation --- .../vaadin-rich-text-editor-base-styles.js | 29 +- .../src/vaadin-rich-text-editor-mixin.js | 140 ++++- .../src/vaadin-rich-text-editor.js | 514 +++++++++--------- 3 files changed, 421 insertions(+), 262 deletions(-) diff --git a/packages/rich-text-editor/src/styles/vaadin-rich-text-editor-base-styles.js b/packages/rich-text-editor/src/styles/vaadin-rich-text-editor-base-styles.js index 2359616474f..a121c2a6072 100644 --- a/packages/rich-text-editor/src/styles/vaadin-rich-text-editor-base-styles.js +++ b/packages/rich-text-editor/src/styles/vaadin-rich-text-editor-base-styles.js @@ -9,17 +9,17 @@ * license. */ import { css } from 'lit'; +import { field } from '@vaadin/field-base/src/styles/field-base-styles.js'; import { icons } from './vaadin-rich-text-editor-base-icons.js'; const base = css` :host { - background: var(--vaadin-rich-text-editor-background, var(--vaadin-background-color)); - border: var(--vaadin-input-field-border-width, 1px) solid - var(--vaadin-input-field-border-color, var(--vaadin-border-color)); - border-radius: var(--vaadin-input-field-border-radius, var(--vaadin-radius-m)); box-sizing: border-box; - display: flex; + display: inline-flex; flex-direction: column; + outline: none; + cursor: default; + -webkit-tap-highlight-color: transparent; } :host([hidden]) { @@ -35,13 +35,24 @@ const base = css` display: none; } + .vaadin-field-container { + display: flex; + flex-direction: column; + flex: auto; + gap: var(--vaadin-input-field-label-spacing, var(--vaadin-gap-xs)); + min-height: 0; + } + .vaadin-rich-text-editor-container { display: flex; flex: auto; flex-direction: column; - max-height: inherit; - min-height: inherit; - border-radius: inherit; + min-height: var(--vaadin-rich-text-editor-min-height, 12em); + max-height: var(--vaadin-rich-text-editor-max-height); + background: var(--vaadin-rich-text-editor-background, var(--vaadin-background-color)); + border: var(--vaadin-input-field-border-width, 1px) solid + var(--vaadin-input-field-border-color, var(--vaadin-border-color)); + border-radius: var(--vaadin-input-field-border-radius, var(--vaadin-radius-m)); contain: paint; } @@ -592,4 +603,4 @@ const states = css` } `; -export const richTextEditorStyles = [icons, base, content, toolbar, states]; +export const richTextEditorStyles = [icons, field, base, content, toolbar, states]; diff --git a/packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js b/packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js index 617c414a5d4..e25ac5d4f16 100644 --- a/packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js +++ b/packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js @@ -13,6 +13,7 @@ import { isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js'; import { timeOut } from '@vaadin/component-base/src/async.js'; import { Debouncer } from '@vaadin/component-base/src/debounce.js'; import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js'; +import { FieldMixin } from '@vaadin/field-base/src/field-mixin.js'; const Quill = window.Quill; @@ -97,8 +98,17 @@ const DEFAULT_I18N = { /** * @polymerMixin */ -export const RichTextEditorMixin = (superClass) => - class RichTextEditorMixinClass extends I18nMixin(DEFAULT_I18N, superClass) { +export const RichTextEditorMixin = (superClass) => { + class RichTextEditorMixinClass extends FieldMixin(I18nMixin(DEFAULT_I18N, superClass)) { + constructor() { + super(); + this.ariaTarget = this; + this._errorController.addEventListener('slot-content-changed', (event) => { + const { hasContent, node } = event.detail; + this.__updateDescriptions(hasContent, node, 'error'); + }); + } + static get properties() { return { /** @@ -243,7 +253,11 @@ export const RichTextEditorMixin = (superClass) => } static get observers() { - return ['_valueChanged(value, _editor)', '_disabledChanged(disabled, readonly, _editor)']; + return [ + '_valueChanged(value, _editor)', + '_disabledChanged(disabled, readonly, _editor)', + '__constraintsChanged(required)', + ]; } /** @@ -324,6 +338,19 @@ export const RichTextEditorMixin = (superClass) => this._editor.emitter.connect(); } + /** + * @return {boolean} + * @override + */ + checkValidity() { + return !this.required || this.__hasValue(); + } + + /** @private */ + __hasValue() { + return this.value !== '' && this.value !== '[{"insert":"\\n"}]'; + } + /** @protected */ ready() { super.ready(); @@ -346,11 +373,26 @@ export const RichTextEditorMixin = (superClass) => this.__setDirection(this.__dir); - const editorContent = editor.querySelector('.ql-editor'); + const editorContent = this.__getEditorContent(); editorContent.setAttribute('role', 'textbox'); editorContent.setAttribute('aria-multiline', 'true'); + if (this.hasAttribute('has-label')) { + const labelNode = this._labelNode; + this.__updateLabels(true, labelNode); + } + + if (this.hasAttribute('has-helper')) { + const helperNode = this._helperNode; + this.__updateDescriptions(true, helperNode, 'helper'); + } + + if (this.hasAttribute('has-error-message')) { + const errorNode = this._errorNode; + this.__updateDescriptions(true, errorNode, 'error'); + } + this._editor.on('text-change', () => { const timeout = 200; this.__debounceSetValue = Debouncer.debounce(this.__debounceSetValue, timeOut.after(timeout), () => { @@ -919,6 +961,65 @@ export const RichTextEditorMixin = (superClass) => } } + /** + * @private + * @override + */ + __labelChanged(hasLabel, labelNode) { + super.__labelChanged(hasLabel, labelNode); + this.__updateLabels(hasLabel, labelNode); + } + + /** + * @private + * @override + */ + __helperChanged(hasHelper, helperNode) { + super.__helperChanged(hasHelper, helperNode); + this.__updateDescriptions(hasHelper, helperNode, 'helper'); + } + + /** @private */ + __updateLabels(hasLabel, labelNode) { + if (!this._toolbar || !this.__getEditorContent()) { + return; + } + const labelId = hasLabel && labelNode ? labelNode.id : null; + if (labelId) { + this._toolbar.setAttribute('aria-labelledby', labelId); + this.__getEditorContent().setAttribute('aria-labelledby', labelId); + } else { + this._toolbar.removeAttribute('aria-labelledby'); + this.__getEditorContent().removeAttribute('aria-labelledby'); + } + } + + /** @private */ + __updateDescriptions(hasContent, node, type) { + if (!this._toolbar || !this.__getEditorContent()) { + return; + } + [this._toolbar, this.__getEditorContent()].forEach((element) => { + const currentDescribedBy = element.getAttribute('aria-describedby'); + const ids = currentDescribedBy ? currentDescribedBy.split(' ') : []; + const filteredIds = ids.filter((id) => !id.includes(`${type === 'helper' ? 'helper' : 'error-message'}-`)); + const nodeId = hasContent && node ? node.id : null; + if (nodeId) { + filteredIds.push(nodeId); + } + if (filteredIds.length > 0) { + element.setAttribute('aria-describedby', filteredIds.join(' ')); + } else { + element.removeAttribute('aria-describedby'); + } + }); + } + + /** @private */ + __getEditorContent() { + return this.shadowRoot.querySelector('.ql-editor'); + } + /** @private */ _disabledChanged(disabled, readonly, editor) { if (disabled === undefined || readonly === undefined || editor === undefined) { @@ -942,6 +1043,28 @@ export const RichTextEditorMixin = (superClass) => this.__oldDisabled = disabled; } + /** @private */ + __constraintsChanged(...constraints) { + const hasConstraints = this.__hasValidConstraints(constraints); + const isLastConstraintRemoved = this.__previousHasConstraints && !hasConstraints; + if ((this.__hasValue() || this.invalid) && hasConstraints) { + this._requestValidation(); + } else if (isLastConstraintRemoved && !this.manualValidation) { + this._setInvalid(false); + } + this.__previousHasConstraints = hasConstraints; + } + + /** @private */ + __hasValidConstraints(constraints) { + return constraints.some((c) => this.__isValidConstraint(c)); + } + + /** @private */ + __isValidConstraint(constraint) { + return Boolean(constraint) || constraint === 0; + } + /** @private */ _valueChanged(value, editor) { if (value && this.__pendingHtmlValue) { @@ -991,5 +1114,12 @@ export const RichTextEditorMixin = (superClass) => // Value changed from outside this.__lastCommittedChange = this.value; } + + if (this.invalid) { + this._requestValidation(); + } } - }; + } + + return RichTextEditorMixinClass; +}; diff --git a/packages/rich-text-editor/src/vaadin-rich-text-editor.js b/packages/rich-text-editor/src/vaadin-rich-text-editor.js index 4b1698086c5..f3bac313ca8 100644 --- a/packages/rich-text-editor/src/vaadin-rich-text-editor.js +++ b/packages/rich-text-editor/src/vaadin-rich-text-editor.js @@ -125,257 +125,275 @@ class RichTextEditor extends RichTextEditorMixin( /** @protected */ render() { return html` -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ +
+ +
-
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + +
+ +
-
+ +
+ +
From 7b57fa4a91a60f4cea5a2200a1fb00ecf7c23457 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Tue, 28 Oct 2025 19:43:04 +0200 Subject: [PATCH 03/20] fix: use aria label for nvda --- .../src/vaadin-rich-text-editor-mixin.js | 166 +++++++++++++++--- 1 file changed, 137 insertions(+), 29 deletions(-) diff --git a/packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js b/packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js index e25ac5d4f16..69f9cb882d0 100644 --- a/packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js +++ b/packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js @@ -305,6 +305,19 @@ export const RichTextEditorMixin = (superClass) => { super.disconnectedCallback(); this._editor.emitter.disconnect(); + + if (this.__labelTextObserver) { + this.__labelTextObserver.disconnect(); + this.__labelTextObserver = null; + } + if (this.__helperTextObserver) { + this.__helperTextObserver.disconnect(); + this.__helperTextObserver = null; + } + if (this.__errorTextObserver) { + this.__errorTextObserver.disconnect(); + this.__errorTextObserver = null; + } } /** @private */ @@ -383,6 +396,8 @@ export const RichTextEditorMixin = (superClass) => { this.__updateLabels(true, labelNode); } + this.__updateRequired(this.required); + if (this.hasAttribute('has-helper')) { const helperNode = this._helperNode; this.__updateDescriptions(true, helperNode, 'helper'); @@ -979,18 +994,62 @@ export const RichTextEditorMixin = (superClass) => { this.__updateDescriptions(hasHelper, helperNode, 'helper'); } + /** @private */ + __constraintsChanged(...constraints) { + const hasConstraints = this.__hasValidConstraints(constraints); + const isLastConstraintRemoved = this.__previousHasConstraints && !hasConstraints; + if ((this.__hasValue() || this.invalid) && hasConstraints) { + this._requestValidation(); + } else if (isLastConstraintRemoved && !this.manualValidation) { + this._setInvalid(false); + } + this.__previousHasConstraints = hasConstraints; + } + + /** + * @param {boolean} required + * @protected + * @override + */ + _requiredChanged(required) { + super._requiredChanged(required); + this.__updateRequired(required); + } + /** @private */ __updateLabels(hasLabel, labelNode) { if (!this._toolbar || !this.__getEditorContent()) { return; } - const labelId = hasLabel && labelNode ? labelNode.id : null; - if (labelId) { - this._toolbar.setAttribute('aria-labelledby', labelId); - this.__getEditorContent().setAttribute('aria-labelledby', labelId); + if (this.__labelTextObserver) { + this.__labelTextObserver.disconnect(); + this.__labelTextObserver = null; + } + if (hasLabel && labelNode) { + this.__updateLabelText(labelNode); + this.__labelTextObserver = new MutationObserver(() => { + this.__updateLabelText(labelNode); + }); + this.__labelTextObserver.observe(labelNode, { + childList: true, + characterData: true, + subtree: true, + }); } else { - this._toolbar.removeAttribute('aria-labelledby'); - this.__getEditorContent().removeAttribute('aria-labelledby'); + this._toolbar.removeAttribute('aria-label'); + this.__getEditorContent().removeAttribute('aria-label'); + } + } + + /** @private */ + __updateLabelText(labelNode) { + const labelText = labelNode.textContent.trim(); + if (labelText) { + this._toolbar.setAttribute('aria-label', labelText); + this.__getEditorContent().setAttribute('aria-label', labelText); + } else { + this._toolbar.removeAttribute('aria-label'); + this.__getEditorContent().removeAttribute('aria-label'); } } @@ -999,20 +1058,81 @@ export const RichTextEditorMixin = (superClass) => { if (!this._toolbar || !this.__getEditorContent()) { return; } - [this._toolbar, this.__getEditorContent()].forEach((element) => { - const currentDescribedBy = element.getAttribute('aria-describedby'); - const ids = currentDescribedBy ? currentDescribedBy.split(' ') : []; - const filteredIds = ids.filter((id) => !id.includes(`${type === 'helper' ? 'helper' : 'error-message'}-`)); - const nodeId = hasContent && node ? node.id : null; - if (nodeId) { - filteredIds.push(nodeId); - } - if (filteredIds.length > 0) { - element.setAttribute('aria-describedby', filteredIds.join(' ')); + const descId = type === 'helper' ? 'rte-shadow-helper-desc' : 'rte-shadow-error-desc'; + const observerKey = type === 'helper' ? '__helperTextObserver' : '__errorTextObserver'; + if (hasContent && node) { + this.__addDescription(node, descId, observerKey); + } else { + this.__removeDescription(descId, observerKey); + } + } + + /** @private */ + __removeDescription(descId, observerKey) { + const descElement = this.shadowRoot.querySelector(`#${descId}`); + if (descElement) { + descElement.remove(); + } + const editorContent = this.__getEditorContent(); + const currentDescribedBy = editorContent.getAttribute('aria-describedby'); + if (currentDescribedBy) { + const ids = currentDescribedBy.split(' ').filter((id) => id !== descId); + if (ids.length > 0) { + editorContent.setAttribute('aria-describedby', ids.join(' ')); } else { - element.removeAttribute('aria-describedby'); + editorContent.removeAttribute('aria-describedby'); + } + } + if (this[observerKey]) { + this[observerKey].disconnect(); + this[observerKey] = null; + } + } + + /** @private */ + __addDescription(node, descId, observerKey) { + let descElement = this.shadowRoot.querySelector(`#${descId}`); + if (!descElement) { + descElement = document.createElement('div'); + descElement.id = descId; + descElement.hidden = true; + this.shadowRoot.appendChild(descElement); + } + descElement.textContent = node.textContent.trim(); + if (this[observerKey]) { + this[observerKey].disconnect(); + } + this[observerKey] = new MutationObserver(() => { + const updatedText = node.textContent.trim(); + if (descElement && descElement.textContent !== updatedText) { + descElement.textContent = updatedText; } }); + this[observerKey].observe(node, { + childList: true, + characterData: true, + subtree: true, + }); + const editorContent = this.__getEditorContent(); + const currentDescribedBy = editorContent.getAttribute('aria-describedby'); + const ids = currentDescribedBy ? currentDescribedBy.split(' ') : []; + if (!ids.includes(descId)) { + ids.push(descId); + } + editorContent.setAttribute('aria-describedby', ids.join(' ')); + } + + /** @private */ + __updateRequired(required) { + const editorContent = this.__getEditorContent(); + if (!editorContent) { + return; + } + if (required) { + editorContent.setAttribute('aria-required', 'true'); + } else { + editorContent.removeAttribute('aria-required'); + } } /** @private */ @@ -1043,18 +1163,6 @@ export const RichTextEditorMixin = (superClass) => { this.__oldDisabled = disabled; } - /** @private */ - __constraintsChanged(...constraints) { - const hasConstraints = this.__hasValidConstraints(constraints); - const isLastConstraintRemoved = this.__previousHasConstraints && !hasConstraints; - if ((this.__hasValue() || this.invalid) && hasConstraints) { - this._requestValidation(); - } else if (isLastConstraintRemoved && !this.manualValidation) { - this._setInvalid(false); - } - this.__previousHasConstraints = hasConstraints; - } - /** @private */ __hasValidConstraints(constraints) { return constraints.some((c) => this.__isValidConstraint(c)); From 076f3a2acf0090338114fa380b24d03bb48fc4a0 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Thu, 30 Oct 2025 14:28:12 +0200 Subject: [PATCH 04/20] fix: validate on clear --- .../src/vaadin-rich-text-editor-mixin.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js b/packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js index 69f9cb882d0..8f15cc16b3f 100644 --- a/packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js +++ b/packages/rich-text-editor/src/vaadin-rich-text-editor-mixin.js @@ -1185,12 +1185,19 @@ export const RichTextEditorMixin = (superClass) => { } if (value == null || value === '[{"insert":"\\n"}]') { + this.__clearingValue = true; this.value = ''; return; } if (value === '') { this._clear(); + if (this.__clearingValue) { + if (this.invalid || this.__previousHasConstraints) { + this._requestValidation(); + } + this.__clearingValue = false; + } return; } @@ -1223,7 +1230,7 @@ export const RichTextEditorMixin = (superClass) => { this.__lastCommittedChange = this.value; } - if (this.invalid) { + if (this.invalid || this.__previousHasConstraints) { this._requestValidation(); } } From 3d2cd8e498e7d3dd9cb797ba271ab80184d43294 Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Thu, 30 Oct 2025 16:59:14 +0200 Subject: [PATCH 05/20] test: add tests for the prototype --- packages/rich-text-editor/test/a11y.test.js | 650 +++++++++++++++++- .../rich-text-editor/test/auto-grow.test.js | 2 +- .../rich-text-editor/test/validation.test.js | 186 +++++ 3 files changed, 836 insertions(+), 2 deletions(-) create mode 100644 packages/rich-text-editor/test/validation.test.js diff --git a/packages/rich-text-editor/test/a11y.test.js b/packages/rich-text-editor/test/a11y.test.js index a112a012f02..d866a5b7c11 100644 --- a/packages/rich-text-editor/test/a11y.test.js +++ b/packages/rich-text-editor/test/a11y.test.js @@ -1,6 +1,6 @@ import { expect } from '@vaadin/chai-plugins'; import { sendKeys } from '@vaadin/test-runner-commands'; -import { fixtureSync, keyboardEventFor, nextRender, nextUpdate } from '@vaadin/testing-helpers'; +import { fixtureSync, keyboardEventFor, nextFrame, nextRender, nextUpdate } from '@vaadin/testing-helpers'; import sinon from 'sinon'; import '../src/vaadin-rich-text-editor.js'; import { getDeepActiveElement } from '@vaadin/a11y-base/src/focus-utils.js'; @@ -14,6 +14,587 @@ describe('accessibility', () => { const flushValueDebouncer = () => rte.__debounceSetValue && rte.__debounceSetValue.flush(); + describe('helper', () => { + let helper; + + beforeEach(async () => { + rte = fixtureSync(''); + await nextRender(); + }); + + it('should not have has-helper attribute by default', () => { + expect(rte.hasAttribute('has-helper')).to.be.false; + }); + + it('should set has-helper attribute when helper text is set', async () => { + rte.helperText = 'Helper text'; + await nextFrame(); + expect(rte.hasAttribute('has-helper')).to.be.true; + }); + + it('should remove has-helper attribute when helper text is cleared', async () => { + rte.helperText = 'Helper text'; + await nextFrame(); + rte.helperText = ''; + await nextFrame(); + expect(rte.hasAttribute('has-helper')).to.be.false; + }); + + it('should set id on the lazily added helper element', async () => { + rte.helperText = 'Helper text'; + await nextFrame(); + helper = rte.querySelector('[slot="helper"]'); + const ID_REGEX = /^helper-vaadin-rich-text-editor-\d+$/u; + expect(helper.getAttribute('id')).to.match(ID_REGEX); + }); + + it('should add helper to aria-describedby when helper text is set', async () => { + rte.helperText = 'Helper text'; + await nextFrame(); + helper = rte.querySelector('[slot="helper"]'); + const aria = rte.getAttribute('aria-describedby'); + expect(aria).to.equal(helper.id); + }); + + it('should remove helper from aria-describedby when helper text is cleared', async () => { + rte.helperText = 'Helper text'; + await nextFrame(); + rte.helperText = ''; + await nextFrame(); + expect(rte.hasAttribute('aria-describedby')).to.be.false; + }); + + describe('shadow DOM', () => { + let toolbar, editorContent; + + beforeEach(async () => { + rte = fixtureSync( + '', + ); + await nextRender(); + toolbar = rte.shadowRoot.querySelector('[part="toolbar"]'); + editorContent = rte.shadowRoot.querySelector('.ql-editor'); + }); + + it('should set aria-describedby on host element', () => { + const describedBy = rte.getAttribute('aria-describedby'); + expect(describedBy).to.be.ok; + expect(describedBy).to.include('helper-vaadin-rich-text-editor-'); + }); + + it('should not set aria-describedby on toolbar', () => { + const describedBy = toolbar.getAttribute('aria-describedby'); + expect(describedBy).to.not.be.ok; + }); + + it('should set aria-describedby on editor content', () => { + const describedBy = editorContent.getAttribute('aria-describedby'); + expect(describedBy).to.be.ok; + expect(describedBy).to.equal('rte-shadow-helper-desc'); + }); + + it('should have helper element with the referenced ID', () => { + const describedBy = rte.getAttribute('aria-describedby'); + expect(describedBy).to.be.ok; + const helperId = describedBy.split(' ').find((id) => id.includes('helper')); + expect(helperId).to.be.ok; + const helperElement = rte.querySelector(`#${helperId}`); + + expect(helperElement).to.be.ok; + expect(helperElement.textContent.trim()).to.equal('Helper text'); + + const shadowHelper = rte.shadowRoot.querySelector('#rte-shadow-helper-desc'); + expect(shadowHelper).to.be.ok; + expect(shadowHelper.textContent.trim()).to.equal('Helper text'); + }); + }); + + describe('slotted', () => { + beforeEach(async () => { + rte = fixtureSync(` + +
Custom helper
+
+ `); + await nextRender(); + helper = rte.querySelector('[slot="helper"]'); + }); + + it('should set id on the slotted helper element', () => { + const ID_REGEX = /^helper-vaadin-rich-text-editor-\d+$/u; + expect(helper.getAttribute('id')).to.match(ID_REGEX); + }); + + it('should set has-helper attribute with slotted helper', () => { + expect(rte.hasAttribute('has-helper')).to.be.true; + }); + + it('should not override custom id on the slotted helper', async () => { + rte = fixtureSync(` + +
Custom helper
+
+ `); + await nextRender(); + helper = rte.querySelector('[slot="helper"]'); + expect(helper.getAttribute('id')).to.equal('custom-helper'); + }); + }); + }); + + describe('error message', () => { + let error; + + describe('when invalid', () => { + beforeEach(async () => { + rte = fixtureSync(''); + rte.invalid = true; + await nextRender(); + }); + + it('should not have has-error-message attribute by default', () => { + expect(rte.hasAttribute('has-error-message')).to.be.false; + }); + + it('should set has-error-message attribute when error message is set', async () => { + rte.errorMessage = 'Error message'; + await nextUpdate(rte); + expect(rte.hasAttribute('has-error-message')).to.be.true; + }); + + it('should remove has-error-message attribute when error message is cleared', async () => { + rte.errorMessage = 'Error message'; + await nextUpdate(rte); + await nextFrame(); + rte.errorMessage = ''; + await nextUpdate(rte); + await nextFrame(); + expect(rte.hasAttribute('has-error-message')).to.be.false; + }); + + it('should set id on the lazily added error element', async () => { + rte.errorMessage = 'Error message'; + await nextUpdate(rte); + await nextFrame(); + error = rte.querySelector('[slot="error-message"]'); + const ID_REGEX = /^error-message-vaadin-.+-\d+$/u; + expect(error.getAttribute('id')).to.match(ID_REGEX); + }); + + it('should add error to aria-describedby when field is invalid', async () => { + rte.errorMessage = 'Error message'; + await nextUpdate(rte); + await nextFrame(); + const aria = rte.getAttribute('aria-describedby'); + expect(aria).to.be.ok; + expect(aria).to.match(/error-message-vaadin-.+-\d+/u); + }); + + it('should remove error from aria-describedby when field becomes valid', async () => { + rte.errorMessage = 'Error message'; + await nextUpdate(rte); + await nextFrame(); + rte.invalid = false; + await nextUpdate(rte); + await nextFrame(); + expect(rte.hasAttribute('aria-describedby')).to.be.false; + }); + + describe('shadow DOM', () => { + let toolbar, editorContent; + + beforeEach(async () => { + rte = fixtureSync( + '', + ); + rte.invalid = true; + await nextRender(); + toolbar = rte.shadowRoot.querySelector('[part="toolbar"]'); + editorContent = rte.shadowRoot.querySelector('.ql-editor'); + }); + + it('should set aria-describedby with error ID on host element', () => { + const describedBy = rte.getAttribute('aria-describedby'); + expect(describedBy).to.be.ok; + expect(describedBy).to.include('error-message-vaadin-rich-text-editor-'); + }); + + it('should not set aria-describedby on toolbar', () => { + const describedBy = toolbar.getAttribute('aria-describedby'); + expect(describedBy).to.not.be.ok; + }); + + it('should set aria-describedby with error on editor content', () => { + const describedBy = editorContent.getAttribute('aria-describedby'); + expect(describedBy).to.be.ok; + expect(describedBy).to.equal('rte-shadow-error-desc'); + }); + + it('should have error element with the referenced ID', () => { + const describedBy = rte.getAttribute('aria-describedby'); + expect(describedBy).to.be.ok; + const errorId = describedBy.split(' ').find((id) => id.includes('error-message')); + expect(errorId).to.be.ok; + const errorElement = rte.querySelector(`#${errorId}`); + + expect(errorElement).to.be.ok; + expect(errorElement.textContent.trim()).to.include('Error text'); + + const shadowError = rte.shadowRoot.querySelector('#rte-shadow-error-desc'); + expect(shadowError).to.be.ok; + expect(shadowError.textContent.trim()).to.equal('Error text'); + }); + }); + }); + + describe('when valid', () => { + beforeEach(async () => { + rte = fixtureSync(''); + await nextRender(); + }); + + it('should not add error to aria-describedby when field is valid', async () => { + rte.errorMessage = 'Error message'; + await nextUpdate(rte); + await nextFrame(); + const aria = rte.getAttribute('aria-describedby'); + expect(aria).to.be.null; + }); + + it('should not set has-error-message when field is valid', async () => { + rte.errorMessage = 'Error message'; + await nextUpdate(rte); + await nextFrame(); + expect(rte.hasAttribute('has-error-message')).to.be.false; + }); + }); + + describe('slotted', () => { + beforeEach(async () => { + rte = fixtureSync(` + +
Custom error
+
+ `); + await nextRender(); + error = rte.querySelector('[slot="error-message"]'); + }); + + it('should set id on the slotted error element', () => { + const ID_REGEX = /^error-message-vaadin-rich-text-editor-\d+$/u; + expect(error.getAttribute('id')).to.match(ID_REGEX); + }); + + it('should set has-error-message attribute with slotted error', () => { + expect(rte.hasAttribute('has-error-message')).to.be.true; + }); + + it('should not override custom id on the slotted error', async () => { + rte = fixtureSync(` + +
Custom error
+
+ `); + await nextRender(); + error = rte.querySelector('[slot="error-message"]'); + expect(error.getAttribute('id')).to.equal('custom-error'); + }); + }); + }); + + describe('aria-describedby', () => { + beforeEach(async () => { + rte = fixtureSync(` + + + `); + await nextRender(); + }); + + it('should only contain helper id when the field is valid', () => { + const aria = rte.getAttribute('aria-describedby'); + expect(aria).to.be.ok; + expect(aria).to.match(/^helper-vaadin-.+-\d+$/u); + expect(aria).to.not.match(/error-message/u); + }); + + it('should add error id when the field becomes invalid', async () => { + rte.invalid = true; + await nextUpdate(rte); + await nextFrame(); + const aria = rte.getAttribute('aria-describedby'); + expect(aria).to.be.ok; + expect(aria).to.match(/helper-vaadin-.+-\d+/u); + expect(aria).to.match(/error-message-vaadin-.+-\d+/u); + }); + + it('should remove error id when the field becomes valid', async () => { + rte.invalid = true; + await nextUpdate(rte); + await nextFrame(); + rte.invalid = false; + await nextUpdate(rte); + await nextFrame(); + const aria = rte.getAttribute('aria-describedby'); + expect(aria).to.be.ok; + expect(aria).to.match(/^helper-vaadin-.+-\d+$/u); + expect(aria).to.not.match(/error-message/u); + }); + + it('should have both helper and error when invalid', async () => { + rte.invalid = true; + await nextUpdate(rte); + await nextFrame(); + const aria = rte.getAttribute('aria-describedby'); + const ids = aria.split(' '); + expect(ids).to.have.lengthOf(2); + expect(aria).to.match(/helper-vaadin-.+-\d+/u); + expect(aria).to.match(/error-message-vaadin-.+-\d+/u); + }); + + it('should have both helper and error elements accessible', async () => { + rte.invalid = true; + await nextUpdate(rte); + await nextFrame(); + const describedBy = rte.getAttribute('aria-describedby'); + expect(describedBy).to.be.ok; + const ids = describedBy.split(' '); + const helperId = ids.find((id) => id.includes('helper')); + const errorId = ids.find((id) => id.includes('error-message')); + + expect(helperId).to.be.ok; + expect(errorId).to.be.ok; + + const helperElement = rte.querySelector(`#${helperId}`); + const errorElement = rte.querySelector(`#${errorId}`); + + expect(helperElement).to.be.ok; + expect(errorElement).to.be.ok; + expect(helperElement.textContent.trim()).to.equal('Helper'); + expect(errorElement.textContent.trim()).to.include('Error'); + }); + }); + + describe('label', () => { + let toolbar, editorContent; + + beforeEach(async () => { + rte = fixtureSync(''); + await nextRender(); + toolbar = rte.shadowRoot.querySelector('[part="toolbar"]'); + editorContent = rte.shadowRoot.querySelector('.ql-editor'); + }); + + it('should not have aria-labelledby attribute by default', () => { + expect(rte.hasAttribute('aria-labelledby')).to.be.false; + expect(toolbar.hasAttribute('aria-labelledby')).to.be.false; + expect(editorContent.hasAttribute('aria-labelledby')).to.be.false; + }); + + it('should set id on the lazily added label element', async () => { + const label = document.createElement('label'); + label.setAttribute('slot', 'label'); + + rte.appendChild(label); + await nextFrame(); + + const ID_REGEX = /^label-vaadin-rich-text-editor-\d+$/u; + expect(label.getAttribute('id')).to.match(ID_REGEX); + }); + + it('should not override custom id on the lazily added label', async () => { + const label = document.createElement('label'); + label.setAttribute('slot', 'label'); + label.id = 'custom-label'; + + rte.appendChild(label); + await nextFrame(); + + expect(label.getAttribute('id')).to.equal('custom-label'); + }); + + it('should set aria-labelledby on host, toolbar and editor when adding a label', async () => { + const label = document.createElement('label'); + label.setAttribute('slot', 'label'); + label.textContent = 'Custom Label'; + + rte.appendChild(label); + await nextFrame(); + await nextFrame(); + + const labelId = label.id; + + expect(rte.getAttribute('aria-labelledby')).to.equal(labelId); + + expect(toolbar.getAttribute('aria-label')).to.equal('Custom Label'); + expect(editorContent.getAttribute('aria-label')).to.equal('Custom Label'); + }); + + it('should remove aria-label from shadow DOM elements when removing a label', async () => { + const label = document.createElement('label'); + label.setAttribute('slot', 'label'); + + rte.appendChild(label); + await nextFrame(); + + rte.removeChild(label); + await nextFrame(); + + expect(rte.hasAttribute('aria-labelledby')).to.be.false; + expect(toolbar.hasAttribute('aria-label')).to.be.false; + expect(editorContent.hasAttribute('aria-label')).to.be.false; + }); + + it('should use label property to set aria-label on shadow DOM elements', async () => { + rte.label = 'Rich Text Editor Label'; + await nextFrame(); + + const ariaLabelledBy = rte.getAttribute('aria-labelledby'); + expect(ariaLabelledBy).to.be.ok; + expect(ariaLabelledBy).to.match(/^label-vaadin-.+-\d+$/u); + expect(toolbar.getAttribute('aria-label')).to.equal('Rich Text Editor Label'); + expect(editorContent.getAttribute('aria-label')).to.equal('Rich Text Editor Label'); + }); + + it('should update aria-label on shadow DOM elements when label property changes', async () => { + rte.label = 'Initial Label'; + await nextFrame(); + const initialLabelId = rte.getAttribute('aria-labelledby'); + + rte.label = 'Updated Label'; + await nextFrame(); + const updatedLabelId = rte.getAttribute('aria-labelledby'); + + expect(initialLabelId).to.equal(updatedLabelId); + expect(updatedLabelId).to.match(/^label-vaadin-.+-\d+$/u); + expect(toolbar.getAttribute('aria-label')).to.equal('Updated Label'); + expect(editorContent.getAttribute('aria-label')).to.equal('Updated Label'); + }); + + it('should remove aria-label when label property is cleared', async () => { + rte.label = 'Some Label'; + await nextFrame(); + + rte.label = ''; + await nextFrame(); + + expect(rte.hasAttribute('aria-labelledby')).to.be.false; + expect(toolbar.hasAttribute('aria-label')).to.be.false; + expect(editorContent.hasAttribute('aria-label')).to.be.false; + }); + + describe('with string label', () => { + beforeEach(async () => { + rte = fixtureSync(''); + await nextRender(); + toolbar = rte.shadowRoot.querySelector('[part="toolbar"]'); + editorContent = rte.shadowRoot.querySelector('.ql-editor'); + }); + + it('should set aria-labelledby on host element', () => { + const labelId = rte.getAttribute('aria-labelledby'); + expect(labelId).to.be.ok; + expect(labelId).to.match(/^label-vaadin-rich-text-editor-\d+$/u); + }); + + it('should set aria-label on toolbar', () => { + const label = toolbar.getAttribute('aria-label'); + expect(label).to.equal('Article Content'); + }); + + it('should set aria-label on editor content', () => { + const label = editorContent.getAttribute('aria-label'); + expect(label).to.equal('Article Content'); + }); + + it('should all have the same label text', () => { + const toolbarLabel = toolbar.getAttribute('aria-label'); + const editorLabel = editorContent.getAttribute('aria-label'); + + expect(toolbarLabel).to.equal('Article Content'); + expect(editorLabel).to.equal('Article Content'); + }); + + it('should have the label element with the referenced ID', () => { + const labelId = rte.getAttribute('aria-labelledby'); + const labelElement = rte.querySelector(`#${labelId}`); + + expect(labelElement).to.be.ok; + expect(labelElement.textContent.trim()).to.equal('Article Content'); + }); + }); + + describe('with slotted label', () => { + beforeEach(async () => { + rte = fixtureSync(` + + + + `); + await nextRender(); + toolbar = rte.shadowRoot.querySelector('[part="toolbar"]'); + editorContent = rte.shadowRoot.querySelector('.ql-editor'); + }); + + it('should set aria-labelledby on host element', () => { + const labelId = rte.getAttribute('aria-labelledby'); + expect(labelId).to.be.ok; + }); + + it('should set aria-label on toolbar', () => { + const label = toolbar.getAttribute('aria-label'); + expect(label).to.equal('Custom Label'); + }); + + it('should set aria-label on editor content', () => { + const label = editorContent.getAttribute('aria-label'); + expect(label).to.equal('Custom Label'); + }); + + it('should reference the slotted label element', () => { + const labelId = rte.getAttribute('aria-labelledby'); + const slottedLabel = rte.querySelector('label[slot="label"]'); + + expect(slottedLabel).to.be.ok; + expect(slottedLabel.id).to.equal(labelId); + expect(slottedLabel.textContent).to.equal('Custom Label'); + }); + }); + + describe('updates', () => { + beforeEach(async () => { + rte = fixtureSync(''); + await nextRender(); + toolbar = rte.shadowRoot.querySelector('[part="toolbar"]'); + editorContent = rte.shadowRoot.querySelector('.ql-editor'); + }); + + it('should update aria-label on shadow DOM elements when label changes', async () => { + const initialHostId = rte.getAttribute('aria-labelledby'); + const initialToolbarLabel = toolbar.getAttribute('aria-label'); + const initialEditorLabel = editorContent.getAttribute('aria-label'); + + expect(initialToolbarLabel).to.equal('Initial'); + expect(initialEditorLabel).to.equal('Initial'); + + rte.label = 'Updated Label'; + await nextFrame(); + + const updatedHostId = rte.getAttribute('aria-labelledby'); + const updatedToolbarLabel = toolbar.getAttribute('aria-label'); + const updatedEditorLabel = editorContent.getAttribute('aria-label'); + + expect(updatedHostId).to.equal(initialHostId); + + expect(updatedToolbarLabel).to.equal('Updated Label'); + expect(updatedEditorLabel).to.equal('Updated Label'); + + const labelElement = rte.querySelector(`#${updatedHostId}`); + expect(labelElement.textContent.trim()).to.equal('Updated Label'); + }); + }); + }); + describe('screen readers', () => { beforeEach(async () => { rte = fixtureSync(''); @@ -402,4 +983,71 @@ describe('accessibility', () => { expect(rte.htmlValue).to.equal('

\t

'); }); }); + + describe('required indicator', () => { + beforeEach(async () => { + rte = fixtureSync(''); + await nextRender(); + }); + + it('should have required attribute on host', () => { + expect(rte.hasAttribute('required')).to.be.true; + }); + + it('should have required indicator element', () => { + const indicator = rte.shadowRoot.querySelector('[part="required-indicator"]'); + expect(indicator).to.exist; + }); + + it('should have required indicator visible', () => { + const indicator = rte.shadowRoot.querySelector('[part="required-indicator"]'); + expect(indicator).to.be.ok; + + const display = getComputedStyle(indicator).display; + expect(display).to.not.equal('none'); + }); + + it('should have aria-hidden on required indicator', () => { + const indicator = rte.shadowRoot.querySelector('[part="required-indicator"]'); + expect(indicator.getAttribute('aria-hidden')).to.equal('true'); + }); + }); + + describe('toolbar role', () => { + let toolbar; + + beforeEach(async () => { + rte = fixtureSync(''); + await nextRender(); + toolbar = rte.shadowRoot.querySelector('[part="toolbar"]'); + }); + + it('should have role="toolbar" on toolbar element', () => { + expect(toolbar.getAttribute('role')).to.equal('toolbar'); + }); + + it('should have aria-label on toolbar', () => { + const label = toolbar.getAttribute('aria-label'); + expect(label).to.equal('Editor'); + }); + }); + + describe('editor content attributes', () => { + let editorContent; + + beforeEach(async () => { + rte = fixtureSync(''); + await nextRender(); + editorContent = rte.shadowRoot.querySelector('.ql-editor'); + }); + + it('should have contenteditable on editor', () => { + expect(editorContent.getAttribute('contenteditable')).to.equal('true'); + }); + + it('should have aria-label on editor', () => { + const label = editorContent.getAttribute('aria-label'); + expect(label).to.equal('Editor'); + }); + }); }); diff --git a/packages/rich-text-editor/test/auto-grow.test.js b/packages/rich-text-editor/test/auto-grow.test.js index 104bada99c2..df67d042b79 100644 --- a/packages/rich-text-editor/test/auto-grow.test.js +++ b/packages/rich-text-editor/test/auto-grow.test.js @@ -57,7 +57,7 @@ describe('rich text editor', () => { }); it('internal flex wrapper should be the same size as rte itself', () => { - expect(rte.clientHeight).to.be.equal(editorContainer.clientHeight); + expect(rte.clientHeight).to.be.approximately(editorContainer.clientHeight, 2); }); it("content container's and content's height should equal flex wrapper's height without toolbar's height", () => { diff --git a/packages/rich-text-editor/test/validation.test.js b/packages/rich-text-editor/test/validation.test.js new file mode 100644 index 00000000000..46a0c3bf06f --- /dev/null +++ b/packages/rich-text-editor/test/validation.test.js @@ -0,0 +1,186 @@ +import { expect } from '@vaadin/chai-plugins'; +import { fixtureSync, nextRender, nextUpdate } from '@vaadin/testing-helpers'; +import sinon from 'sinon'; +import '../src/vaadin-rich-text-editor.js'; + +describe('validation', () => { + let rte, validateSpy; + + describe('initial', () => { + beforeEach(() => { + rte = document.createElement('vaadin-rich-text-editor'); + validateSpy = sinon.spy(rte, 'validate'); + }); + + afterEach(() => { + rte.remove(); + }); + + it('should not validate without value', async () => { + document.body.appendChild(rte); + await nextRender(); + expect(validateSpy.called).to.be.false; + }); + + describe('with value', () => { + beforeEach(() => { + rte.value = '[{"insert":"Hello World"}]'; + }); + + it('should not validate by default', async () => { + document.body.appendChild(rte); + await nextRender(); + expect(validateSpy.called).to.be.false; + }); + }); + }); + + describe('basic', () => { + let validatedSpy; + + beforeEach(async () => { + rte = fixtureSync(''); + await nextRender(); + validatedSpy = sinon.spy(); + rte.addEventListener('validated', validatedSpy); + }); + + it('should fire a validated event on validation success', () => { + rte.validate(); + + expect(validatedSpy.calledOnce).to.be.true; + const event = validatedSpy.firstCall.args[0]; + expect(event.detail.valid).to.be.true; + }); + + it('should fire a validated event on validation failure', async () => { + rte.required = true; + await nextUpdate(rte); + + rte.validate(); + + expect(validatedSpy.calledOnce).to.be.true; + const event = validatedSpy.firstCall.args[0]; + expect(event.detail.valid).to.be.false; + }); + }); + + describe('required', () => { + beforeEach(async () => { + rte = fixtureSync(''); + await nextRender(); + }); + + it('should fail validation when required and empty', () => { + rte.required = true; + expect(rte.checkValidity()).to.be.false; + expect(rte.validate()).to.be.false; + expect(rte.invalid).to.be.true; + }); + + it('should pass validation when required and has value', async () => { + rte.value = '[{"insert":"Hello"}]'; + rte.required = true; + await nextUpdate(rte); + expect(rte.checkValidity()).to.be.true; + expect(rte.validate()).to.be.true; + expect(rte.invalid).to.be.false; + }); + + it('should pass validation when not required and empty', () => { + expect(rte.checkValidity()).to.be.true; + expect(rte.validate()).to.be.true; + expect(rte.invalid).to.be.false; + }); + + it('should update "invalid" state when "required" is removed', async () => { + rte.required = true; + await nextUpdate(rte); + rte.validate(); + expect(rte.invalid).to.be.true; + + rte.required = false; + await nextUpdate(rte); + expect(rte.invalid).to.be.false; + }); + + it('should update "invalid" state when value changes', async () => { + rte.required = true; + await nextUpdate(rte); + rte.validate(); + expect(rte.invalid).to.be.true; + + rte.value = '[{"insert":"Hello"}]'; + await nextUpdate(rte); + expect(rte.invalid).to.be.false; + }); + + it('should not consider Quill empty content as a value', () => { + rte.required = true; + rte.value = '[{"insert":"\\n"}]'; + expect(rte.checkValidity()).to.be.false; + }); + + it('should set required attribute to true when required', () => { + rte.required = true; + expect(rte.hasAttribute('required')).to.be.true; + }); + + it('should display required indicator when required', () => { + rte.required = true; + expect(rte.hasAttribute('required')).to.be.true; + }); + }); + + describe('checkValidity', () => { + beforeEach(async () => { + rte = fixtureSync(''); + await nextRender(); + }); + + it('should return true when valid', () => { + expect(rte.checkValidity()).to.be.true; + }); + + it('should return true when manually set to invalid but has no constraint violations', () => { + rte.invalid = true; + expect(rte.checkValidity()).to.be.true; + }); + + it('should return false when required and empty', () => { + rte.required = true; + expect(rte.checkValidity()).to.be.false; + }); + + it('should return true when required and has value', async () => { + rte.required = true; + rte.value = '[{"insert":"Test"}]'; + await nextUpdate(rte); + expect(rte.checkValidity()).to.be.true; + }); + }); + + describe('change event', () => { + let changeSpy; + + beforeEach(async () => { + rte = fixtureSync(''); + await nextRender(); + changeSpy = sinon.spy(); + rte.addEventListener('change', changeSpy); + }); + + it('should validate on change event', async () => { + rte.required = true; + await nextUpdate(rte); + + const editor = rte._editor; + editor.insertText(0, 'Hello'); + + rte.dispatchEvent(new Event('change', { bubbles: true })); + + await nextUpdate(rte); + expect(rte.invalid).to.be.false; + }); + }); +}); From 673c9726fd67d738025af0dfe3c99a519bbfe57f Mon Sep 17 00:00:00 2001 From: ugur-vaadin Date: Thu, 30 Oct 2025 17:08:30 +0200 Subject: [PATCH 06/20] test: update snapshots --- .../rich-text-editor.test.snap.js | 638 +++++++++--------- 1 file changed, 334 insertions(+), 304 deletions(-) diff --git a/packages/rich-text-editor/test/dom/__snapshots__/rich-text-editor.test.snap.js b/packages/rich-text-editor/test/dom/__snapshots__/rich-text-editor.test.snap.js index 7d33b47e372..d2d125b058b 100644 --- a/packages/rich-text-editor/test/dom/__snapshots__/rich-text-editor.test.snap.js +++ b/packages/rich-text-editor/test/dom/__snapshots__/rich-text-editor.test.snap.js @@ -16,19 +16,19 @@ snapshots["vaadin-rich-text-editor host"] = > @@ -416,12 +416,23 @@ snapshots["vaadin-rich-text-editor host"] = > + +