From c1c9941c4b7f7948b5b344d6d14679f8f35a9f11 Mon Sep 17 00:00:00 2001 From: Joe Galluccio Date: Thu, 12 Feb 2026 18:19:33 -0500 Subject: [PATCH 01/10] new module and some config --- composer.json | 1 + composer.lock | 51 ++++++++++++++++++- ...y_form_display.paragraph.image.default.yml | 5 +- conf/drupal/config/core.extension.yml | 1 + 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 315a15786c..1f75f4c396 100644 --- a/composer.json +++ b/composer.json @@ -175,6 +175,7 @@ "drupal/csv_field": "^3.0", "drupal/csv_serialization": "^2.0 || ^3.0", "drupal/datalayer": "^2", + "drupal/decorative_image_widget": "^1.0", "drupal/devel": "^5", "drupal/diff": "^1.7", "drupal/draggableviews": "^2.1", diff --git a/composer.lock b/composer.lock index fba60a6a2b..20b58662b5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3e18a2b82efdddd50527c678699a2d87", + "content-hash": "2e7f50eb5857b1c3e75e53c24f7fcfaf", "packages": [ { "name": "akamai-open/edgegrid-auth", @@ -4715,6 +4715,55 @@ "issues": "https://www.drupal.org/project/issues/datalayer" } }, + { + "name": "drupal/decorative_image_widget", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/decorative_image_widget.git", + "reference": "1.0.3" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/decorative_image_widget-1.0.3.zip", + "reference": "1.0.3", + "shasum": "70fa54b7e78d1e5d021dc204a49ef02308ce7790" + }, + "require": { + "drupal/core": "^9.2 || ^10 || ^11" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "1.0.3", + "datestamp": "1768256827", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "bkosborne", + "homepage": "https://www.drupal.org/user/788032" + }, + { + "name": "mably", + "homepage": "https://www.drupal.org/user/3375160" + } + ], + "description": "Modifies image widgets to require alt text OR be marked as decorative.", + "homepage": "https://drupal.org/project/decorative_image_widget", + "support": { + "source": "https://git.drupalcode.org/project/decorative_image_widget", + "issues": "https://www.drupal.org/project/issues/decorative_image_widget" + } + }, { "name": "drupal/devel", "version": "5.3.1", diff --git a/conf/drupal/config/core.entity_form_display.paragraph.image.default.yml b/conf/drupal/config/core.entity_form_display.paragraph.image.default.yml index f26aa653a3..53f6774d7a 100644 --- a/conf/drupal/config/core.entity_form_display.paragraph.image.default.yml +++ b/conf/drupal/config/core.entity_form_display.paragraph.image.default.yml @@ -14,6 +14,7 @@ dependencies: - image.style.thumbnail - paragraphs.paragraphs_type.image module: + - decorative_image_widget - image - text id: paragraph.image.default @@ -28,7 +29,9 @@ content: settings: progress_indicator: throbber preview_image_style: thumbnail - third_party_settings: { } + third_party_settings: + decorative_image_widget: + use_decorative_checkbox: true field_image_administrative_title: type: string_textfield weight: 2 diff --git a/conf/drupal/config/core.extension.yml b/conf/drupal/config/core.extension.yml index fedd4e098a..60398bd053 100644 --- a/conf/drupal/config/core.extension.yml +++ b/conf/drupal/config/core.extension.yml @@ -40,6 +40,7 @@ module: datalayer: 0 datetime: 0 datetime_range: 0 + decorative_image_widget: 0 diff: 0 draggableviews: 0 dropzonejs: 0 From 45ed4b70c2c842f87e55cf9910e37b71150eba52 Mon Sep 17 00:00:00 2001 From: Arthur Baghdasaryan Date: Mon, 16 Mar 2026 15:18:59 +0400 Subject: [PATCH 02/10] Entity embed support --- composer.json | 3 + composer.lock | 2 +- ....entity_form_display.node.news.default.yml | 5 +- .../EntityEmbedDecorativeImageTest.php | 79 ++++++ .../DecorativeImageWidgetTest.php | 73 +++++ ...decorative_image_widget_entity_embed.patch | 251 ++++++++++++++++++ 6 files changed, 411 insertions(+), 2 deletions(-) create mode 100644 docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php create mode 100644 docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/DecorativeImageWidgetTest.php create mode 100644 patches/DP-45040_decorative_image_widget_entity_embed.patch diff --git a/composer.json b/composer.json index d197d3a1ef..07af961614 100644 --- a/composer.json +++ b/composer.json @@ -421,6 +421,9 @@ "500 error: Possible infinite loop": "https://www.drupal.org/files/issues/2019-02-15/diff-500-errors-possible-infinite-loop-3033455-2-d8.patch", "Make revisions overview page accessible (https://www.drupal.org/project/diff/issues/3228798)": "https://www.drupal.org/files/issues/2024-05-14/diff-3228798-17.patch" }, + "drupal/decorative_image_widget": { + "Entity embed support (https://www.drupal.org/project/decorative_image_widget/issues/3577172)": "patches/DP-45040_decorative_image_widget_entity_embed.patch" + }, "drupal/dropzonejs": { "Dropzone element overwrites potential metadata and libraries added by other modules (https://www.drupal.org/project/drupal/issues/3303028)": "https://www.drupal.org/files/issues/2023-01-18/dropzone_overwrites_metadata_and_libraries-3303028-2_with-3211288.patch" }, diff --git a/composer.lock b/composer.lock index f91c0a2648..f0b70849c2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2e7f50eb5857b1c3e75e53c24f7fcfaf", + "content-hash": "db3015e3bd54bc982230f4bbe830cba7", "packages": [ { "name": "akamai-open/edgegrid-auth", diff --git a/conf/drupal/config/core.entity_form_display.node.news.default.yml b/conf/drupal/config/core.entity_form_display.node.news.default.yml index 98931e4e75..10afd66eb7 100644 --- a/conf/drupal/config/core.entity_form_display.node.news.default.yml +++ b/conf/drupal/config/core.entity_form_display.node.news.default.yml @@ -33,6 +33,7 @@ dependencies: - allowed_formats - content_moderation - datetime + - decorative_image_widget - entity_hierarchy - entity_reference_tree - field_group @@ -302,7 +303,9 @@ content: settings: progress_indicator: throbber preview_image_style: thumbnail - third_party_settings: { } + third_party_settings: + decorative_image_widget: + use_decorative_checkbox: true field_news_lede: type: string_textfield weight: 6 diff --git a/docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php b/docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php new file mode 100644 index 0000000000..00caca3f36 --- /dev/null +++ b/docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php @@ -0,0 +1,79 @@ + ['attributes', 'alt'], + ]; + } + + /** + * Decorative unchecked: empty alt should trigger a validation error. + */ + public function testEmptyAltWithoutDecorativeFailsValidation(): void { + $form_state = new FormState(); + $form_state->setValue(['attributes', 'alt'], ''); + $form_state->setValue(['decorative'], FALSE); + + $element = $this->buildAltElement(); + + DecorativeImageWidgetHelper::validateEntityEmbedAlt($element, $form_state); + + $this->assertTrue($form_state->hasAnyErrors(), 'Empty alt without decorative checked must fail validation.'); + } + + /** + * Decorative checked: empty alt should pass validation. + */ + public function testEmptyAltWithDecorativePassesValidation(): void { + $form_state = new FormState(); + $form_state->setValue(['attributes', 'alt'], ''); + $form_state->setValue(['attributes', 'decorative'], TRUE); + + $element = $this->buildAltElement(); + + DecorativeImageWidgetHelper::validateEntityEmbedAlt($element, $form_state); + + // We only assert that the alt value stays empty when decorative is checked. + $this->assertSame('', $form_state->getValue(['attributes', 'alt']), 'Alt remains empty when decorative is checked.'); + } + + /** + * Decorative checked: any typed alt is forced to empty on save. + */ + public function testTypedAltIsClearedWhenDecorativeChecked(): void { + $form_state = new FormState(); + $form_state->setValue(['attributes', 'alt'], 'Some description'); + $form_state->setValue(['attributes', 'decorative'], TRUE); + + $element = $this->buildAltElement(); + + DecorativeImageWidgetHelper::validateEntityEmbedAlt($element, $form_state); + + $this->assertSame('', $form_state->getValue(['attributes', 'alt']), 'Alt is cleared when decorative is checked.'); + } + +} + diff --git a/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/DecorativeImageWidgetTest.php b/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/DecorativeImageWidgetTest.php new file mode 100644 index 0000000000..d3adaae240 --- /dev/null +++ b/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/DecorativeImageWidgetTest.php @@ -0,0 +1,73 @@ +createUser(); + $admin->addRole('administrator'); + $admin->activate(); + $admin->save(); + return $admin; + } + + /** + * Creates and returns a news node with an image. + */ + private function createNewsWithImage() { + $image = File::create([ + 'uri' => 'public://test_news_image.jpg', + ]); + $this->markEntityForCleanup($image); + $image->save(); + + $news_node = $this->createNode([ + 'type' => 'news', + 'title' => $this->randomMachineName(), + 'status' => 1, + 'moderation_state' => MassModeration::PUBLISHED, + 'field_news_image' => [ + 'target_id' => $image->id(), + ], + ]); + $news_node->save(); + + return $news_node; + } + + /** + * Verifies that decorative_image_widget adds the checkbox to field_news_image. + */ + public function testNewsImageHasDecorativeCheckbox() { + $this->drupalLogin($this->createAdmin()); + + $news = $this->createNewsWithImage(); + + // Edit the News node and inspect the image widget. + $this->drupalGet($news->toUrl('edit-form')->toString()); + $page = $this->getSession()->getPage(); + + $widget = $page->find('css', '.field--name-field-news-image .image-widget'); + $this->assertNotNull($widget, 'Image widget container found for field_news_image.'); + + // Decorative checkbox should be present when decorative_image_widget is enabled + // and configured for this field. + $decorative_checkbox = $widget->find('css', 'input.decorative-checkbox[type="checkbox"]'); + $this->assertNotNull($decorative_checkbox, 'Decorative checkbox present on field_news_image widget.'); + } + +} + diff --git a/patches/DP-45040_decorative_image_widget_entity_embed.patch b/patches/DP-45040_decorative_image_widget_entity_embed.patch new file mode 100644 index 0000000000..4b831fd794 --- /dev/null +++ b/patches/DP-45040_decorative_image_widget_entity_embed.patch @@ -0,0 +1,251 @@ +diff --git a/decorative_image_widget.module b/decorative_image_widget.module +index 8b2dded80e881a7df919b7e8c93737e29b425aeb..560101e2b887a161df2fe0c58c40560d406eaf22 100644 +--- a/decorative_image_widget.module ++++ b/decorative_image_widget.module +@@ -51,3 +51,17 @@ function decorative_image_widget_field_widget_single_element_form_alter(array &$ + DecorativeImageWidgetHelper::alter($element, $context['widget']); + } + } ++ ++/** ++ * Implements hook_entity_embed_display_plugins_alter(). ++ * ++ * Swap the core image entity embed display plugin class for our decorated ++ * version that adds the Decorative checkbox and validation. ++ */ ++function decorative_image_widget_entity_embed_display_plugins_alter(array &$definitions) { ++ foreach ($definitions as &$definition) { ++ if (!empty($definition['class']) && $definition['class'] === 'Drupal\\entity_embed\\Plugin\\entity_embed\\EntityEmbedDisplay\\ImageFieldFormatter') { ++ $definition['class'] = \Drupal\decorative_image_widget\Plugin\entity_embed\EntityEmbedDisplay\DecorativeImageFieldFormatter::class; ++ } ++ } ++} +diff --git a/js/decorative-image-widget.js b/js/decorative-image-widget.js +index 55de6fd8902b83a5d986c68b89390bfb202482b8..ec7bb1133a00d740a0a9966b379878b451522ad6 100644 +--- a/js/decorative-image-widget.js ++++ b/js/decorative-image-widget.js +@@ -43,11 +43,22 @@ + // text field. + once( + 'decorative-image-widget', +- '.image-widget .decorative-checkbox', ++ '.decorative-checkbox', + context, +- ).forEach(function processCheckbox(checkbox) { +- const widget = checkbox.closest('.image-widget'); ++ ).forEach((checkbox) => { ++ // For field widgets, the checkbox lives inside .image-widget; for the ++ // entity embed dialog, it lives directly in the dialog form. Support ++ // both by looking for the nearest image widget first, then falling ++ // back to the enclosing form. ++ const widget = ++ checkbox.closest('.image-widget') || checkbox.closest('form'); ++ if (!widget) { ++ return; ++ } + const altTextField = widget.querySelector('.alt-textfield'); ++ if (!altTextField) { ++ return; ++ } + checkbox.addEventListener('change', (event) => { + enableOrDisableAltTextField(altTextField, !event.target.checked); + }); +diff --git a/src/DecorativeImageWidgetHelper.php b/src/DecorativeImageWidgetHelper.php +index 6047467fd56b55338055fa5c64bed16541f335bb..224cdb0387db576ea2c61a6c9cc41d4ae7c691ac 100644 +--- a/src/DecorativeImageWidgetHelper.php ++++ b/src/DecorativeImageWidgetHelper.php +@@ -191,4 +191,102 @@ class DecorativeImageWidgetHelper { + } + } + ++ /** ++ * Alters the entity embed dialog form to add a decorative checkbox. ++ * ++ * This mirrors the UX provided for image widgets, but applied to the image ++ * embed dialog used by entity_embed. ++ * ++ * @param array $form ++ * The form array. ++ * @param \Drupal\Core\Form\FormStateInterface $formState ++ * The form state. ++ */ ++ public static function alterEntityEmbedDialogForm(array &$form, FormStateInterface $formState) { ++ // At the time this alter runs, the image entity embed display plugin's alt ++ // element has already been moved under the "attributes" container. ++ if (empty($form['attributes']['alt']) || !is_array($form['attributes']['alt'])) { ++ return; ++ } ++ ++ $alt =& $form['attributes']['alt']; ++ ++ // Determine if the decorative checkbox should be checked by default. When ++ // editing an existing embed, the current alt value is in the default value. ++ $alt_default = $alt['#default_value'] ?? ''; ++ $decorative_default = ($alt_default === ''); ++ ++ $decorative = [ ++ '#type' => 'checkbox', ++ '#title' => t('Decorative'), ++ '#description' => t( ++ 'This image is decorative and should be hidden from screen readers.', ++ ['@url' => 'https://www.w3.org/WAI/tutorials/images/decorative/'] ++ ), ++ '#default_value' => $decorative_default, ++ '#weight' => $alt['#weight'] ?? 0, ++ '#attributes' => [ ++ 'class' => ['decorative-checkbox'], ++ ], ++ ]; ++ ++ // Place the decorative checkbox inside the attributes group as a sibling of ++ // the alt field, so it appears in the same section in the dialog. ++ $form['attributes']['decorative'] = $decorative; ++ ++ // Add a class name to the alt textfield so JS can easily find it. ++ $alt['#attributes']['class'][] = 'alt-textfield'; ++ ++ // Add validation for the alt text that will require it have a value unless ++ // the decorative checkbox is checked. ++ $alt['#element_validate'][] = [static::class, 'validateEntityEmbedAlt']; ++ ++ // Attach the same JS library used for widgets so the decorative checkbox ++ // disables/enables the alt field. ++ $form['#attached']['library'][] = 'decorative_image_widget/decorative_image_widget'; ++ } ++ ++ /** ++ * Validation for the entity embed dialog alt field. ++ * ++ * Requires either alt text or the decorative checkbox to be checked. ++ * ++ * @param array $element ++ * The alt form element. ++ * @param \Drupal\Core\Form\FormStateInterface $formState ++ * The form state. ++ */ ++ public static function validateEntityEmbedAlt(array $element, FormStateInterface $formState) { ++ // In the embed dialog there is no file upload button, so we always perform ++ // validation when the form is submitted. ++ $alt_value = trim((string) ($formState->getValue($element['#parents']) ?? '')); ++ ++ // Normalize two double quotes ("") to an empty alt value. This mirrors ++ // the behavior provided by MediaImageDecorator::EMPTY_STRING in the ++ // entity_embed module for the core image dialog, without introducing a ++ // hard dependency on that module. ++ if ($alt_value === '""') { ++ $alt_value = ''; ++ $formState->setValue($element['#parents'], ''); ++ } ++ ++ $decorative_parents = $element['#parents']; ++ array_pop($decorative_parents); ++ $decorative_parents[] = 'decorative'; ++ $decorative_checked = (bool) $formState->getValue($decorative_parents); ++ ++ // If the decorative checkbox is checked, always store an empty alt value, ++ // regardless of what the user typed. This ensures the underlying ++ // markup and rendered both end up with alt="" and ++ // allows us to infer "decorative" from the empty alt on subsequent edits. ++ if ($decorative_checked) { ++ $formState->setValue($element['#parents'], ''); ++ $alt_value = ''; ++ } ++ ++ if ($alt_value === '' && !$decorative_checked) { ++ $formState->setErrorByName(implode('][', $element['#parents']), t('You must provide alternative text or indicate the image is decorative.')); ++ } ++ } ++ + } +diff --git a/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php b/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php +new file mode 100644 +index 0000000000000000000000000000000000000000..9cff9e2a6be5ade0a2feec17e802152b6efec2df +--- /dev/null ++++ b/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php +@@ -0,0 +1,86 @@ ++ 'checkbox', ++ '#title' => $this->t('Decorative'), ++ '#description' => $this->t('This image is decorative and should be hidden from screen readers.', [ ++ ':url' => 'https://www.w3.org/WAI/tutorials/images/decorative/', ++ ]), ++ '#default_value' => ($alt_value === ''), ++ '#parents' => ['decorative'], ++ '#weight' => $form['alt']['#weight'] ?? 0, ++ '#attributes' => [ ++ 'class' => ['decorative-checkbox'], ++ ], ++ ]; ++ ++ // Attach JS behavior (if the decorative_image_widget library is available) ++ // so that checking the decorative box disables the alt field. ++ try { ++ $library_discovery = \Drupal::service('library.discovery'); ++ if ($library_discovery->getLibraryByName('decorative_image_widget', 'decorative_image_widget')) { ++ $form['#attached']['library'][] = 'decorative_image_widget/decorative_image_widget'; ++ } ++ } ++ catch (\Exception $e) { ++ // If the library is not available, we still keep the validation behavior. ++ } ++ ++ return $form; ++ } ++ ++ /** ++ * Validation callback requiring either alt text or decorative checkbox. ++ */ ++ public static function validateAltOrDecorative(array $element, FormStateInterface $form_state, array &$complete_form) { ++ $parents = $element['#parents']; ++ $alt_value = trim((string) ($form_state->getValue($parents) ?? '')); ++ ++ $decorative_checked = (bool) $form_state->getValue(['decorative']); ++ ++ // If the decorative checkbox is checked, always store an empty alt value, ++ // regardless of what the user typed. ++ if ($decorative_checked) { ++ $form_state->setValue($parents, ''); ++ $alt_value = ''; ++ } ++ ++ if ($alt_value === '' && !$decorative_checked) { ++ $form_state->setError($element, t('You must provide alternative text or indicate the image is decorative.')); ++ } ++ } ++ ++} ++ From 0d16b7697cafd4b95a18a3c1c767bc966a12e8cb Mon Sep 17 00:00:00 2001 From: Arthur Baghdasaryan Date: Mon, 16 Mar 2026 15:21:17 +0400 Subject: [PATCH 03/10] Entity embed support --- changelogs/DP-45040.yml | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 changelogs/DP-45040.yml diff --git a/changelogs/DP-45040.yml b/changelogs/DP-45040.yml new file mode 100644 index 0000000000..03fb1ad1eb --- /dev/null +++ b/changelogs/DP-45040.yml @@ -0,0 +1,41 @@ +# +# Write your changelog entry here. Every pull request must have a changelog yml file. +# +# Change types: +# ############################################################################# +# You can use one of the following types: +# - Added: For new features. +# - Changed: For changes to existing functionality. +# - Deprecated: For soon-to-be removed features. +# - Removed: For removed features. +# - Fixed: For any bug fixes. +# - Security: In case of vulnerabilities. +# +# Format +# ############################################################################# +# The format is crucial. Please follow the examples below. For reference, the requirements are: +# - All 3 parts are required and you must include "Type", "description" and "issue". +# - "Type" must be left aligned and followed by a colon. +# - "description" must be indented with 2 spaces followed by a colon +# - "issue" must be indented with 4 spaces followed by a colon. +# - "issue" is for the Jira ticket number only e.g. DP-1234 +# - No extra spaces, indents, or blank lines are allowed. +# +# Example: +# ############################################################################# +# Fixed: +# - description: Fixes scrolling on edit pages in Safari. +# issue: DP-13314 +# +# You may add more than 1 description & issue for each type using the following format: +# Changed: +# - description: Automating the release branch. +# issue: DP-10166 +# - description: Second change item that needs a description. +# issue: DP-19875 +# - description: Third change item that needs a description along with an issue. +# issue: DP-19843 +# +Added: + - description: Decorative image widget module. + issue: DP-45040 From 4c826db10a820c96134fe8e46fda15f9271f7e97 Mon Sep 17 00:00:00 2001 From: Arthur Baghdasaryan Date: Mon, 16 Mar 2026 15:24:30 +0400 Subject: [PATCH 04/10] Entity embed support --- .../tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php | 1 - .../src/ExistingSiteJavascript/DecorativeImageWidgetTest.php | 1 - 2 files changed, 2 deletions(-) diff --git a/docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php b/docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php index 00caca3f36..2d5bf4d3dd 100644 --- a/docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php +++ b/docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php @@ -76,4 +76,3 @@ public function testTypedAltIsClearedWhenDecorativeChecked(): void { } } - diff --git a/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/DecorativeImageWidgetTest.php b/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/DecorativeImageWidgetTest.php index d3adaae240..83407de5eb 100644 --- a/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/DecorativeImageWidgetTest.php +++ b/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/DecorativeImageWidgetTest.php @@ -70,4 +70,3 @@ public function testNewsImageHasDecorativeCheckbox() { } } - From 5620549ebec4833992d98b260337b8bbba2adbe3 Mon Sep 17 00:00:00 2001 From: Joe Galluccio Date: Thu, 19 Mar 2026 17:25:42 -0400 Subject: [PATCH 05/10] updated a group of fields --- .../core.entity_form_display.node.event.default.yml | 9 +++++++-- ...ld.field.node.binder.field_binder_banner_image.yml | 2 +- .../field.field.node.location.field_bg_narrow.yml | 11 +++++++++-- .../config/field.field.node.news.field_news_image.yml | 2 +- .../config/field.storage.node.field_bg_narrow.yml | 2 +- .../field.storage.node.field_binder_banner_image.yml | 2 +- .../config/field.storage.node.field_news_image.yml | 2 +- docroot/modules/custom/mass_fields/mass_fields.module | 7 ++++--- .../ImageAltDescriptionTest.php | 4 ++-- 9 files changed, 27 insertions(+), 14 deletions(-) diff --git a/conf/drupal/config/core.entity_form_display.node.event.default.yml b/conf/drupal/config/core.entity_form_display.node.event.default.yml index 13abf83aa7..d3224173a6 100644 --- a/conf/drupal/config/core.entity_form_display.node.event.default.yml +++ b/conf/drupal/config/core.entity_form_display.node.event.default.yml @@ -46,6 +46,7 @@ dependencies: - content_moderation - datetime - datetime_range + - decorative_image_widget - entity_browser_entity_form - entity_hierarchy - entity_reference_tree @@ -335,7 +336,9 @@ content: settings: progress_indicator: throbber preview_image_style: thumbnail - third_party_settings: { } + third_party_settings: + decorative_image_widget: + use_decorative_checkbox: true field_event_lede: type: string_textfield weight: 121 @@ -371,7 +374,9 @@ content: settings: progress_indicator: throbber preview_image_style: thumbnail - third_party_settings: { } + third_party_settings: + decorative_image_widget: + use_decorative_checkbox: true field_event_meeting_agenda_sect: type: entity_reference_paragraphs weight: 59 diff --git a/conf/drupal/config/field.field.node.binder.field_binder_banner_image.yml b/conf/drupal/config/field.field.node.binder.field_binder_banner_image.yml index 1e580b5c94..e3e2bae8c1 100644 --- a/conf/drupal/config/field.field.node.binder.field_binder_banner_image.yml +++ b/conf/drupal/config/field.field.node.binder.field_binder_banner_image.yml @@ -12,7 +12,7 @@ field_name: field_binder_banner_image entity_type: node bundle: binder label: 'Banner image' -description: 'Optional. Add a rectangular image that relates to the overarching theme of the binder. Minimum size: 600 x 450 pixels. Images using a different height to width ratio will be cropped.' +description: 'Optional. Image should be purely decorative and not convey any information that is not in the text of this page. Image should be rectangular and relate to the overarching theme of the binder. Minimum size: 600 x 450 pixels. Images using a different height to width ratio will be cropped.' required: false translatable: false default_value: { } diff --git a/conf/drupal/config/field.field.node.location.field_bg_narrow.yml b/conf/drupal/config/field.field.node.location.field_bg_narrow.yml index 8da5e70c07..aaf277eca3 100644 --- a/conf/drupal/config/field.field.node.location.field_bg_narrow.yml +++ b/conf/drupal/config/field.field.node.location.field_bg_narrow.yml @@ -6,7 +6,14 @@ dependencies: - field.storage.node.field_bg_narrow - node.type.location module: + - content_translation - image +third_party_settings: + content_translation: + translation_sync: + alt: alt + title: title + file: '0' id: node.location.field_bg_narrow field_name: field_bg_narrow entity_type: node @@ -25,8 +32,8 @@ settings: max_filesize: '' max_resolution: '' min_resolution: 800x400 - alt_field: false - alt_field_required: true + alt_field: true + alt_field_required: false title_field: false title_field_required: false default_image: diff --git a/conf/drupal/config/field.field.node.news.field_news_image.yml b/conf/drupal/config/field.field.node.news.field_news_image.yml index 23229182a7..86bc1a309b 100644 --- a/conf/drupal/config/field.field.node.news.field_news_image.yml +++ b/conf/drupal/config/field.field.node.news.field_news_image.yml @@ -12,7 +12,7 @@ field_name: field_news_image entity_type: node bundle: news label: 'Featured image' -description: "

