diff --git a/README.rst b/README.rst
index ff929a91..942076f9 100644
--- a/README.rst
+++ b/README.rst
@@ -136,6 +136,7 @@ For a manual install:
'djangocms_frontend.contrib.link',
'djangocms_frontend.contrib.listgroup',
'djangocms_frontend.contrib.media',
+ 'djangocms_frontend.contrib.modal',
'djangocms_frontend.contrib.tabs',
'djangocms_frontend.contrib.utilities',
diff --git a/djangocms_frontend/contrib/modal/__init__.py b/djangocms_frontend/contrib/modal/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/djangocms_frontend/contrib/modal/cms_plugins.py b/djangocms_frontend/contrib/modal/cms_plugins.py
new file mode 100644
index 00000000..a82e2652
--- /dev/null
+++ b/djangocms_frontend/contrib/modal/cms_plugins.py
@@ -0,0 +1,120 @@
+from cms.plugin_pool import plugin_pool
+from django.utils.translation import gettext_lazy as _
+
+from ... import settings
+from ...cms_plugins import CMSUIPlugin
+from ...common import AttributesMixin
+from .. import modal
+from . import forms, models
+
+mixin_factory = settings.get_renderer(modal)
+
+
+@plugin_pool.register_plugin
+class ModalPlugin(mixin_factory("Modal"), AttributesMixin, CMSUIPlugin):
+ """
+ Component > "Modal" Plugin
+ https://getbootstrap.com/docs/5.0/components/modal/
+ """
+
+ name = _("Modal")
+ module = _("Frontend")
+ model = models.Modal
+ form = forms.ModalForm
+ change_form_template = "djangocms_frontend/admin/modal.html"
+ allow_children = True
+ child_classes = [
+ "ModalTriggerPlugin",
+ "ModalContainerPlugin",
+ ]
+
+ fieldsets = [
+ (None, {"fields": ("modal_siblings",)}),
+ ]
+
+
+@plugin_pool.register_plugin
+class ModalTriggerPlugin(mixin_factory("ModalTrigger"), AttributesMixin, CMSUIPlugin):
+ """
+ Component > "Modal" Plugin
+ https://getbootstrap.com/docs/5.0/components/modal/
+ """
+
+ name = _("Modal trigger")
+ module = _("Frontend")
+ model = models.ModalTrigger
+ form = forms.ModalTriggerForm
+ allow_children = True
+ parent_classes = [
+ "ModalPlugin"
+ ]
+
+ fieldsets = [
+ (None, {"fields": ("trigger_identifier",)}),
+ ]
+
+
+@plugin_pool.register_plugin
+class ModalContainerPlugin(mixin_factory("ModalContainer"), CMSUIPlugin):
+ """
+ Component > "Modal Container" Plugin
+ https://getbootstrap.com/docs/5.0/components/modal/
+ """
+
+ name = _("Modal container")
+ module = _("Frontend")
+ model = models.ModalContainer
+ form = forms.ModalContainerForm
+ allow_children = True
+ parent_classes = [
+ "ModalPlugin"
+ ]
+ child_classes = [
+ "ModalInnerPlugin",
+
+ ]
+ fieldsets = [
+ (
+ None,
+ {
+ "fields": (
+ "container_identifier",
+ ("modal_centered"),
+ ("modal_static", "modal_scrollable"),
+ ("modal_size", "modal_fullscreen"),
+ ("modal_footer"),
+ )
+ }
+ ),
+ ]
+
+
+@plugin_pool.register_plugin
+class ModalInnerPlugin(
+ mixin_factory("ModalInner"),
+ CMSUIPlugin,
+):
+ """
+ Component > "Modal Container Content" Plugin
+ https://getbootstrap.com/docs/5.0/components/modal/
+ """
+ name = _("Modal inner")
+ module = _("Frontend")
+ model = models.ModalInner
+ form = forms.ModalInnerForm
+ allow_children = True
+ parent_classes = [
+ "ModalContainerPlugin",
+ ]
+
+ fieldsets = [
+ (
+ None,
+ {
+ "fields": (
+ "inner_type",
+ "attributes",
+ )
+ },
+ ),
+ ]
diff --git a/djangocms_frontend/contrib/modal/constants.py b/djangocms_frontend/contrib/modal/constants.py
new file mode 100644
index 00000000..601eccd9
--- /dev/null
+++ b/djangocms_frontend/contrib/modal/constants.py
@@ -0,0 +1,26 @@
+from django.utils.translation import gettext_lazy as _
+
+MODAL_CENTERED_CHOICES = (
+ ("modal-dialog-centered", _("Vertically centered")),
+ ("modal-dialog-centered modal-dialog-scrollable", _("Vertically centered scrollable")),
+)
+
+MODAL_SIZE_CHOICES = (
+ ("modal-sm", _("Small")),
+ ("modal-lg", _("Large")),
+ ("modal-xl", _("Extra Large")),
+)
+
+MODAL_FULLSCREEN_CHOICES = (
+ ("modal-fullscreen", _("Allways")),
+ ("modal-fullscreen-sm-down", _("Fullscreen below sm")),
+ ("modal-fullscreen-md-down", _("Fullscreen below md")),
+ ("modal-fullscreen-lg-down", _("Fullscreen below lg")),
+ ("modal-fullscreen-xl-down", _("Fullscreen below xl")),
+ ("modal-fullscreen-xxl-down", _("Fullscreen below xxl")),
+)
+
+MODAL_INNER_TYPE_CHOICES = (
+ ("modal-body", _("Body")),
+ ("modal-header", _("Header")),
+)
diff --git a/djangocms_frontend/contrib/modal/forms.py b/djangocms_frontend/contrib/modal/forms.py
new file mode 100644
index 00000000..bb7b01af
--- /dev/null
+++ b/djangocms_frontend/contrib/modal/forms.py
@@ -0,0 +1,174 @@
+from django import forms
+from django.utils.translation import gettext_lazy as _
+from entangled.forms import EntangledModelForm
+
+from ... import settings
+from ...fields import AttributesFormField, ButtonGroup, TagTypeFormField
+from ...helpers import first_choice
+from ...models import FrontendUIItem
+from .. import modal
+from .constants import (
+ MODAL_CENTERED_CHOICES,
+ MODAL_FULLSCREEN_CHOICES,
+ MODAL_INNER_TYPE_CHOICES,
+ MODAL_SIZE_CHOICES,
+)
+
+# TODO leaving this comment for now
+# data-bs-toggle="modal" data-bs-target="#modalExample"
+# aria-expanded="false" aria-controls="modalExample">
+# data-bs-target can also be classes
+# data-bs-parent links to the wrapper modal
+#
+
+mixin_factory = settings.get_forms(modal)
+
+
+class ModalForm(mixin_factory("Modal"), EntangledModelForm):
+ """
+ Component > "Modal" Plugin
+ https://getbootstrap.com/docs/5.0/components/modal/
+ """
+
+ class Meta:
+ model = FrontendUIItem
+ entangled_fields = {
+ "config": [
+ "modal_siblings",
+ "attributes",
+ ]
+ }
+ untangled_fields = ("tag_type",)
+
+ modal_siblings = forms.CharField(
+ label=_("Siblings"),
+ initial=".card",
+ required=False,
+ help_text=_("Element to be used to create a modal."),
+ )
+ attributes = AttributesFormField()
+ tag_type = TagTypeFormField()
+
+
+class ModalTriggerForm(mixin_factory("ModalTrigger"), EntangledModelForm):
+ """
+ Component > "Modal Trigger" Plugin
+ https://getbootstrap.com/docs/5.0/components/modal/
+ """
+
+ class Meta:
+ model = FrontendUIItem
+ entangled_fields = {
+ "config": [
+ "trigger_identifier",
+ "attributes",
+ ]
+ }
+ untangled_fields = ("tag_type",)
+
+ trigger_identifier = forms.SlugField(
+ label=_("Unique identifier"),
+ required=True,
+ help_text=_("Identifier to connect trigger with container."),
+ )
+ attributes = AttributesFormField()
+ tag_type = TagTypeFormField()
+
+
+class ModalContainerForm(mixin_factory("ModalContainer"), EntangledModelForm):
+ """
+ Component > "Modal Container" Plugin
+ https://getbootstrap.com/docs/5.0/components/modal/
+ """
+
+ class Meta:
+ model = FrontendUIItem
+ entangled_fields = {
+ "config": [
+ "container_identifier",
+ "attributes",
+ "modal_centered",
+ "modal_static",
+ "modal_footer",
+ "modal_scrollable",
+ "modal_size",
+ "modal_fullscreen",
+ ]
+ }
+ untangled_fields = ("tag_type",)
+
+ container_identifier = forms.SlugField(
+ label=_("Unique identifier"),
+ required=True,
+ help_text=_("Identifier to connect trigger with container."),
+ )
+
+ modal_centered = forms.ChoiceField(
+ label=_("Centered"),
+ choices=settings.EMPTY_CHOICE + MODAL_CENTERED_CHOICES,
+ required=False,
+ )
+
+ modal_static = forms.BooleanField(
+ label=_("Static backdrop"),
+ required=False,
+ help_text=_("If selected, the modal will not close when clicking outside of it."),
+ )
+
+ modal_footer = forms.BooleanField(
+ label=_("Footer and close button"),
+ required=False,
+ help_text=_("If selected, the modal will show a modal footer with a close button."),
+ )
+
+ modal_scrollable = forms.BooleanField(
+ label=_("Scrollable"),
+ required=False,
+ help_text=_("Enable scrolling in the container."),
+ )
+
+ modal_size = forms.ChoiceField(
+ label=_("Size"),
+ choices=settings.EMPTY_CHOICE + MODAL_SIZE_CHOICES,
+ required=False,
+ )
+
+ modal_fullscreen = forms.ChoiceField(
+ label=_("Fullscreen"),
+ choices=settings.EMPTY_CHOICE + MODAL_FULLSCREEN_CHOICES,
+ required=False,
+ )
+
+ attributes = AttributesFormField()
+ tag_type = TagTypeFormField()
+
+
+class ModalInnerForm(mixin_factory("ModalInner"), EntangledModelForm):
+ """
+ Component > "Modal Container Content" Plugin
+ https://getbootstrap.com/docs/5.0/components/modal/
+ """
+
+ class Meta:
+ model = FrontendUIItem
+ entangled_fields = {
+ "config": [
+ "inner_type",
+ ]
+ }
+ untangled_fields = ("tag_type",)
+
+ inner_type = forms.ChoiceField(
+ label=_("Type"),
+ choices=settings.EMPTY_CHOICE + MODAL_INNER_TYPE_CHOICES,
+ required=True,
+ )
+
+ inner_type = forms.ChoiceField(
+ label=_("Inner type"),
+ choices=MODAL_INNER_TYPE_CHOICES,
+ initial=first_choice(MODAL_INNER_TYPE_CHOICES),
+ help_text=_("Define the structure of the plugin."),
+ widget=ButtonGroup(attrs=dict(label_class="btn-secondary")),
+ )
+ attributes = AttributesFormField()
diff --git a/djangocms_frontend/contrib/modal/frameworks/__init__.py b/djangocms_frontend/contrib/modal/frameworks/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/djangocms_frontend/contrib/modal/frameworks/bootstrap5.py b/djangocms_frontend/contrib/modal/frameworks/bootstrap5.py
new file mode 100644
index 00000000..10aa0891
--- /dev/null
+++ b/djangocms_frontend/contrib/modal/frameworks/bootstrap5.py
@@ -0,0 +1,21 @@
+class ModalRenderMixin:
+ render_template = "djangocms_frontend/bootstrap5/modal.html"
+
+
+class ModalContainerRenderMixin:
+ render_template = "djangocms_frontend/bootstrap5/modal-container.html"
+
+ def render(self, context, instance, placeholder):
+ return super().render(context, instance, placeholder)
+
+
+class ModalInnerRenderMixin:
+ render_template = "djangocms_frontend/bootstrap5/modal-inner.html"
+
+ def render(self, context, instance, placeholder):
+ instance.add_classes(instance.inner_type)
+ return super().render(context, instance, placeholder)
+
+
+class ModalTriggerRenderMixin:
+ render_template = "djangocms_frontend/bootstrap5/modal-trigger.html"
diff --git a/djangocms_frontend/contrib/modal/migrations/0001_initial.py b/djangocms_frontend/contrib/modal/migrations/0001_initial.py
new file mode 100644
index 00000000..891086c9
--- /dev/null
+++ b/djangocms_frontend/contrib/modal/migrations/0001_initial.py
@@ -0,0 +1,47 @@
+# Generated by Django 3.2.11 on 2022-01-12 21:03
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ initial = True
+
+ dependencies = [
+ ("djangocms_frontend", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="Collapse",
+ fields=[],
+ options={
+ "proxy": True,
+ "indexes": [],
+ "constraints": [],
+ "verbose_name": "Collapse",
+ },
+ bases=("djangocms_frontend.frontenduiitem",),
+ ),
+ migrations.CreateModel(
+ name="CollapseContainer",
+ fields=[],
+ options={
+ "proxy": True,
+ "indexes": [],
+ "constraints": [],
+ "verbose_name": "Collapse container",
+ },
+ bases=("djangocms_frontend.frontenduiitem",),
+ ),
+ migrations.CreateModel(
+ name="CollapseTrigger",
+ fields=[],
+ options={
+ "proxy": True,
+ "indexes": [],
+ "constraints": [],
+ "verbose_name": "Collapse trigger",
+ },
+ bases=("djangocms_frontend.frontenduiitem",),
+ ),
+ ]
diff --git a/djangocms_frontend/contrib/modal/migrations/__init__.py b/djangocms_frontend/contrib/modal/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/djangocms_frontend/contrib/modal/models.py b/djangocms_frontend/contrib/modal/models.py
new file mode 100644
index 00000000..7f3ac7a0
--- /dev/null
+++ b/djangocms_frontend/contrib/modal/models.py
@@ -0,0 +1,66 @@
+from django.utils.translation import gettext_lazy as _
+
+from djangocms_frontend.models import FrontendUIItem
+
+# TODO leaving this comment for now
+# data-bs-toggle="modal" data-bs-target="#modalExample"
+# aria-expanded="false" aria-controls="modalExample">
+# data-bs-target can also be classes
+# data-bs-parent links to the wrapper modal
+#
+
+
+class Modal(FrontendUIItem):
+ """
+ Component > "Modal" Plugin
+ https://getbootstrap.com/docs/5.0/components/modal/
+ """
+
+ class Meta:
+ proxy = True
+ verbose_name = _("Modal")
+
+ def get_short_description(self):
+ return f"(modal-{str(self.pk)})"
+
+
+class ModalTrigger(FrontendUIItem):
+ """
+ Component > "Modal Trigger" Plugin
+ https://getbootstrap.com/docs/5.0/components/modal/
+ """
+
+ class Meta:
+ proxy = True
+ verbose_name = _("Modal trigger")
+
+ def get_short_description(self):
+ return f"({self.trigger_identifier})"
+
+
+class ModalContainer(FrontendUIItem):
+ """
+ Component > "Modal Container" Plugin
+ https://getbootstrap.com/docs/5.0/components/modal/
+ """
+
+ class Meta:
+ proxy = True
+ verbose_name = _("Modal container")
+
+ def get_short_description(self):
+ return f"({self.container_identifier})"
+
+
+class ModalInner(FrontendUIItem):
+ """
+ Component > "Modal Inner" Plugin
+ https://getbootstrap.com/docs/5.0/components/modal/
+ """
+
+ class Meta:
+ proxy = True
+ verbose_name = _("Modal inner")
+
+ def get_short_description(self):
+ return f"({self.inner_type})"
diff --git a/djangocms_frontend/contrib/modal/templates/djangocms_frontend/admin/modal.html b/djangocms_frontend/contrib/modal/templates/djangocms_frontend/admin/modal.html
new file mode 100644
index 00000000..600927d1
--- /dev/null
+++ b/djangocms_frontend/contrib/modal/templates/djangocms_frontend/admin/modal.html
@@ -0,0 +1,9 @@
+{% extends "djangocms_frontend/admin/base.html" %}
+{% load i18n %}
+
+{% block custom_class %}djangocms-bootstrap5-modal{% endblock %}
+{% block custom_message %}
+
+ {% trans "There is no configuration required here." %}
+
+{% endblock %}
diff --git a/djangocms_frontend/contrib/modal/templates/djangocms_frontend/bootstrap5/modal-container.html b/djangocms_frontend/contrib/modal/templates/djangocms_frontend/bootstrap5/modal-container.html
new file mode 100644
index 00000000..bd54e211
--- /dev/null
+++ b/djangocms_frontend/contrib/modal/templates/djangocms_frontend/bootstrap5/modal-container.html
@@ -0,0 +1,18 @@
+{% load cms_tags frontend %}
+ <{{ instance.tag_type }} {{ instance.get_attributes }}>
+
+
+ {{ instance.tag_type }}>
\ No newline at end of file
diff --git a/djangocms_frontend/contrib/modal/templates/djangocms_frontend/bootstrap5/modal-inner.html b/djangocms_frontend/contrib/modal/templates/djangocms_frontend/bootstrap5/modal-inner.html
new file mode 100644
index 00000000..2aed8dec
--- /dev/null
+++ b/djangocms_frontend/contrib/modal/templates/djangocms_frontend/bootstrap5/modal-inner.html
@@ -0,0 +1,11 @@
+{% load cms_tags frontend %}
+<{{ instance.tag_type }}{{ instance.get_attributes }}>
+{% with parent=instance %}
+ {% for plugin in instance.child_plugin_instances %}
+ {% with forloop as parentloop %}{% render_plugin plugin %}{% endwith %}
+ {% endfor %}
+{% endwith %}
+{% if instance.inner_type == "modal-header" %}
+
+ {% endif %}
+{{ instance.tag_type }}>
\ No newline at end of file
diff --git a/djangocms_frontend/contrib/modal/templates/djangocms_frontend/bootstrap5/modal-trigger.html b/djangocms_frontend/contrib/modal/templates/djangocms_frontend/bootstrap5/modal-trigger.html
new file mode 100644
index 00000000..62bcdd09
--- /dev/null
+++ b/djangocms_frontend/contrib/modal/templates/djangocms_frontend/bootstrap5/modal-trigger.html
@@ -0,0 +1,12 @@
+{% load cms_tags frontend %}
+<{{ instance.tag_type }}{{ instance.get_attributes }}
+ id="trigger-{{ instance.trigger_identifier }}"
+ role="tab"
+ data-bs-toggle="modal"
+ data-bs-target="#{{ instance.trigger_identifier }}"
+ aria-controls="{{ instance.trigger_identifier }}"
+ aria-expanded="false">
+ {% for plugin in instance.child_plugin_instances %}
+ {% with forloop as parentloop %}{% render_plugin plugin %}{% endwith %}
+ {% endfor %}
+{{ instance.tag_type }}>
diff --git a/djangocms_frontend/contrib/modal/templates/djangocms_frontend/bootstrap5/modal.html b/djangocms_frontend/contrib/modal/templates/djangocms_frontend/bootstrap5/modal.html
new file mode 100644
index 00000000..5bd8f17c
--- /dev/null
+++ b/djangocms_frontend/contrib/modal/templates/djangocms_frontend/bootstrap5/modal.html
@@ -0,0 +1,12 @@
+{% load cms_tags frontend %}
+<{{ instance.tag_type }}{{ instance.get_attributes }}
+id="parent-{{ instance.pk|safe }}"
+data-bs-children="{{ instance.modal_siblings }}"
+role="tablist"
+>
+{% with parent=instance %}
+ {% for plugin in instance.child_plugin_instances %}
+ {% with forloop as parentloop %}{% render_plugin plugin %}{% endwith %}
+ {% endfor %}
+{% endwith %}
+{{ instance.tag_type }}>
diff --git a/docs/source/components.rst b/docs/source/components.rst
index d3fdf149..64e5c07b 100644
--- a/docs/source/components.rst
+++ b/docs/source/components.rst
@@ -424,6 +424,31 @@ Media component
The media component is another legacy component from djangocms-bootstrap4.
**djangocms-frontend** recreates it using responsive utilities.
+***************
+Modal component
+***************
+
+The modal component shows content inside an modal.
+
+Also see Bootstrap 5 `Modal
`_
+documentation.
+
+Modal consist of an Modal Plugin which uses an Modal Trigger Plugin and an Modal Container Plugin.
+Inside the Modal Container add an Modal Inner Plugin and select the inner type "header" or "body".
+
+.. image:: screenshots/modal-example.png
+ :width: 394
+
+The unique identifier of the Modal Trigger Plugin and Modal Container Plugin have to be identical.
+
+The layout of the modal can be modified in the Modal Container plugin.
+
+.. image:: screenshots/modal-settings.png
+
+.. note::
+
+ Restriction:
+ The footer is not editable and shows only a close button.
.. index::
single: Picture
diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst
index 183e0ff6..33bf55c5 100644
--- a/docs/source/getting_started.rst
+++ b/docs/source/getting_started.rst
@@ -60,6 +60,7 @@ Add the following entries to your ``INSTALLED_APPS``:
"djangocms_frontend.contrib.link",
"djangocms_frontend.contrib.listgroup",
"djangocms_frontend.contrib.media",
+ 'djangocms_frontend.contrib.modal',
"djangocms_frontend.contrib.tabs",
"djangocms_frontend.contrib.utilities",
diff --git a/docs/source/screenshots/modal-example.png b/docs/source/screenshots/modal-example.png
new file mode 100644
index 00000000..d6d2bfe8
Binary files /dev/null and b/docs/source/screenshots/modal-example.png differ
diff --git a/docs/source/screenshots/modal-settings.png b/docs/source/screenshots/modal-settings.png
new file mode 100644
index 00000000..a3ea3746
Binary files /dev/null and b/docs/source/screenshots/modal-settings.png differ
diff --git a/tests/modal/__init__.py b/tests/modal/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/modal/test_models.py b/tests/modal/test_models.py
new file mode 100644
index 00000000..96a46275
--- /dev/null
+++ b/tests/modal/test_models.py
@@ -0,0 +1,31 @@
+from django.test import TestCase
+
+from djangocms_frontend.contrib.modal.models import Modal, ModalTrigger, ModalContainer, ModalInner
+
+
+class ModalModelTestCase(TestCase):
+ def test_Modal_instance(self):
+ instance = Modal.objects.create()
+ self.assertEqual(str(instance), "Modal (1)")
+ self.assertEqual(instance.get_short_description(), "(modal-1)")
+
+ def test_modal_trigger_instance(self):
+ instance = ModalTrigger.objects.create(
+ config={"trigger_identifier": "Monty Python"}
+ )
+ self.assertEqual(str(instance), "ModalTrigger (1)")
+ self.assertEqual(instance.get_short_description(), "(Monty Python)")
+
+ def test_modal_container_instance(self):
+ instance = ModalContainer.objects.create(
+ config={"container_identifier": "Monty Python"}
+ )
+ self.assertEqual(str(instance), "ModalContainer (1)")
+ self.assertEqual(instance.get_short_description(), "(Monty Python)")
+
+ def test_modal_inner_instance(self):
+ instance = ModalInner.objects.create(
+ config={"container_identifier": "Monty Python"}
+ )
+ self.assertEqual(str(instance), "ModalInner (1)")
+ self.assertEqual(instance.get_short_description(), "(Monty Python)")
diff --git a/tests/modal/test_plugins.py b/tests/modal/test_plugins.py
new file mode 100644
index 00000000..2bd98cd7
--- /dev/null
+++ b/tests/modal/test_plugins.py
@@ -0,0 +1,76 @@
+from cms.api import add_plugin
+from cms.test_utils.testcases import CMSTestCase
+
+from djangocms_frontend.contrib.modal.cms_plugins import (
+ ModalContainerPlugin,
+ ModalPlugin,
+ ModalTriggerPlugin,
+)
+from djangocms_frontend.contrib.modal.forms import (
+ ModalContainerForm,
+ ModalForm,
+ ModalTriggerForm,
+)
+
+from ..fixtures import TestFixture
+
+
+class ModalPluginTestCase(TestFixture, CMSTestCase):
+ def test_modal_plugin(self):
+ plugin = add_plugin(
+ placeholder=self.placeholder,
+ plugin_type=ModalPlugin.__name__,
+ language=self.language,
+ )
+ plugin.initialize_from_form(ModalForm).save()
+ self.publish(self.page, self.language)
+
+ with self.login_user_context(self.superuser):
+ response = self.client.get(self.request_url)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'data-bs-children=".modal"')
+
+ def test_modal_trigger_plugin(self):
+ plugin = add_plugin(
+ placeholder=self.placeholder,
+ plugin_type=ModalTriggerPlugin.__name__,
+ language=self.language,
+ config=dict(
+ trigger_identifier=10,
+ ),
+ )
+ plugin.initialize_from_form(ModalTriggerForm).save()
+ self.publish(self.page, self.language)
+
+ with self.login_user_context(self.superuser):
+ response = self.client.get(self.request_url)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'aria-controls="10"')
+ self.assertContains(response, 'data-bs-target="#10"')
+ self.assertContains(response, 'id="trigger-10"')
+
+ def test_modal_container_plugin(self):
+ parent = add_plugin(
+ placeholder=self.placeholder,
+ plugin_type=ModalPlugin.__name__,
+ language=self.language,
+ )
+ parent.initialize_from_form(ModalForm).save()
+
+ plugin = add_plugin(
+ target=parent,
+ placeholder=self.placeholder,
+ plugin_type=ModalContainerPlugin.__name__,
+ language=self.language,
+ config=dict(
+ container_identifier=10,
+ ),
+ )
+ plugin.initialize_from_form(ModalContainerForm).save()
+ self.publish(self.page, self.language)
+
+ with self.login_user_context(self.superuser):
+ response = self.client.get(self.request_url)
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'aria-labelledby="trigger-10"')
+ self.assertContains(response, "10")