diff --git a/Classes/Backend/Controller/PageEditController.php b/Classes/Backend/Controller/PageEditController.php index 7346939..d3839f3 100644 --- a/Classes/Backend/Controller/PageEditController.php +++ b/Classes/Backend/Controller/PageEditController.php @@ -188,6 +188,11 @@ public function __invoke(ServerRequestInterface $request): ResponseInterface $view->assignMultiple([ 'pageId' => $this->pageRecord->getUid(), 'iframeSrc' => $iframeUrl, + 'iframeTitle' => sprintf( + '%s: %s', + $this->getLanguageService()->sL('LLL:EXT:visual_editor/Resources/Private/Language/locallang_mod.xlf:edit_page'), + (string)$this->pageRecord->get('title'), + ), ]); $this->makeButtons($view, $request); diff --git a/README.md b/README.md index 86cce98..7111cd2 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This extension provides visual editing features for content elements in TYPO3 CM - 🧲 Drag-and-drop repositioning of content elements (➕ adding and 🗑️ deleting elements) - ⚡ Real-time preview of changes without page reloads - 😊 User-friendly interface for non-technical editors +- ♿ Accessibility-aware editing controls for TYPO3 editors @@ -153,6 +154,14 @@ Visual Editor uses your regular frontend CSS for the editing view. CSS that is c If your project defines custom rich-text styles, add the relevant rules to your frontend CSS so the page output and the editor share the same styling. Projects with custom `lib.parseFunc_RTE` setups may also need matching frontend rules. +## Accessibility + +Visual Editor is designed with WCAG 2.2 AA as a goal, but this is not a full compliance claim for every TYPO3 project. + +The editor interface includes accessible labels, keyboard-focusable controls, validation announcements, and semantic roles. It has been tested with axe DevTools and NVDA. + +Final accessibility depends on the project templates, CSS, semantic HTML, and editor-authored content. Drag-and-drop workflows are pointer-oriented, so projects should verify alternative workflows for their editor needs. Project-level accessibility should be checked in the integrated TYPO3 site. + ## License and Authors: License type, contributors, contact information This extension is licensed under the [GPL-2.0-or-later](https://spdx.org/licenses/GPL-2.0-or-later.html) license. diff --git a/Resources/Private/Language/de.locallang.xlf b/Resources/Private/Language/de.locallang.xlf index c284484..d35fa80 100644 --- a/Resources/Private/Language/de.locallang.xlf +++ b/Resources/Private/Language/de.locallang.xlf @@ -84,6 +84,24 @@ Änderungen zurücksetzen + + Inhaltselement bearbeiten + + + Inhaltselement anzeigen + + + Inhaltselement löschen + + + Inhaltselement hinzufügen + + + Aktionsleiste + + + Bild bearbeiten + diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf index f5a0daa..6226820 100644 --- a/Resources/Private/Language/locallang.xlf +++ b/Resources/Private/Language/locallang.xlf @@ -84,6 +84,24 @@ reset changes + + Edit content element + + + Show content element + + + Delete content element + + + Add content element + + + Action Bar + + + Edit Image + diff --git a/Resources/Private/Templates/PageEdit.html b/Resources/Private/Templates/PageEdit.html index dca4de7..e28cf20 100644 --- a/Resources/Private/Templates/PageEdit.html +++ b/Resources/Private/Templates/PageEdit.html @@ -10,7 +10,7 @@ - + diff --git a/Resources/Public/Css/editable.css b/Resources/Public/Css/editable.css index b0b93bd..1f86d97 100644 --- a/Resources/Public/Css/editable.css +++ b/Resources/Public/Css/editable.css @@ -80,7 +80,8 @@ img[data-veedit] { outline-offset: -5px; pointer-events: initial; - &:hover { + &:hover, + &:focus { /*filter: brightness(0.6);*/ cursor: pointer; outline-color: #5432fe; @@ -108,6 +109,7 @@ img[data-veedit] { background: center / contain no-repeat url('data:image/svg+xml,'); } -*:has(> img[data-veedit]:hover)::after { +*:has(> img[data-veedit]:hover)::after, +*:has(> img[data-veedit]:focus)::after { opacity: 1; } diff --git a/Resources/Public/JavaScript/Backend/components/ve-auto-save-toggle.js b/Resources/Public/JavaScript/Backend/components/ve-auto-save-toggle.js index eeb5cb6..6ee02bf 100644 --- a/Resources/Public/JavaScript/Backend/components/ve-auto-save-toggle.js +++ b/Resources/Public/JavaScript/Backend/components/ve-auto-save-toggle.js @@ -18,6 +18,11 @@ export class VeAutoSaveToggle extends LitElement { this.classList.toggle('btn-primary', this.active); this.classList.toggle('active', this.active); this.classList.toggle('btn-default', !this.active); + this.setAttribute('role', 'switch'); + this.setAttribute('aria-checked', String(this.active)); + this.setAttribute('aria-disabled', String(this.hasAttribute('disabled'))); + this.tabIndex = this.hasAttribute('disabled') ? -1 : 0; + this.setAttribute('aria-label', this.label); } firstUpdated(changedProperties) { @@ -37,6 +42,7 @@ export class VeAutoSaveToggle extends LitElement { this.label = this.innerText; this.disposeUpdateEditorStateListener = null; this.onClick = this.#onClick.bind(this); + this.onKeydown = this.#onKeydown.bind(this); } connectedCallback() { @@ -47,12 +53,14 @@ export class VeAutoSaveToggle extends LitElement { } this.addEventListener('click', this.onClick); + this.addEventListener('keydown', this.onKeydown); } disconnectedCallback() { this.disposeUpdateEditorStateListener?.(); this.disposeUpdateEditorStateListener = null; this.removeEventListener('click', this.onClick); + this.removeEventListener('keydown', this.onKeydown); super.disconnectedCallback(); } @@ -73,6 +81,10 @@ export class VeAutoSaveToggle extends LitElement { } #onClick(e) { + if(this.hasAttribute('disabled')){ + return; + } + e.preventDefault(); this.active = !this.active; @@ -82,6 +94,13 @@ export class VeAutoSaveToggle extends LitElement { sendMessage('doSave'); } } + #onKeydown(e) { + if (this.hasAttribute('disabled') || (e.key !== 'Enter' && e.key !== ' ')) { + return; + } + e.preventDefault(); + this.#onClick(e); + } static styles = css` :host { diff --git a/Resources/Public/JavaScript/Backend/components/ve-backend-save-button.js b/Resources/Public/JavaScript/Backend/components/ve-backend-save-button.js index f4bcf84..4987542 100644 --- a/Resources/Public/JavaScript/Backend/components/ve-backend-save-button.js +++ b/Resources/Public/JavaScript/Backend/components/ve-backend-save-button.js @@ -21,6 +21,11 @@ export class VeBackendSaveButton extends LitElement { this.classList.toggle('btn-default', this.isInteractionDisabled && !this.hasInvalidFields); this.classList.toggle('btn-warning', !this.isVisuallyDisabled); this.classList.toggle('btn-danger', this.hasInvalidFields); + this.setAttribute('role', 'button'); + this.setAttribute('aria-disabled', String(this.isInteractionDisabled)); + this.setAttribute('aria-busy', String(this.saving)); + this.setAttribute('tabindex', this.isInteractionDisabled ? '-1' : '0'); + this.setAttribute('aria-label', this.getLabel()); } constructor() { @@ -28,7 +33,8 @@ export class VeBackendSaveButton extends LitElement { this.count = 0; this.invalidCount = 0; this.saving = false; - this.onClick = this.onClick.bind(this); + this.onClick = this.#onClick.bind(this); + this.onKeydown = this.#onKeydown.bind(this); this.disposeUpdateEditorStateListener = null; this.disposeOnSaveListener = null; this.disposeSaveEndedListener = null; @@ -51,6 +57,7 @@ export class VeBackendSaveButton extends LitElement { } this.addEventListener('click', this.onClick); + this.addEventListener('keydown', this.onKeydown); } disconnectedCallback() { @@ -61,11 +68,12 @@ export class VeBackendSaveButton extends LitElement { this.disposeSaveEndedListener?.(); this.disposeSaveEndedListener = null; this.removeEventListener('click', this.onClick); + this.removeEventListener('keydown', this.onKeydown); super.disconnectedCallback(); } - render() { + getLabel() { let label = lll('save'); if (this.count > 0) { label = this.count === 1 ? lll('save.change') : lll('save.changes', this.count); @@ -76,6 +84,11 @@ export class VeBackendSaveButton extends LitElement { if (this.saving) { label = lll('saving'); } + return label; + } + + render() { + const label = this.getLabel(); const icon = this.hasInvalidFields ? 'actions-exclamation-circle-alt' : 'actions-save'; return html` @@ -101,7 +114,7 @@ export class VeBackendSaveButton extends LitElement { } } - onClick(e) { + #onClick(e) { e.preventDefault(); if (this.isInteractionDisabled) { return; @@ -109,6 +122,14 @@ export class VeBackendSaveButton extends LitElement { sendMessage('doSave'); } + #onKeydown(e) { + if (this.disabled || (e.key !== 'Enter' && e.key !== ' ')) { + return; + } + e.preventDefault(); + sendMessage('doSave'); + } + get hasChanges() { return this.count > 0; } diff --git a/Resources/Public/JavaScript/Backend/components/ve-show-empty-toggle.js b/Resources/Public/JavaScript/Backend/components/ve-show-empty-toggle.js index 64eabec..d39551c 100644 --- a/Resources/Public/JavaScript/Backend/components/ve-show-empty-toggle.js +++ b/Resources/Public/JavaScript/Backend/components/ve-show-empty-toggle.js @@ -25,6 +25,7 @@ export class VeShowEmptyToggle extends LitElement { sendMessage('showEmpty', this.active); this.onShowEmptyChange = this.#onShowEmptyChange.bind(this); this.onClick = this.#onClick.bind(this); + this.onKeydown = this.#onKeydown.bind(this); } connectedCallback() { @@ -32,11 +33,13 @@ export class VeShowEmptyToggle extends LitElement { showEmptyActive.addEventListener('change', this.onShowEmptyChange); this.addEventListener('click', this.onClick); + this.addEventListener('keydown', this.onKeydown); } disconnectedCallback() { showEmptyActive.removeEventListener('change', this.onShowEmptyChange); this.removeEventListener('click', this.onClick); + this.removeEventListener('keydown', this.onKeydown); super.disconnectedCallback(); } @@ -45,6 +48,10 @@ export class VeShowEmptyToggle extends LitElement { this.classList.toggle('btn-primary', this.active); this.classList.toggle('active', this.active); this.classList.toggle('btn-default', !this.active); + this.setAttribute('role', 'switch'); + this.setAttribute('aria-checked', String(this.active)); + this.setAttribute('aria-label', this.label); + this.tabIndex = 0; } render() { @@ -65,6 +72,14 @@ export class VeShowEmptyToggle extends LitElement { showEmptyActive.set(this.active); sendMessage('showEmpty', this.active); } + + #onKeydown(e) { + if (e.key !== 'Enter' && e.key !== ' ') { + return; + } + e.preventDefault(); + this.#onClick(e); + } } customElements.define('ve-show-empty-toggle', VeShowEmptyToggle); diff --git a/Resources/Public/JavaScript/Backend/components/ve-spotlight-toggle.js b/Resources/Public/JavaScript/Backend/components/ve-spotlight-toggle.js index 0a4ca59..1b73d29 100644 --- a/Resources/Public/JavaScript/Backend/components/ve-spotlight-toggle.js +++ b/Resources/Public/JavaScript/Backend/components/ve-spotlight-toggle.js @@ -24,6 +24,7 @@ export class VeSpotlightToggle extends LitElement { this.active = spotlightActive.get(); this.onSpotlightChange = this.#onSpotlightChange.bind(this); this.onClick = this.#onClick.bind(this); + this.onKeydown = this.#onKeydown.bind(this); } connectedCallback() { @@ -31,11 +32,13 @@ export class VeSpotlightToggle extends LitElement { spotlightActive.addEventListener('change', this.onSpotlightChange); this.addEventListener('click', this.onClick); + this.addEventListener('keydown', this.onKeydown); } disconnectedCallback() { spotlightActive.removeEventListener('change', this.onSpotlightChange); this.removeEventListener('click', this.onClick); + this.removeEventListener('keydown', this.onKeydown); super.disconnectedCallback(); } @@ -44,6 +47,10 @@ export class VeSpotlightToggle extends LitElement { this.classList.toggle('btn-primary', this.active); this.classList.toggle('active', this.active); this.classList.toggle('btn-default', !this.active); + this.setAttribute('role', 'switch'); + this.setAttribute('aria-checked', String(this.active)); + this.setAttribute('aria-label', this.label); + this.tabIndex = 0; } render() { @@ -63,6 +70,14 @@ export class VeSpotlightToggle extends LitElement { this.active = !this.active; spotlightActive.set(this.active); } + + #onKeydown(e) { + if (e.key !== 'Enter' && e.key !== ' ') { + return; + } + e.preventDefault(); + this.#onClick(e); + } } customElements.define('ve-spotlight-toggle', VeSpotlightToggle); diff --git a/Resources/Public/JavaScript/Frontend/components/ve-content-area.js b/Resources/Public/JavaScript/Frontend/components/ve-content-area.js index a2d1303..441a77d 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-content-area.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-content-area.js @@ -1,5 +1,6 @@ import {css, html, LitElement} from 'lit'; import {lll} from "@typo3/core/lit-helper.js"; +import {getAriaRole} from '@typo3/visual-editor/Frontend/get-aria-role'; /** * @extends {HTMLElement} @@ -51,6 +52,10 @@ export class VeContentArea extends LitElement { } element.setAttribute('was', parentTagName); + const role = getAriaRole(parentTagName); + if (role && !element.hasAttribute('role')) { + element.setAttribute('role', role); + } const properties = Object.keys(element.constructor.properties).map(prop => prop.toLowerCase()); for (const attributeName of parent.getAttributeNames()) { if (!properties.includes(attributeName.toLowerCase())) { @@ -59,7 +64,7 @@ export class VeContentArea extends LitElement { } // move every child into element (keep position before and after the element) and remove parent - const oldFirstChild = element.firstChild + const oldFirstChild = element.firstChild; let isAfterSelf = false; for (const child of Array.from(parent.childNodes)) { if (child === element) { diff --git a/Resources/Public/JavaScript/Frontend/components/ve-content-element.js b/Resources/Public/JavaScript/Frontend/components/ve-content-element.js index 5628d56..b29b125 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-content-element.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-content-element.js @@ -5,6 +5,7 @@ import {sendMessage} from '@typo3/visual-editor/Shared/iframe-messaging'; import {openModal} from '@typo3/visual-editor/Frontend/components/ve-iframe-popup'; import {dataHandlerStore} from '@typo3/visual-editor/Frontend/stores/data-handler-store'; import {calculateAllDebounced} from '@typo3/visual-editor/Frontend/auto-no-overlap'; +import {getAriaRole} from '@typo3/visual-editor/Frontend/get-aria-role'; /** * @extends {HTMLElement} @@ -92,6 +93,12 @@ export class VeContentElement extends LitElement { this.isHovered = false; setTimeout(calculateAllDebounced); }); + this.addEventListener('focusin', () => { + this.isFocusWithin = true; + }); + this.addEventListener('focusout', () => { + this.isFocusWithin = false; + }); } connectedCallback() { @@ -159,6 +166,10 @@ export class VeContentElement extends LitElement { const child = element.firstElementChild; element.setAttribute('was', child.tagName.toLowerCase()); + const role = getAriaRole(child.tagName.toLowerCase()); + if (role && !element.hasAttribute('role')) { + element.setAttribute('role', role); + } const properties = Object.keys(element.constructor.properties).map(prop => prop.toLowerCase()); for (const attributeName of child.getAttributeNames()) { if (!properties.includes(attributeName.toLowerCase())) { @@ -174,11 +185,17 @@ export class VeContentElement extends LitElement { render() { const toggleIcon = this.isHidden ? 'actions-toggle-off' : 'actions-toggle-on'; + const editLabel = lll('frontend.editContentElement'); + const toggleLabel = lll('frontend.showContentElement'); + const deleteLabel = lll('frontend.deleteContentElement'); + const addLabel = lll('frontend.addContentElementButton'); const actionBar = html` ${(this.canBeMoved && this.hasContentAreaAsParent) ? '⠿ ' : ''}${this.elementName} @@ -191,12 +208,14 @@ export class VeContentElement extends LitElement { url="${this.editContentContextualUrl}" edit-url="${this.editContentUrl}" class="button" + title="${editLabel}" + aria-label="${editLabel}" > ` : html` - + ` @@ -204,19 +223,19 @@ export class VeContentElement extends LitElement { ${ this.hiddenFieldName ? html` - + ` : '' } - + ${ window.veInfo.allowNewContent ? html` - + ` : '' } `; @@ -237,7 +256,7 @@ export class VeContentElement extends LitElement { tx_container_parent="${this.tx_container_parent}" >` : '' } -
+
`; } @@ -284,7 +303,9 @@ export class VeContentElement extends LitElement { } .action-bar { - display: none; + display: flex; + opacity: 0; + pointer-events: none; gap: 2px; position: absolute; bottom: 100%; @@ -305,8 +326,10 @@ export class VeContentElement extends LitElement { cursor: grab; } - .action-bar.hovered { - display: flex; + .action-bar.hovered, + .action-bar:focus-within { + opacity: 1; + pointer-events: initial; } .action-bar.dragAndDropActive { @@ -324,13 +347,15 @@ export class VeContentElement extends LitElement { .button { display: inline-flex; + align-items: center; + justify-content: center; color: #d9d9d9; + background: transparent; border: 1px solid transparent; border-radius: 0.2em; padding: 0.2em 0.5em; text-decoration: none; cursor: pointer; - height: max-content; transition: border 0.2s; } @@ -339,6 +364,13 @@ export class VeContentElement extends LitElement { border-color: #d9d9d9; background-color: #212121; } + + .button:focus-visible { + border-color: #d9d9d9; + background-color: #212121; + outline: 2px solid #5432fe; + outline-offset: 2px; + } `; } diff --git a/Resources/Public/JavaScript/Frontend/components/ve-editable-rich-text.js b/Resources/Public/JavaScript/Frontend/components/ve-editable-rich-text.js index 653432a..ffefd7d 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-editable-rich-text.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-editable-rich-text.js @@ -1,4 +1,5 @@ import {LitElement} from 'lit'; +import {lll} from '@typo3/core/lit-helper.js'; import {ClassicEditor as Editor} from '@ckeditor/ckeditor5-editor-classic'; // import {InlineEditor as Editor} from '@ckeditor/ckeditor5-editor-inline'; // TODO fix issues with inline editor import {initCKEditorInstance} from '@typo3/rte-ckeditor/init-ckeditor-instance.js'; @@ -70,6 +71,11 @@ export class VeEditableRichText extends LitElement { element.appendChild(wrapper); this.editor = await initCKEditorInstance(this.options || {}, wrapper, wrapper, Editor); + const editableElement = this.editor.ui.getEditableElement(); + if (editableElement instanceof HTMLElement) { + const fieldLabel = this.name || this.title || this.field || this.placeholder; + editableElement.setAttribute('aria-label', lll('editable.title', fieldLabel)); + } this.editor.editing.view.document.getRoot('main').placeholder = this.placeholder; this.editor.model.document.on('change:data', () => { this.value = this.editor.getData({ skipListItemIds: true }); diff --git a/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js b/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js index 9ead545..d7c7596 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js @@ -1,6 +1,6 @@ import {css, html, LitElement} from 'lit'; -import {lll} from '@typo3/core/lit-helper.js'; import {classMap} from 'lit/directives/class-map.js'; +import {lll} from '@typo3/core/lit-helper.js'; import {dataHandlerStore} from '@typo3/visual-editor/Frontend/stores/data-handler-store'; import {showEmptyActive} from '@typo3/visual-editor/Shared/local-stores'; import '@typo3/visual-editor/Frontend/components/ve-icon'; @@ -155,7 +155,7 @@ export class VeEditableText extends LitElement { behavior: 'auto', }); - const slot = this.shadowRoot?.querySelector('.slot'); + const slot = this.#getSlot(); slot?.focus({preventScroll: true}); } @@ -184,10 +184,12 @@ export class VeEditableText extends LitElement { ${buttonControls} ` : html``; const shouldBeInline = this.shouldBeInline(); + const fieldLabel = this.name || this.title || this.field || this.placeholder; + const ariaLabel = lll('editable.title', fieldLabel); this.classList.toggle('block', !shouldBeInline); - const slot = this.shadowRoot?.querySelector('.slot'); + const slot = this.#getSlot(); const showPlaceholder = !this.focused && !(slot?.innerText || this.value).length; const isEmpty = this.value === ''; return html` @@ -202,7 +204,11 @@ export class VeEditableText extends LitElement { style="--button-count: ${buttonCount};" contenteditable="plaintext-only" role="textbox" + aria-label="${ariaLabel}" aria-invalid="${this.invalid ? 'true' : 'false'}" + aria-errormessage="${this.validationErrors.join(', ')}" + aria-multiline="${this.allowNewlines ? 'true' : 'false'}" + aria-required="${this.isRequired() ? 'true' : 'false'}" spellcheck="true" data-placeholder="${showPlaceholder ? (this.placeholder || '\u200B'/* placeholder keeps firefox from breaking out*/) : ''}" @focus="${() => { @@ -340,7 +346,7 @@ export class VeEditableText extends LitElement { this.changed = dataHandlerStore.hasChangedData(this.table, this.uid, this.field); this.valueInitial = dataHandlerStore.initialData[this.table]?.[this.uid]?.[this.field] ?? this.valueInitial; const storedValue = dataHandlerStore.data[this.table]?.[this.uid]?.[this.field] ?? this.valueInitial; - const slot = this.shadowRoot?.querySelector('.slot'); + const slot = this.#getSlot(); const isFocused = this.matches(':focus-within'); if (!isFocused && storedValue?.trim() !== slot?.innerText?.trim()) { this.skipNextValueNormalization = true; @@ -353,14 +359,18 @@ export class VeEditableText extends LitElement { * @param {string} value */ #setSlotText(value) { - const element = this.shadowRoot?.querySelector('.slot'); + const element = this.#getSlot(); if (element) { element.innerText = value; } } #getSlotText() { - return this.shadowRoot?.querySelector('.slot')?.innerText.replace(/\n$/, '') ?? ''; + return this.#getSlot()?.innerText.replace(/\n$/, '') ?? ''; + } + + #getSlot() { + return this.shadowRoot?.querySelector('.slot'); } /** @@ -486,8 +496,7 @@ export class VeEditableText extends LitElement { let normalizedValue = normalizeValue(value, this.validation).text; const min = Number(this.validation?.min || 0); - const isRequired = Boolean(this.validation?.required || false); - if (normalizedValue.length < min && !isRequired) { + if (normalizedValue.length < min && !this.isRequired()) { normalizedValue = ''; } @@ -500,6 +509,10 @@ export class VeEditableText extends LitElement { return normalizedValue; } + isRequired() { + return Boolean(this.validation?.required || false); + } + /** * @param {string} value */ diff --git a/Resources/Public/JavaScript/Frontend/components/ve-icon.js b/Resources/Public/JavaScript/Frontend/components/ve-icon.js index 751a32d..402630e 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-icon.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-icon.js @@ -36,7 +36,7 @@ export class VeIcon extends LitElement { render() { const icon = unsafeHTML(this.icons[this.name]) || html`missing icon ${this.name}`; return html` - ${icon}`; + `; } static styles = css` diff --git a/Resources/Public/JavaScript/Frontend/components/ve-iframe-popup.js b/Resources/Public/JavaScript/Frontend/components/ve-iframe-popup.js index 4e5a9a6..d62a843 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-iframe-popup.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-iframe-popup.js @@ -39,6 +39,11 @@ export class VeIframePopup extends LitElement { align-items: center; gap: 5px; } + + button:focus-visible { + outline: 2px solid #5432fe; + outline-offset: 2px; + } `; constructor() { @@ -58,7 +63,7 @@ export class VeIframePopup extends LitElement { render() { return html` - `; diff --git a/Resources/Public/JavaScript/Frontend/components/ve-reset-button.js b/Resources/Public/JavaScript/Frontend/components/ve-reset-button.js index 1d4d5e2..ef75413 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-reset-button.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-reset-button.js @@ -15,7 +15,12 @@ export class VeResetButton extends LitElement { padding: 0; font: inherit; cursor: pointer; - outline: inherit; + border-radius: 0.2em; + } + + button:focus-visible { + outline: 2px solid #5432fe; + outline-offset: 2px; } `; @@ -26,8 +31,9 @@ export class VeResetButton extends LitElement { } render() { + const label = lll('frontend.resetChanges'); return html` - `; diff --git a/Resources/Public/JavaScript/Frontend/components/ve-validation-message.js b/Resources/Public/JavaScript/Frontend/components/ve-validation-message.js index 125d055..30f9e85 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-validation-message.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-validation-message.js @@ -47,7 +47,7 @@ export class VeValidationMessage extends LitElement { render() { return html` -
+
${this.reason}
`; diff --git a/Resources/Public/JavaScript/Frontend/components/ve-validation-overlay.js b/Resources/Public/JavaScript/Frontend/components/ve-validation-overlay.js index 932956b..8b4afcf 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-validation-overlay.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-validation-overlay.js @@ -24,7 +24,10 @@ export class VeValidationOverlay extends LitElement { const uniqueValidationErrors = [...new Set(this.validationErrors)]; return html` -
+
${uniqueValidationErrors.map((reason) => html` { - const data = JSON.parse(image.dataset.veedit); - if (!data.editUrl) { - return; - } + const data = JSON.parse(image.dataset.veedit); + if (!data.editUrl) { + return; + } + image.setAttribute('role', 'button'); + image.setAttribute('tabindex', '0'); + image.setAttribute('aria-label', lll('frontend.editImageButton')); + const listener = (e) => { e.preventDefault(); e.stopImmediatePropagation(); @@ -25,6 +29,14 @@ export function initializeImageHandling() { tag.setAttribute('url', data.url); tag.setAttribute('edit-url', data.editUrl); tag.click(); + }; + image.addEventListener('click', listener); + image.addEventListener('keydown', (e) => { + if ((e.key !== 'Enter' && e.key !== ' ')) { + return; + } + + listener(e); }); } } diff --git a/Resources/Public/JavaScript/Frontend/initialize-spotlight-handling.js b/Resources/Public/JavaScript/Frontend/initialize-spotlight-handling.js index 99785f3..f725eb7 100644 --- a/Resources/Public/JavaScript/Frontend/initialize-spotlight-handling.js +++ b/Resources/Public/JavaScript/Frontend/initialize-spotlight-handling.js @@ -4,7 +4,7 @@ import {spotlightActive} from '@typo3/visual-editor/Shared/local-stores'; export function initializeSpotlightHandling() { const setSpotlight = () => { if (spotlightActive.get()) { - highlight('ve-editable-text, ve-editable-rich-text, .ck-editor__top'); + highlight('ve-editable-text, ve-editable-rich-text, .ck-editor__top, img[data-veedit]'); } else { reset(); }