Optional. Add an image to accompany this item.

\r\n

Only add image ALT text if the image is not purely decorative and conveys meaningful information to visitors. If purely decorative, leave blank.

" +description: "

Optional. Add an image to accompany this item.

\r\n

Only add image ALT text if the image is not purely decorative and conveys meaningful information to visitors. If purely decorative, leave blank and check the \"decorative\" checkbox below.

" required: false translatable: false default_value: { } diff --git a/conf/drupal/config/field.storage.node.field_bg_narrow.yml b/conf/drupal/config/field.storage.node.field_bg_narrow.yml index 01428da08e..d40e12374a 100644 --- a/conf/drupal/config/field.storage.node.field_bg_narrow.yml +++ b/conf/drupal/config/field.storage.node.field_bg_narrow.yml @@ -13,7 +13,7 @@ type: image settings: target_type: file display_field: false - display_default: false + display_default: true uri_scheme: public default_image: uuid: '' diff --git a/conf/drupal/config/field.storage.node.field_binder_banner_image.yml b/conf/drupal/config/field.storage.node.field_binder_banner_image.yml index 835e4daa71..089c7f68ac 100644 --- a/conf/drupal/config/field.storage.node.field_binder_banner_image.yml +++ b/conf/drupal/config/field.storage.node.field_binder_banner_image.yml @@ -13,7 +13,7 @@ type: image settings: target_type: file display_field: false - display_default: false + display_default: true uri_scheme: public default_image: uuid: '' diff --git a/conf/drupal/config/field.storage.node.field_news_image.yml b/conf/drupal/config/field.storage.node.field_news_image.yml index 8e25917089..41ee94d8a7 100644 --- a/conf/drupal/config/field.storage.node.field_news_image.yml +++ b/conf/drupal/config/field.storage.node.field_news_image.yml @@ -13,7 +13,7 @@ type: image settings: target_type: file display_field: false - display_default: false + display_default: true uri_scheme: public default_image: uuid: '' diff --git a/docroot/modules/custom/mass_fields/mass_fields.module b/docroot/modules/custom/mass_fields/mass_fields.module index 99000ecea2..9c75fbb09c 100644 --- a/docroot/modules/custom/mass_fields/mass_fields.module +++ b/docroot/modules/custom/mass_fields/mass_fields.module @@ -950,6 +950,7 @@ function mass_fields_field_widget_single_element_image_image_form_alter(&$elemen 'field_featured_item_image' => '_mass_fields_image_widget_alt_help_text_process', 'field_event_image' => '_mass_fields_event_image_widget_alt_help_text_process', 'field_event_logo' => '_mass_fields_event_image_widget_alt_help_text_process', + 'field_bg_narrow' => '_mass_fields_event_image_widget_alt_help_text_process', ]; // Handle fields that have a direct mapping @@ -983,7 +984,7 @@ function _mass_fields_microsite_key_message_image_process($element, &$form_state */ function _mass_fields_image_widget_alt_help_text_process($element, &$form_state, $form) { if (isset($element['alt'])) { - $text = 'If the image conveys information that is not part of the link text, describe it here. If the image is purely decorative, leave it blank. Screen readers will read the alt text first, then the link text.'; + $text = 'If the image conveys information that is not part of the link text, describe it here. Screen readers will read the alt text first, then the link text. If the image is purely decorative, leave it blank and check the "decorative" checkbox below.'; $element['alt']['#description'] = new TranslatableMarkup($text); } return $element; @@ -994,7 +995,7 @@ function _mass_fields_image_widget_alt_help_text_process($element, &$form_state, */ function _mass_fields_image_paragraph_widget_alt_help_text_process($element, &$form_state, $form) { if (isset($element['alt'])) { - $text = 'Alternative Text is read by screen readers and should be a concise description of the image’s purpose. If the image is purely decorative, leave this field blank.'; + $text = 'Alternative Text is read by screen readers and should be a concise description of the image’s purpose. If the image is purely decorative, leave this field blank and check the "decorative" checkbox below.'; $element['alt']['#description'] = new TranslatableMarkup($text); } return $element; @@ -1005,7 +1006,7 @@ function _mass_fields_image_paragraph_widget_alt_help_text_process($element, &$f */ function _mass_fields_event_image_widget_alt_help_text_process($element, &$form_state, $form) { if (isset($element['alt'])) { - $text = 'Enter a short description of the image to be used by screen readers ONLY if the image is not decorative. If the image is decorative (only adds visual decoration to the page, rather than to convey information that is important to understanding the page) leave this field blank.'; + $text = 'Enter a short description of the image to be used by screen readers ONLY if the image is not decorative. If the image is decorative (only adds visual decoration to the page, rather than to convey information that is important to understanding the page) leave this field blank and check the "decorative" checkbox below.'; $element['alt']['#description'] = new TranslatableMarkup($text); } return $element; diff --git a/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/ImageAltDescriptionTest.php b/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/ImageAltDescriptionTest.php index 645f85430a..afaae8d81a 100644 --- a/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/ImageAltDescriptionTest.php +++ b/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/ImageAltDescriptionTest.php @@ -207,7 +207,7 @@ public function testNewsImageAltTextDescription() { ); $this->assertNotEquals( - 'Enter a short description of the image to be used by screen readers ONLY if the image is not decorative. If the image is decorative (only adds visual decoration to the page, rather than to convey information that is important to understanding the page) leave this field blank.', + 'Enter a short description of the image to be used by screen readers ONLY if the image is not decorative. If the image is decorative (only adds visual decoration to the page, rather than to convey information that is important to understanding the page) leave this field blank and check the "decorative" checkbox below.', $news_image_alt_description, 'Alt text description for field_news_image is not overridden by event image descriptions.' ); @@ -233,7 +233,7 @@ public function testOrgPageImageAltTextDescriptions() { // Trim the HTML content to remove extra whitespace. $highlight_alt_description = trim($highlight_alt_description); $this->assertEquals( - 'If the image conveys information that is not part of the link text, describe it here. If the image is purely decorative, leave it blank. Screen readers will read the alt text first, then the link text.', + 'If the image conveys information that is not part of the link text, describe it here. Screen readers will read the alt text first, then the link text. If the image is purely decorative, leave it blank and check the "decorative" checkbox below.', $highlight_alt_description, 'Alt text description for field_featured_item_highlight is correct.' ); From 5aedaee26ef0e184858e9f1d46ff21e8ef978dd6 Mon Sep 17 00:00:00 2001 From: Joe Galluccio Date: Fri, 20 Mar 2026 12:12:36 -0400 Subject: [PATCH 06/10] fixed alt text for location banner which uses focal point --- .../custom/mass_fields/mass_fields.module | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/docroot/modules/custom/mass_fields/mass_fields.module b/docroot/modules/custom/mass_fields/mass_fields.module index 9c75fbb09c..b9d90131f4 100644 --- a/docroot/modules/custom/mass_fields/mass_fields.module +++ b/docroot/modules/custom/mass_fields/mass_fields.module @@ -950,7 +950,7 @@ function mass_fields_field_widget_single_element_image_image_form_alter(&$elemen 'field_featured_item_image' => '_mass_fields_image_widget_alt_help_text_process', 'field_event_image' => '_mass_fields_event_image_widget_alt_help_text_process', 'field_event_logo' => '_mass_fields_event_image_widget_alt_help_text_process', - 'field_bg_narrow' => '_mass_fields_event_image_widget_alt_help_text_process', + 'field_bg_narrow' => '_mass_fields_location_image_widget_alt_help_text_process', ]; // Handle fields that have a direct mapping @@ -970,6 +970,23 @@ function mass_fields_field_widget_single_element_image_image_form_alter(&$elemen } } +/** + * Implements hook_field_widget_single_element_WIDGET_TYPE_form_alter(). + * + * Handles fields using the Image (Focal Point) widget, which has a different + * widget type ID ('image_focal_point') than the standard Image widget + * ('image_image') and therefore requires its own hook implementation. + */ +function mass_fields_field_widget_single_element_image_focal_point_form_alter(&$element, FormStateInterface $form_state, $context) { + $field_process_map = [ + 'field_bg_narrow' => '_mass_fields_event_image_widget_alt_help_text_process', + ]; + + if (isset($field_process_map[$element['#field_name']])) { + $element['#process'][] = $field_process_map[$element['#field_name']]; + } +} + /** * Process callback for microsite key message image field. */ @@ -1002,11 +1019,22 @@ function _mass_fields_image_paragraph_widget_alt_help_text_process($element, &$f } /** - * Helper function to change alt text. + * Helper function to change alt text - event page. */ function _mass_fields_event_image_widget_alt_help_text_process($element, &$form_state, $form) { if (isset($element['alt'])) { - $text = 'Enter a short description of the image to be used by screen readers ONLY if the image is not decorative. If the image is decorative (only adds visual decoration to the page, rather than to convey information that is important to understanding the page) leave this field blank and check the "decorative" checkbox below.'; + $text = 'Enter a short description of the image to be used by screen readers only if the image is not decorative. If the image is decorative (only adds visual decoration to the page, rather than to convey information that is important to understanding the page) leave this field blank and check the "decorative" checkbox below.'; + $element['alt']['#description'] = new TranslatableMarkup($text); + } + return $element; +} + +/** + * Helper function to change alt text - location page banner image. + */ +function _mass_fields_location_image_widget_alt_help_text_process($element, &$form_state, $form) { + if (isset($element['alt'])) { + $text = 'Enter a short description of the image to be used by screen readers only if the image is not decorative. If the image is decorative (only adds visual decoration to the page, rather than to convey information that is important to understanding the page) leave this field blank and check the "decorative" checkbox below.'; $element['alt']['#description'] = new TranslatableMarkup($text); } return $element; From 9929071da92968be0f8896c040cdd1761467b3b9 Mon Sep 17 00:00:00 2001 From: Arthur Baghdasaryan Date: Mon, 13 Apr 2026 11:50:28 +0400 Subject: [PATCH 07/10] Default unchecked --- composer.json | 2 +- .../EntityEmbedDecorativeImageTest.php | 25 +++++++++++++++- ...dget_entity_embed_default_unchecked.patch} | 29 ++++++++----------- 3 files changed, 37 insertions(+), 19 deletions(-) rename patches/{DP-45040_decorative_image_widget_entity_embed.patch => DP-45040_decorative_image_widget_entity_embed_default_unchecked.patch} (90%) diff --git a/composer.json b/composer.json index eadacb48e9..3f6bf3fb79 100644 --- a/composer.json +++ b/composer.json @@ -439,7 +439,7 @@ "Make revisions overview page accessible (https://www.drupal.org/project/diff/issues/3228798)": "https://www.drupal.org/files/issues/2024-05-14/diff-3228798-17.patch" }, "drupal/decorative_image_widget": { - "Entity embed support (https://www.drupal.org/project/decorative_image_widget/issues/3577172)": "patches/DP-45040_decorative_image_widget_entity_embed.patch" + "Entity embed support (https://www.drupal.org/project/decorative_image_widget/issues/3577172)": "patches/DP-45040_decorative_image_widget_entity_embed_default_unchecked.patch" }, "drupal/dropzonejs": { "Dropzone element overwrites potential metadata and libraries added by other modules (https://www.drupal.org/project/drupal/issues/3303028)": "https://www.drupal.org/files/issues/2023-01-18/dropzone_overwrites_metadata_and_libraries-3303028-2_with-3211288.patch" diff --git a/docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php b/docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php index 2d5bf4d3dd..0043c91f09 100644 --- a/docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php +++ b/docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php @@ -35,7 +35,7 @@ private function buildAltElement(): array { public function testEmptyAltWithoutDecorativeFailsValidation(): void { $form_state = new FormState(); $form_state->setValue(['attributes', 'alt'], ''); - $form_state->setValue(['decorative'], FALSE); + $form_state->setValue(['attributes', 'decorative'], FALSE); $element = $this->buildAltElement(); @@ -75,4 +75,27 @@ public function testTypedAltIsClearedWhenDecorativeChecked(): void { $this->assertSame('', $form_state->getValue(['attributes', 'alt']), 'Alt is cleared when decorative is checked.'); } + /** + * Decorative defaults to unchecked for new entity embed images. + */ + public function testEntityEmbedDecorativeDefaultsUnchecked(): void { + $form_state = new FormState(); + $form = [ + 'attributes' => [ + 'alt' => [ + '#default_value' => '', + '#weight' => 0, + '#attributes' => [], + '#element_validate' => [], + ], + ], + '#attached' => [], + ]; + + DecorativeImageWidgetHelper::alterEntityEmbedDialogForm($form, $form_state); + + $this->assertArrayHasKey('decorative', $form['attributes'], 'Decorative checkbox is added to entity embed form.'); + $this->assertFalse((bool) $form['attributes']['decorative']['#default_value'], 'Decorative checkbox defaults to unchecked for new embeds.'); + } + } diff --git a/patches/DP-45040_decorative_image_widget_entity_embed.patch b/patches/DP-45040_decorative_image_widget_entity_embed_default_unchecked.patch similarity index 90% rename from patches/DP-45040_decorative_image_widget_entity_embed.patch rename to patches/DP-45040_decorative_image_widget_entity_embed_default_unchecked.patch index 4b831fd794..fd1d5a8d96 100644 --- a/patches/DP-45040_decorative_image_widget_entity_embed.patch +++ b/patches/DP-45040_decorative_image_widget_entity_embed_default_unchecked.patch @@ -1,12 +1,12 @@ diff --git a/decorative_image_widget.module b/decorative_image_widget.module -index 8b2dded80e881a7df919b7e8c93737e29b425aeb..560101e2b887a161df2fe0c58c40560d406eaf22 100644 --- a/decorative_image_widget.module +++ b/decorative_image_widget.module -@@ -51,3 +51,17 @@ function decorative_image_widget_field_widget_single_element_form_alter(array &$ +@@ -51,3 +51,18 @@ DecorativeImageWidgetHelper::alter($element, $context['widget']); } } + ++ +/** + * Implements hook_entity_embed_display_plugins_alter(). + * @@ -15,13 +15,12 @@ index 8b2dded80e881a7df919b7e8c93737e29b425aeb..560101e2b887a161df2fe0c58c40560d + */ +function decorative_image_widget_entity_embed_display_plugins_alter(array &$definitions) { + foreach ($definitions as &$definition) { -+ if (!empty($definition['class']) && $definition['class'] === 'Drupal\\entity_embed\\Plugin\\entity_embed\\EntityEmbedDisplay\\ImageFieldFormatter') { ++ if (!empty($definition['class']) && $definition['class'] === 'Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay\ImageFieldFormatter') { + $definition['class'] = \Drupal\decorative_image_widget\Plugin\entity_embed\EntityEmbedDisplay\DecorativeImageFieldFormatter::class; + } + } +} diff --git a/js/decorative-image-widget.js b/js/decorative-image-widget.js -index 55de6fd8902b83a5d986c68b89390bfb202482b8..ec7bb1133a00d740a0a9966b379878b451522ad6 100644 --- a/js/decorative-image-widget.js +++ b/js/decorative-image-widget.js @@ -43,11 +43,22 @@ @@ -51,13 +50,13 @@ index 55de6fd8902b83a5d986c68b89390bfb202482b8..ec7bb1133a00d740a0a9966b379878b4 enableOrDisableAltTextField(altTextField, !event.target.checked); }); diff --git a/src/DecorativeImageWidgetHelper.php b/src/DecorativeImageWidgetHelper.php -index 6047467fd56b55338055fa5c64bed16541f335bb..224cdb0387db576ea2c61a6c9cc41d4ae7c691ac 100644 --- a/src/DecorativeImageWidgetHelper.php +++ b/src/DecorativeImageWidgetHelper.php -@@ -191,4 +191,102 @@ class DecorativeImageWidgetHelper { +@@ -191,4 +191,102 @@ } } ++ + /** + * Alters the entity embed dialog form to add a decorative checkbox. + * @@ -78,10 +77,9 @@ index 6047467fd56b55338055fa5c64bed16541f335bb..224cdb0387db576ea2c61a6c9cc41d4a + + $alt =& $form['attributes']['alt']; + -+ // Determine if the decorative checkbox should be checked by default. When -+ // editing an existing embed, the current alt value is in the default value. -+ $alt_default = $alt['#default_value'] ?? ''; -+ $decorative_default = ($alt_default === ''); ++ // Default to unchecked so authors make an explicit decorative choice. ++ // This matches expected behavior when adding a new image in rich text. ++ $decorative_default = FALSE; + + $decorative = [ + '#type' => 'checkbox', @@ -158,11 +156,9 @@ index 6047467fd56b55338055fa5c64bed16541f335bb..224cdb0387db576ea2c61a6c9cc41d4a + } diff --git a/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php b/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php -new file mode 100644 -index 0000000000000000000000000000000000000000..9cff9e2a6be5ade0a2feec17e802152b6efec2df ---- /dev/null +--- a/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php +++ b/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php -@@ -0,0 +1,86 @@ +@@ -0,0 +1,85 @@ + $this->t('This image is decorative and should be hidden from screen readers.', [ + ':url' => 'https://www.w3.org/WAI/tutorials/images/decorative/', + ]), -+ '#default_value' => ($alt_value === ''), ++ // Default to unchecked so authors make an explicit decorative choice. ++ '#default_value' => FALSE, + '#parents' => ['decorative'], + '#weight' => $form['alt']['#weight'] ?? 0, + '#attributes' => [ @@ -248,4 +244,3 @@ index 0000000000000000000000000000000000000000..9cff9e2a6be5ade0a2feec17e802152b + } + +} -+ From f9a3d72cb58ce0aa689b67dab2fd0bd1fa5bbecd Mon Sep 17 00:00:00 2001 From: Arthur Baghdasaryan Date: Mon, 13 Apr 2026 14:44:37 +0400 Subject: [PATCH 08/10] Decorative --- .../css/components/image-widget.css | 9 ++ ...idget_entity_embed_default_unchecked.patch | 120 +++++------------- 2 files changed, 38 insertions(+), 91 deletions(-) diff --git a/docroot/themes/custom/mass_admin_theme/css/components/image-widget.css b/docroot/themes/custom/mass_admin_theme/css/components/image-widget.css index b6d83cd665..633fc6534d 100644 --- a/docroot/themes/custom/mass_admin_theme/css/components/image-widget.css +++ b/docroot/themes/custom/mass_admin_theme/css/components/image-widget.css @@ -7,3 +7,12 @@ .image-widget-data input { margin-bottom: 1.5rem; } + +.image-widget .form-type--checkbox.form-item[class*="-decorative"] { + clear: none; + margin-left: 9rem; +} + +.image-widget .form-type--checkbox.form-item[class*="-decorative"] .description { + margin-left: 0; +} diff --git a/patches/DP-45040_decorative_image_widget_entity_embed_default_unchecked.patch b/patches/DP-45040_decorative_image_widget_entity_embed_default_unchecked.patch index fd1d5a8d96..3b768c3e94 100644 --- a/patches/DP-45040_decorative_image_widget_entity_embed_default_unchecked.patch +++ b/patches/DP-45040_decorative_image_widget_entity_embed_default_unchecked.patch @@ -1,7 +1,7 @@ diff --git a/decorative_image_widget.module b/decorative_image_widget.module --- a/decorative_image_widget.module +++ b/decorative_image_widget.module -@@ -51,3 +51,18 @@ +@@ -51,3 +51,15 @@ DecorativeImageWidgetHelper::alter($element, $context['widget']); } } @@ -9,9 +9,6 @@ diff --git a/decorative_image_widget.module b/decorative_image_widget.module + +/** + * Implements hook_entity_embed_display_plugins_alter(). -+ * -+ * Swap the core image entity embed display plugin class for our decorated -+ * version that adds the Decorative checkbox and validation. + */ +function decorative_image_widget_entity_embed_display_plugins_alter(array &$definitions) { + foreach ($definitions as &$definition) { @@ -52,84 +49,56 @@ diff --git a/js/decorative-image-widget.js b/js/decorative-image-widget.js diff --git a/src/DecorativeImageWidgetHelper.php b/src/DecorativeImageWidgetHelper.php --- a/src/DecorativeImageWidgetHelper.php +++ b/src/DecorativeImageWidgetHelper.php -@@ -191,4 +191,102 @@ - } - } - +@@ -130,6 +130,7 @@ + ['@url' => 'https://www.w3.org/WAI/tutorials/images/decorative/'] + ), + '#title' => t('Decorative'), ++ '#description_display' => 'after', + '#weight' => $element['alt']['#weight'], + '#access' => $element['alt']['#access'] ?? TRUE, + '#default_value' => $decorativeDefaultValue, +@@ -187,6 +188,60 @@ + $decorative_form_element = array_merge($parents, ['decorative']); + $decorative_checked = (bool) $formState->getValue($decorative_form_element); + if ($missing_alt_text && !$decorative_checked) { ++ $formState->setErrorByName(implode('][', $element['#parents']), t('You must provide alternative text or indicate the image is decorative.')); ++ } ++ } ++ + + /** + * Alters the entity embed dialog form to add a decorative checkbox. -+ * -+ * This mirrors the UX provided for image widgets, but applied to the image -+ * embed dialog used by entity_embed. -+ * -+ * @param array $form -+ * The form array. -+ * @param \Drupal\Core\Form\FormStateInterface $formState -+ * The form state. + */ + public static function alterEntityEmbedDialogForm(array &$form, FormStateInterface $formState) { -+ // At the time this alter runs, the image entity embed display plugin's alt -+ // element has already been moved under the "attributes" container. + if (empty($form['attributes']['alt']) || !is_array($form['attributes']['alt'])) { + return; + } + + $alt =& $form['attributes']['alt']; + -+ // Default to unchecked so authors make an explicit decorative choice. -+ // This matches expected behavior when adding a new image in rich text. -+ $decorative_default = FALSE; -+ + $decorative = [ + '#type' => 'checkbox', + '#title' => t('Decorative'), -+ '#description' => t( -+ 'This image is decorative and should be hidden from screen readers.', -+ ['@url' => 'https://www.w3.org/WAI/tutorials/images/decorative/'] -+ ), -+ '#default_value' => $decorative_default, -+ '#weight' => $alt['#weight'] ?? 0, ++ '#description' => t('This image is decorative and should be hidden from screen readers.', ['@url' => 'https://www.w3.org/WAI/tutorials/images/decorative/']), ++ '#description_display' => 'after', ++ '#default_value' => FALSE, ++ '#weight' => ($alt['#weight'] ?? 0) + 1, + '#attributes' => [ + 'class' => ['decorative-checkbox'], + ], + ]; + -+ // Place the decorative checkbox inside the attributes group as a sibling of -+ // the alt field, so it appears in the same section in the dialog. + $form['attributes']['decorative'] = $decorative; -+ -+ // Add a class name to the alt textfield so JS can easily find it. + $alt['#attributes']['class'][] = 'alt-textfield'; -+ -+ // Add validation for the alt text that will require it have a value unless -+ // the decorative checkbox is checked. + $alt['#element_validate'][] = [static::class, 'validateEntityEmbedAlt']; -+ -+ // Attach the same JS library used for widgets so the decorative checkbox -+ // disables/enables the alt field. + $form['#attached']['library'][] = 'decorative_image_widget/decorative_image_widget'; + } + + /** + * Validation for the entity embed dialog alt field. -+ * -+ * Requires either alt text or the decorative checkbox to be checked. -+ * -+ * @param array $element -+ * The alt form element. -+ * @param \Drupal\Core\Form\FormStateInterface $formState -+ * The form state. + */ + public static function validateEntityEmbedAlt(array $element, FormStateInterface $formState) { -+ // In the embed dialog there is no file upload button, so we always perform -+ // validation when the form is submitted. + $alt_value = trim((string) ($formState->getValue($element['#parents']) ?? '')); -+ -+ // Normalize two double quotes ("") to an empty alt value. This mirrors -+ // the behavior provided by MediaImageDecorator::EMPTY_STRING in the -+ // entity_embed module for the core image dialog, without introducing a -+ // hard dependency on that module. + if ($alt_value === '""') { + $alt_value = ''; + $formState->setValue($element['#parents'], ''); @@ -140,25 +109,19 @@ diff --git a/src/DecorativeImageWidgetHelper.php b/src/DecorativeImageWidgetHelp + $decorative_parents[] = 'decorative'; + $decorative_checked = (bool) $formState->getValue($decorative_parents); + -+ // If the decorative checkbox is checked, always store an empty alt value, -+ // regardless of what the user typed. This ensures the underlying -+ // markup and rendered both end up with alt="" and -+ // allows us to infer "decorative" from the empty alt on subsequent edits. + if ($decorative_checked) { + $formState->setValue($element['#parents'], ''); + $alt_value = ''; + } + + if ($alt_value === '' && !$decorative_checked) { -+ $formState->setErrorByName(implode('][', $element['#parents']), t('You must provide alternative text or indicate the image is decorative.')); -+ } -+ } -+ - } + $formState->setErrorByName(implode('][', $element['#parents']), t('You must provide alternative text or indicate the image is decorative.')); + } + } diff --git a/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php b/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php --- a/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php +++ b/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php -@@ -0,0 +1,85 @@ +@@ -0,0 +1,60 @@ +t("Alternative Text is read by screen readers and should be a concise description of the image's purpose. If the image is purely decorative, leave this field blank and check the \"decorative\" checkbox below."); + $form['alt']['#attributes']['class'][] = 'alt-textfield'; + $form['alt']['#element_validate'][] = [static::class, 'validateAltOrDecorative']; + + $form['decorative'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Decorative'), -+ '#description' => $this->t('This image is decorative and should be hidden from screen readers.', [ -+ ':url' => 'https://www.w3.org/WAI/tutorials/images/decorative/', -+ ]), -+ // Default to unchecked so authors make an explicit decorative choice. ++ '#description' => $this->t('This image is decorative and should be hidden from screen readers.', [':url' => 'https://www.w3.org/WAI/tutorials/images/decorative/']), ++ '#description_display' => 'after', + '#default_value' => FALSE, + '#parents' => ['decorative'], -+ '#weight' => $form['alt']['#weight'] ?? 0, ++ '#weight' => ($form['alt']['#weight'] ?? 0) + 1, + '#attributes' => [ + 'class' => ['decorative-checkbox'], + ], + ]; + -+ // Attach JS behavior (if the decorative_image_widget library is available) -+ // so that checking the decorative box disables the alt field. + try { + $library_discovery = \Drupal::service('library.discovery'); + if ($library_discovery->getLibraryByName('decorative_image_widget', 'decorative_image_widget')) { @@ -216,28 +163,19 @@ diff --git a/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldForm + } + } + catch (\Exception $e) { -+ // If the library is not available, we still keep the validation behavior. + } + + return $form; + } + -+ /** -+ * Validation callback requiring either alt text or decorative checkbox. -+ */ + public static function validateAltOrDecorative(array $element, FormStateInterface $form_state, array &$complete_form) { + $parents = $element['#parents']; + $alt_value = trim((string) ($form_state->getValue($parents) ?? '')); -+ + $decorative_checked = (bool) $form_state->getValue(['decorative']); -+ -+ // If the decorative checkbox is checked, always store an empty alt value, -+ // regardless of what the user typed. + if ($decorative_checked) { + $form_state->setValue($parents, ''); + $alt_value = ''; + } -+ + if ($alt_value === '' && !$decorative_checked) { + $form_state->setError($element, t('You must provide alternative text or indicate the image is decorative.')); + } From 086dcc2cebccfe9dd93c812dc1e5c9deb202717f Mon Sep 17 00:00:00 2001 From: Arthur Baghdasaryan Date: Mon, 13 Apr 2026 14:48:55 +0400 Subject: [PATCH 09/10] Decorative --- composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index ba76f94206..a36faa785c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "870e508373f079ffadd6e463cf8cc5d9", + "content-hash": "c45356fbc3ac7feb45e280002e0c7bff", "packages": [ { "name": "akamai-open/edgegrid-auth", From 59b54720cff4b43cf1d7b3ba1dd7220e09901ab1 Mon Sep 17 00:00:00 2001 From: Arthur Baghdasaryan Date: Mon, 13 Apr 2026 15:07:51 +0400 Subject: [PATCH 10/10] Decorative --- .../src/ExistingSiteJavascript/ImageAltDescriptionTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/ImageAltDescriptionTest.php b/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/ImageAltDescriptionTest.php index afaae8d81a..8f8f3ca33f 100644 --- a/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/ImageAltDescriptionTest.php +++ b/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/ImageAltDescriptionTest.php @@ -242,7 +242,7 @@ public function testOrgPageImageAltTextDescriptions() { $item_alt_description = $page->find('css', '#edit-field-organization-sections-0-subform-field-section-long-form-content-0-subform-field-featured-item-mosaic-items-0-subform-field-featured-item-image-0-alt--description')->getHtml(); $item_alt_description = trim($item_alt_description); $this->assertEquals( - 'If the image conveys information that is not part of the link text, describe it here. If the image is purely decorative, leave it blank. Screen readers will read the alt text first, then the link text.', + 'If the image conveys information that is not part of the link text, describe it here. Screen readers will read the alt text first, then the link text. If the image is purely decorative, leave it blank and check the "decorative" checkbox below.', $item_alt_description, 'Alt text description for field_featured_item_image is correct.' ); @@ -277,7 +277,7 @@ public function testEventImageAltTextDescriptions() { // Trim the HTML content to remove extra whitespace. $event_image_alt_description = trim($event_image_alt_description); $this->assertEquals( - 'Enter a short description of the image to be used by screen readers ONLY if the image is not decorative. If the image is decorative (only adds visual decoration to the page, rather than to convey information that is important to understanding the page) leave this field blank.', + 'Enter a short description of the image to be used by screen readers only if the image is not decorative. If the image is decorative (only adds visual decoration to the page, rather than to convey information that is important to understanding the page) leave this field blank and check the "decorative" checkbox below.', $event_image_alt_description, 'Alt text description for field_event_image is correct.' ); @@ -286,7 +286,7 @@ public function testEventImageAltTextDescriptions() { $event_logo_alt_description = $page->find('css', '#edit-field-event-logo-0-alt--description')->getHtml(); $event_logo_alt_description = trim($event_logo_alt_description); $this->assertEquals( - 'Enter a short description of the image to be used by screen readers ONLY if the image is not decorative. If the image is decorative (only adds visual decoration to the page, rather than to convey information that is important to understanding the page) leave this field blank.', + 'Enter a short description of the image to be used by screen readers only if the image is not decorative. If the image is decorative (only adds visual decoration to the page, rather than to convey information that is important to understanding the page) leave this field blank and check the "decorative" checkbox below.', $event_logo_alt_description, 'Alt text description for field_event_logo is correct.' );