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 diff --git a/composer.json b/composer.json index 8eb55fc173..3f6bf3fb79 100644 --- a/composer.json +++ b/composer.json @@ -188,6 +188,7 @@ "drupal/csv_field": "^3.0", "drupal/csv_serialization": "^4.0", "drupal/datalayer": "^2", + "drupal/decorative_image_widget": "^1.0", "drupal/devel": "^5", "drupal/diff": "^1.7", "drupal/draggableviews": "^2.1", @@ -437,6 +438,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_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/composer.lock b/composer.lock index e49be31450..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": "647812e9d2be3f7a8cc0e8b848f2dd8d", + "content-hash": "c45356fbc3ac7feb45e280002e0c7bff", "packages": [ { "name": "akamai-open/edgegrid-auth", @@ -4611,6 +4611,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", @@ -12022,12 +12071,12 @@ "version": "v7.0.2", "source": { "type": "git", - "url": "https://github.com/firebase/php-jwt.git", + "url": "https://github.com/googleapis/php-jwt.git", "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65", "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65", "shasum": "" }, 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/core.entity_form_display.node.news.default.yml b/conf/drupal/config/core.entity_form_display.node.news.default.yml index ee753da202..02bcd25720 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 @@ -34,6 +34,7 @@ dependencies: - allowed_formats - content_moderation - datetime + - decorative_image_widget - entity_hierarchy - entity_reference_tree - field_group @@ -304,7 +305,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/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 ea3738276b..035d62cd82 100644 --- a/conf/drupal/config/core.extension.yml +++ b/conf/drupal/config/core.extension.yml @@ -39,6 +39,7 @@ module: datalayer: 0 datetime: 0 datetime_range: 0 + decorative_image_widget: 0 diff: 0 draggableviews: 0 dropzonejs: 0 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 498b7818d0..50bf190d75 100644 --- a/docroot/modules/custom/mass_fields/mass_fields.module +++ b/docroot/modules/custom/mass_fields/mass_fields.module @@ -976,6 +976,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_location_image_widget_alt_help_text_process', ]; // Handle fields that have a direct mapping @@ -995,6 +996,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. */ @@ -1009,7 +1027,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; @@ -1020,18 +1038,29 @@ 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; } /** - * 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.'; + $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; 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..0043c91f09 --- /dev/null +++ b/docroot/modules/custom/mass_fields/tests/src/ExistingSite/EntityEmbedDecorativeImageTest.php @@ -0,0 +1,101 @@ + ['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(['attributes', '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.'); + } + + /** + * 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/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..83407de5eb --- /dev/null +++ b/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/DecorativeImageWidgetTest.php @@ -0,0 +1,72 @@ +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/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/ImageAltDescriptionTest.php b/docroot/modules/custom/mass_fields/tests/src/ExistingSiteJavascript/ImageAltDescriptionTest.php index 645f85430a..8f8f3ca33f 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.' ); @@ -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.' ); 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 new file mode 100644 index 0000000000..3b768c3e94 --- /dev/null +++ b/patches/DP-45040_decorative_image_widget_entity_embed_default_unchecked.patch @@ -0,0 +1,184 @@ +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,15 @@ + DecorativeImageWidgetHelper::alter($element, $context['widget']); + } + } ++ ++ ++/** ++ * Implements hook_entity_embed_display_plugins_alter(). ++ */ ++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 +--- 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 +--- a/src/DecorativeImageWidgetHelper.php ++++ b/src/DecorativeImageWidgetHelper.php +@@ -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. ++ */ ++ public static function alterEntityEmbedDialogForm(array &$form, FormStateInterface $formState) { ++ if (empty($form['attributes']['alt']) || !is_array($form['attributes']['alt'])) { ++ return; ++ } ++ ++ $alt =& $form['attributes']['alt']; ++ ++ $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/']), ++ '#description_display' => 'after', ++ '#default_value' => FALSE, ++ '#weight' => ($alt['#weight'] ?? 0) + 1, ++ '#attributes' => [ ++ 'class' => ['decorative-checkbox'], ++ ], ++ ]; ++ ++ $form['attributes']['decorative'] = $decorative; ++ $alt['#attributes']['class'][] = 'alt-textfield'; ++ $alt['#element_validate'][] = [static::class, 'validateEntityEmbedAlt']; ++ $form['#attached']['library'][] = 'decorative_image_widget/decorative_image_widget'; ++ } ++ ++ /** ++ * Validation for the entity embed dialog alt field. ++ */ ++ public static function validateEntityEmbedAlt(array $element, FormStateInterface $formState) { ++ $alt_value = trim((string) ($formState->getValue($element['#parents']) ?? '')); ++ 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 ($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 +--- a/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php ++++ b/src/Plugin/entity_embed/EntityEmbedDisplay/DecorativeImageFieldFormatter.php +@@ -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/']), ++ '#description_display' => 'after', ++ '#default_value' => FALSE, ++ '#parents' => ['decorative'], ++ '#weight' => ($form['alt']['#weight'] ?? 0) + 1, ++ '#attributes' => [ ++ 'class' => ['decorative-checkbox'], ++ ], ++ ]; ++ ++ 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) { ++ } ++ ++ return $form; ++ } ++ ++ 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 ($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.')); ++ } ++ } ++ ++}