diff --git a/resources/js/copy_clipboard.js b/resources/js/copy_clipboard.js
new file mode 100644
index 000000000..2a75f7144
--- /dev/null
+++ b/resources/js/copy_clipboard.js
@@ -0,0 +1,32 @@
+/**
+ * @file
+ * Attaches behaviors to copy text to clipboard for elements with data-copy-target attribute.
+ */
+(function (Drupal) {
+
+ const handleClick = function (event) {
+ const element = event.currentTarget;
+ const targetSelector = element.getAttribute('data-copy-target');
+ const targetElement = document.querySelector(targetSelector);
+
+ if (targetElement) {
+ const copyText = targetElement.innerText || targetElement.value;
+ navigator.clipboard.writeText(copyText);
+ }
+ };
+
+ Drupal.behaviors.copyClipboard = {
+ attach: function (context) {
+ once('oebt-clipcopy', '[data-copy-target]', context).forEach(function (element) {
+ element.addEventListener('click', handleClick);
+ });
+ },
+
+ detach: function (context, settings, trigger) {
+ once.remove('oebt-clipcopy', '[data-copy-target]', context).forEach(function (element) {
+ element.removeEventListener('click', handleClick);
+ });
+ }
+ };
+
+})(Drupal);
diff --git a/resources/sass/copyright_overlay.scss b/resources/sass/copyright_overlay.scss
new file mode 100644
index 000000000..3c03a34e3
--- /dev/null
+++ b/resources/sass/copyright_overlay.scss
@@ -0,0 +1,20 @@
+.copyright-overlay {
+ background-color: rgba(var(--bs-white-rgb),var(--bs-bg-opacity));
+ color: var(--bs-gray-700);
+ padding: 0 0.5rem;
+ display: inline-flex;
+ .copyright-preview {
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 7rem;
+ font-size: 0.875rem;
+ }
+ .copyright-trigger {
+ font-size: 0.875rem;
+ }
+ .copyright-header {
+ font-size: 1.25rem;
+ }
+}
diff --git a/runner.yml.dist b/runner.yml.dist
index a38b056db..e6c93b650 100644
--- a/runner.yml.dist
+++ b/runner.yml.dist
@@ -30,6 +30,11 @@ drupal:
- "vendor"
- "${drupal.root}"
+selenium:
+ host: "http://selenium"
+ port: "4444"
+ browser: "chrome"
+
commands:
drupal:site-setup:
- { task: "run", command: "drupal:symlink-project" }
diff --git a/templates/patterns/copyright_overlay/copyright_overlay.ui_patterns.yml b/templates/patterns/copyright_overlay/copyright_overlay.ui_patterns.yml
new file mode 100644
index 000000000..c0ff1a00e
--- /dev/null
+++ b/templates/patterns/copyright_overlay/copyright_overlay.ui_patterns.yml
@@ -0,0 +1,30 @@
+copyright_overlay:
+ label: 'Copyright overlay'
+ description: 'A component to show copyright information.'
+ fields:
+ trigger_label:
+ type: text
+ label: 'Trigger label'
+ description: 'The label of the modal trigger. Optional.'
+ preview: 'View more'
+ modal_title:
+ type: text
+ label: 'Modal title'
+ description: 'The title of the modal header. Optional.'
+ preview: 'Copyright'
+ content:
+ type: render
+ label: 'Content'
+ description: 'Copyright overlay content.'
+ preview: '© Copyright ipsum amet John Doe on Doe Images.'
+ settings:
+ trigger_attributes:
+ type: attributes
+ label: Trigger attributes
+ description: Extra attributes for the modal trigger
+ allow_token: true
+ attributes:
+ type: attributes
+ label: Copyright overlay attributes
+ description: Extra attributes for the copyright overlay
+ allow_token: true
diff --git a/templates/patterns/copyright_overlay/pattern-copyright-overlay--preview.html.twig b/templates/patterns/copyright_overlay/pattern-copyright-overlay--preview.html.twig
new file mode 100644
index 000000000..e5f63d69b
--- /dev/null
+++ b/templates/patterns/copyright_overlay/pattern-copyright-overlay--preview.html.twig
@@ -0,0 +1,14 @@
+{#
+/**
+ * @file
+ * Template override for preview mode of "Copyright overlay" pattern.
+ */
+#}
+
+{{ pattern('copyright_overlay', {
+ content: content,
+}) }}
+
+{{ pattern('copyright_overlay', {
+ content: '© Second copyright element. Suspendisse vel mauris vitae ipsum blandit condimentum ut eget quam.',
+}) }}
diff --git a/templates/patterns/copyright_overlay/pattern-copyright-overlay.html.twig b/templates/patterns/copyright_overlay/pattern-copyright-overlay.html.twig
new file mode 100644
index 000000000..36f1b35d9
--- /dev/null
+++ b/templates/patterns/copyright_overlay/pattern-copyright-overlay.html.twig
@@ -0,0 +1,51 @@
+{#
+/**
+ * @file
+ * Copyright overlay pattern.
+ */
+#}
+{{ attach_library('oe_bootstrap_theme/pattern.copyright_overlay') }}
+
+{% set _footer %}
+ {% include '@oe-bcl/button' with {
+ label: 'Copy Copyright'|t,
+ attributes: create_attribute({
+ 'data-bs-dismiss': 'modal',
+ 'data-copy-target': '#' ~ copy_selector
+ })
+ } only %}
+{% endset %}
+
+{% set _content %}
+
+ {{ content }}
+
+{% endset %}
+
+
+
+ {{ content }}
+
+
+ {{ pattern('link', {
+ label: trigger_label|default('View more'|t),
+ path: '#',
+ attributes: trigger_attributes
+ .addClass('copyright-trigger')
+ .setAttribute('data-bs-toggle', 'modal')
+ .setAttribute('data-bs-target', '#' ~ modal_id),
+ }) }}
+
+ {{ pattern('modal', {
+ 'title': modal_title|default('Copyright'|t),
+ 'show_close_button': true,
+ 'body': _content,
+ 'footer': _footer,
+ 'attributes': create_attribute({
+ id: modal_id
+ })
+ }) }}
+
diff --git a/tests/src/Functional/CheckboxSwitchWidgetTest.php b/tests/src/Functional/CheckboxSwitchWidgetTest.php
index f953de74b..afe9cc597 100644
--- a/tests/src/Functional/CheckboxSwitchWidgetTest.php
+++ b/tests/src/Functional/CheckboxSwitchWidgetTest.php
@@ -4,10 +4,10 @@
namespace Drupal\Tests\oe_bootstrap_theme\Functional;
+use Drupal\Tests\BrowserTestBase;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
-use Drupal\Tests\BrowserTestBase;
/**
* Tests the checkbox switch third party settings on compatible widgets.
diff --git a/tests/src/Functional/ThemeSettingsTest.php b/tests/src/Functional/ThemeSettingsTest.php
index 27d8e8123..2946f92e9 100644
--- a/tests/src/Functional/ThemeSettingsTest.php
+++ b/tests/src/Functional/ThemeSettingsTest.php
@@ -4,8 +4,8 @@
namespace Drupal\Tests\oe_bootstrap_theme\Functional;
-use Drupal\oe_bootstrap_theme\BackwardCompatibility;
use Drupal\Tests\BrowserTestBase;
+use Drupal\oe_bootstrap_theme\BackwardCompatibility;
/**
* Tests the theme settings.
diff --git a/tests/src/FunctionalJavascript/CopyClipboardTest.php b/tests/src/FunctionalJavascript/CopyClipboardTest.php
new file mode 100644
index 000000000..4a12dab7e
--- /dev/null
+++ b/tests/src/FunctionalJavascript/CopyClipboardTest.php
@@ -0,0 +1,77 @@
+drupalLogin($this->drupalCreateUser([], NULL, TRUE));
+
+ $this->drupalGet('/patterns/copyright_overlay');
+
+ // Mock the clipboard API.
+ $this->getSession()->executeScript(<<getSession()->getPage()->findAll('css', '.copyright-overlay');
+
+ $this->assertCopyrightCopiedToClipboard($elements['0'], '© Copyright ipsum amet John Doe on Doe Images.');
+ $this->assertCopyrightCopiedToClipboard($elements['1'], '© Second copyright element. Suspendisse vel mauris vitae ipsum blandit condimentum ut eget quam.');
+ }
+
+ /**
+ * Asserts that the text in the element corresponds to the clipboard.
+ *
+ * @param \Behat\Mink\Element\NodeElement $element
+ * The copyright element.
+ * @param string $expected
+ * The expected result.
+ */
+ protected function assertCopyrightCopiedToClipboard(NodeElement $element, string $expected): void {
+ $assert_session = $this->assertSession();
+
+ $assert_session->elementExists('css', '.copyright-trigger', $element)->click();
+ $modal = $assert_session->waitForElementVisible('css', '.modal.show');
+
+ $assert_session->elementExists('css', '[data-copy-target]', $modal)->click();
+
+ $actual = $this->getSession()->evaluateScript('return window.copiedText;');
+
+ $this->assertEquals($expected, $actual);
+ }
+
+}
diff --git a/tests/src/Kernel/MarkupRenderingTest.php b/tests/src/Kernel/MarkupRenderingTest.php
index f49e5356a..42c74cebf 100644
--- a/tests/src/Kernel/MarkupRenderingTest.php
+++ b/tests/src/Kernel/MarkupRenderingTest.php
@@ -59,6 +59,7 @@ class MarkupRenderingTest extends KernelTestBase implements FormInterface {
'card_layout',
'carousel',
'columns',
+ 'copyright_overlay',
'content_banner',
'date_block',
'description_list',
diff --git a/tests/src/Kernel/ValueObject/ImageValueObjectTest.php b/tests/src/Kernel/ValueObject/ImageValueObjectTest.php
index a322b0cae..3869801e6 100644
--- a/tests/src/Kernel/ValueObject/ImageValueObjectTest.php
+++ b/tests/src/Kernel/ValueObject/ImageValueObjectTest.php
@@ -4,13 +4,13 @@
namespace Drupal\Tests\oe_bootstrap_theme\Kernel\ValueObject;
+use Drupal\Tests\oe_bootstrap_theme\Kernel\AbstractKernelTestBase;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\file\Entity\File;
use Drupal\image\Entity\ImageStyle;
use Drupal\oe_bootstrap_theme\ValueObject\ImageValueObject;
-use Drupal\Tests\oe_bootstrap_theme\Kernel\AbstractKernelTestBase;
/**
* Test image value object with image field type. Extracted from oe_theme.
diff --git a/tests/src/Kernel/fixtures/markup_rendering_patterns/copyright_overlay.yml b/tests/src/Kernel/fixtures/markup_rendering_patterns/copyright_overlay.yml
new file mode 100644
index 000000000..7dc48da1d
--- /dev/null
+++ b/tests/src/Kernel/fixtures/markup_rendering_patterns/copyright_overlay.yml
@@ -0,0 +1,78 @@
+copyright_overlay:
+ render:
+ '#type': pattern
+ '#id': copyright_overlay
+ '#fields':
+ content: 'Default ipsum dolor sit amet.'
+ assert:
+ contains:
+ '.modal-header h5': 'Copyright'
+ 'span.copyright-preview': 'Default ipsum dolor sit amet'
+ 'div.copyright-content': 'Default ipsum dolor sit amet'
+ '.copyright-trigger[data-bs-target="#bcl-copyright-modal"]': 'View more'
+ count:
+ '.copyright-trigger[data-bs-target="#bcl-copyright-modal"]': 1
+ 'div#bcl-copyright-modal': 1
+ '.copyright-overlay': 1
+ 'button[data-copy-target="#copyright-content"]': 1
+copyright_overlay_with_custom_trigger_label:
+ render:
+ '#type': pattern
+ '#id': copyright_overlay
+ '#fields':
+ content: 'Lorem Ipsum dolor sit amet'
+ trigger_label: 'Show more'
+ assert:
+ contains:
+ '.modal-header h5': 'Copyright'
+ 'span.copyright-preview': 'Lorem Ipsum dolor sit amet'
+ 'div.copyright-content': 'Lorem Ipsum dolor sit amet'
+ '.copyright-trigger[data-bs-target="#bcl-copyright-modal"]': 'Show more'
+ count:
+ '.copyright-trigger[data-bs-target="#bcl-copyright-modal"]': 1
+ 'div#bcl-copyright-modal': 1
+ '.copyright-overlay': 1
+ 'button[data-copy-target="#copyright-content"]': 1
+copyright_overlay_with_custom_modal_title:
+ render:
+ '#type': pattern
+ '#id': copyright_overlay
+ '#fields':
+ content: 'Aenean eget accumsan neque, eu accumsan eros.'
+ modal_title: 'New title'
+ assert:
+ contains:
+ '.modal-header h5': 'New title'
+ 'span.copyright-preview': 'Aenean eget accumsan neque, eu accumsan eros.'
+ 'div.copyright-content': 'Aenean eget accumsan neque, eu accumsan eros.'
+ '.copyright-trigger[data-bs-target="#bcl-copyright-modal"]': 'View more'
+ count:
+ '.copyright-trigger[data-bs-target="#bcl-copyright-modal"]': 1
+ 'div#bcl-copyright-modal': 1
+ '.copyright-overlay': 1
+ 'button[data-copy-target="#copyright-content"]': 1
+copyright_overlay_with_attributes:
+ render:
+ '#type': pattern
+ '#id': copyright_overlay
+ '#fields':
+ content: 'Pellentesque ullamcorper erat sed arcu dapibus.'
+ '#settings':
+ attributes: 'class="test-class" id="test-id" data-test="attr-value"'
+ trigger_attributes: 'class="my-trigger-class" data-test="trigger-attr-value"'
+ assert:
+ contains:
+ '.modal-header h5': 'Copyright'
+ 'span.copyright-preview': 'Pellentesque ullamcorper erat sed arcu dapibus.'
+ 'div.copyright-content': 'Pellentesque ullamcorper erat sed arcu dapibus.'
+ '.copyright-trigger[data-bs-target="#bcl-copyright-modal"]': 'View more'
+ count:
+ '.copyright-trigger[data-bs-target="#bcl-copyright-modal"]': 1
+ 'div#bcl-copyright-modal': 1
+ '.copyright-overlay': 1
+ 'button[data-copy-target="#copyright-content"]': 1
+ '.copyright-overlay.test-class': 1
+ '.copyright-overlay#test-id': 1
+ '.copyright-overlay[data-test="attr-value"]': 1
+ '.copyright-trigger.my-trigger-class': 1
+ '.copyright-trigger[data-test="trigger-attr-value"]': 1