<div class="rtds-dialog">
    <div role="dialog" id="dialog1" aria-labelledby="dialog1Label" aria-modal="true" class="rtds-hidden">

        <div class="rtds-dialog__header">
            <h2 id="dialog1Label" class="rtds-dialog__title rtds-heading-3">

                <svg class="rtds-icon rtds-fill-current rtds-w-6 rtds-h-6 md:rtds-w-8 md:rtds-h-8 rtds-dialog__left-icon" aria-hidden="true" focusable="false" role="img">
                    <use href="../../icons.svg#outline--information-circle" />
                </svg>

                Titolo modale
            </h2>

            <button type="button" class="rtds-btn 
    rtds-btn--icon rtds-btn--l rtds-dialog__close-button" id="close-dialog-button" onclick="closeDialog(this)"><svg class="rtds-icon rtds-fill-current rtds-w-6 rtds-h-6 md:rtds-w-8 md:rtds-h-8" aria-hidden="true" focusable="false" role="img">
                    <use href="../../icons.svg#mini--x-mark" />
                </svg>
                <span class="rtds-sr-only">

                    Chiudi la modale corrente
                </span>

            </button>

        </div>

        <div class="rtds-dialog__content" tabindex="0">

            Qui error necessitatibus nam rerum corporis et culpa iste eos excepturi ipsam vel sequi voluptatem ad distinctio accusantium sed galisum voluptas. Quo voluptatem dicta non totam possimus est voluptatem magnam. Et exercitationem molestiae ut veniam consectetur rem maxime odit.

        </div>

        <div class="rtds-dialog__actions">

            <button type="button" class="rtds-btn 
    rtds-btn--primary
     rtds-btn--outlined" onclick="closeDialog(this)">

                Annulla

            </button>

            <button type="button" class="rtds-btn 
    rtds-btn--primary
     rtds-btn--primary">

                Salva

            </button>

        </div>

    </div>
</div>
<div class="rtds-dialog{% block classes %}{% if classes %} {{ classes }}{% endif %}{% endblock classes %}">
    <div role="dialog" id="{{ dialogId }}" aria-labelledby="{{ dialogId }}Label" aria-modal="true" class="rtds-hidden">
        {% block contentBefore %}
        {% endblock contentBefore %}
        <div class="rtds-dialog__header">
            <h2 id="{{ dialogId }}Label" class="rtds-dialog__title{% if titleClasses %} {{ titleClasses }}{% endif %}">
                {% if leftIcon %}
                    {% render '@icon', {id: leftIcon, classes:'rtds-dialog__left-icon', size:'rtds-w-6 rtds-h-6 md:rtds-w-8 md:rtds-h-8'}, true %}
                {% endif %}
                {{ label }}
            </h2>

            {% block closeButton %}
            {% if closeButton %}
                {% render '@button' + closeButton.variant, closeButton.context, true %}
            {% endif %}
            {% endblock closeButton %}
        </div>
        
        
        {% block content %}
        <div class="rtds-dialog__content" tabindex="0">
            {% block contentInner %}
                {{ content|safe }}
            {% endblock contentInner %}
        </div>
        {% endblock content %}

    {% block actions %}
        {% if hasActions %}
        <div class="rtds-dialog__actions">
            {% block cta %}
                {% for button in buttons %}
                    {% if button.type == "button-icon-left" %}
                        {% render "@button--icon-left", button, true %}
                    {% elseif button.type == "button-icon-right" %}
                        {% render "@button--icon-right", button, true %}
                    {% else %}
                        {% render "@button", button, true %}
                    {% endif %}
                {% endfor %}
            {% endblock cta %}
        </div>
        {% endif %}
    {% endblock actions %}
    {% block contentAfter %}
    {% endblock contentAfter %}
    </div>
</div>
{
  "dialogId": "dialog1",
  "label": "Titolo modale",
  "content": "Qui error necessitatibus nam rerum corporis et culpa iste eos excepturi ipsam vel sequi voluptatem ad distinctio accusantium sed galisum voluptas. Quo voluptatem dicta non totam possimus est voluptatem magnam. Et exercitationem molestiae ut veniam consectetur rem maxime odit.",
  "titleClasses": "rtds-heading-3",
  "hasActions": true,
  "buttons": [
    {
      "label": "Annulla",
      "callback": "closeDialog(this)",
      "classes": "rtds-btn--outlined"
    },
    {
      "label": "Salva",
      "classes": "rtds-btn--primary"
    }
  ],
  "closeButton": {
    "variant": "--icon",
    "context": {
      "label": "Chiudi la modale corrente",
      "icon": "mini--x-mark",
      "id": "close-dialog-button",
      "classes": "rtds-btn--l rtds-dialog__close-button",
      "iconSize": "rtds-w-6 rtds-h-6 md:rtds-w-8 md:rtds-h-8",
      "callback": "closeDialog(this)"
    }
  }
}
  • Content:
    /**
     * MODAL DIALOG
     *
    */
    :where(.rtds-dialog) [role="dialog"] {
        min-height: 100vh;
        flex-direction: column;
        --tw-bg-opacity: 1;
        background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
        display: flex;
    }
    @media (min-width: 768px) {
    
        body:where(.rtds-dialog) [role="dialog"] .rtds-primary-navigation__backdrop,:where(.rtds-dialog) [role="dialog"]
        :root .rtds-primary-navigation__backdrop {
            position: fixed;
            right: var(--spacing-0);
            bottom: var(--spacing-0);
            left: var(--spacing-0);
            z-index: 10;
        }
    
        @media (min-width: 1024px) {
            body:where(.rtds-dialog) [role="dialog"] .rtds-primary-navigation__backdrop,:where(.rtds-dialog) [role="dialog"]
        :root .rtds-primary-navigation__backdrop {
                display: none;
            }
        }
    
        body:where(.rtds-dialog) [role="dialog"] .rtds-primary-navigation__backdrop,:where(.rtds-dialog) [role="dialog"]
        :root .rtds-primary-navigation__backdrop {
            position: fixed;
            right: var(--spacing-0);
            bottom: var(--spacing-0);
            left: var(--spacing-0);
            z-index: 10;
        }
    
        @media (min-width: 1024px) {
            body:where(.rtds-dialog) [role="dialog"] .rtds-primary-navigation__backdrop,:where(.rtds-dialog) [role="dialog"]
        :root .rtds-primary-navigation__backdrop {
                display: none;
            }
        }
    
        :where(.rtds-dialog) [role="dialog"] {
            position: absolute;
            left: 50vw;
            top: 50vh;
            max-height: 90vh;
            min-height: 13rem;
            --tw-translate-x: -50%;
            --tw-translate-y: -50%;
            transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
            overflow: hidden;
            border-radius: 0.375rem;
            --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
            --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
            box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
        }
    }
    :where(.rtds-dialog-backdrop.is-active) [role="dialog"] {
        display: flex;
    }
    /* HEADER */
    .rtds-dialog__header {
        justify-content: space-between;
        border-bottom-width: 1px;
        --tw-bg-opacity: 1;
        background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
            border-color: var(--color-border-gray-01);
        display: flex;
        align-items: center;
        gap: var(--spacing-2);
        padding-left: var(--spacing-4);
        padding-right: var(--spacing-4);
        padding-top: var(--spacing-3);
        padding-bottom: var(--spacing-3);
    }
    @media (min-width: 1024px) {
        .rtds-dialog__header {
            padding-left: var(--spacing-6);
            padding-right: var(--spacing-6);
            padding-top: var(--spacing-4);
            padding-bottom: var(--spacing-4);
        }
    }
    :where(.rtds-modal-dialog--inverse) .rtds-dialog__header {
            color: var(--color-content-inverse);
            background-color: var(--color-background-primary);
        }
    @media (min-width: 1024px) {
        :where(.rtds-modal-dialog--s) .rtds-dialog__header {
            padding-left: var(--spacing-4);
            padding-right: var(--spacing-4);
            padding-top: var(--spacing-2);
            padding-bottom: var(--spacing-2);
        }
    }
    .rtds-dialog__content {
        flex: 1 1 0%;
        overflow-y: auto;
        --tw-bg-opacity: 1;
        background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
    }
    .rtds-dialog__content:focus-visible {
            box-shadow: inset 0 0 0 3px var(--tw-ring-color);
        }
    .rtds-dialog__content,
        .rtds-dialog__actions {
        padding-left: var(--spacing-4);
        padding-right: var(--spacing-4);
        padding-top: var(--spacing-3);
        padding-bottom: var(--spacing-3);
    }
    @media (min-width: 1024px) {
        .rtds-dialog__content,
        .rtds-dialog__actions {
            padding-left: var(--spacing-6);
            padding-right: var(--spacing-6);
            padding-top: var(--spacing-4);
            padding-bottom: var(--spacing-4);
        }
    }
    .rtds-dialog__actions {
        bottom: var(--spacing-0);
        border-top-width: 1px;
        --tw-bg-opacity: 1;
        background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
        padding-top: var(--spacing-4);
            border-color: var(--color-border-gray-01);
        display: flex;
        gap: var(--spacing-2);
    }
    @media (min-width: 640px) {
        @media (min-height: 480px) {
            .rtds-dialog__actions {
                position: sticky;
            }
        }
    }
    @media (min-width: 1024px) {
        :where(.rtds-modal-dialog--s) .rtds-dialog__actions {
            padding: var(--spacing-4);
        }
    }
    .rtds-dialog__actions .rtds-btn {
        flex: 1 1 0%;
    }
    @media (min-width: 1024px) {
        .rtds-dialog__actions .rtds-btn {
            flex: none;
        }
    }
    .rtds-dialog__close-button {
        border-color: transparent;
        background-color: transparent;
    }
    .rtds-dialog__close-button:focus {
        border-color: transparent;
        background-color: transparent;
    }
    @media screen and (min-width: 768px) {
            :where(.rtds-dialog) [role="dialog"] {
                min-width: calc(768px - (15px * 2));
            /* == breakpoint - left+right margin */
            }
        }
    /* dialog::backdrop, */
    .rtds-dialog-backdrop {
        position: fixed;
        top: var(--spacing-0);
        right: var(--spacing-0);
        bottom: var(--spacing-0);
        left: var(--spacing-0);
        z-index: 40;
        display: none;
        height: 100%;
        width: 100%;
        overflow-y: auto;
        background-color: rgb(0 0 0 / 0.25);
    }
    .rtds-dialog-backdrop.is-active {
        display: block;
    }
    .no-scroll {
            overflow-y: auto !important;
        }
    /* this is added to the body when a dialog is open */
    .has-dialog {
            overflow: hidden !important;
        }
    
  • URL: /components/raw/modal-dialog/modal-dialog.built.css
  • Filesystem Path: components/04-organisms/modal-dialog/modal-dialog.built.css
  • Size: 5.8 KB
  • Content:
    /**
     * MODAL DIALOG
     *
    */
    @layer components {
        :where(.rtds-dialog) [role="dialog"] {
            @apply rtds-flex rtds-flex-col rtds-bg-white md:rtds-rounded-md md:rtds-overflow-hidden rtds-min-h-[100vh] md:rtds-min-h-[13rem] md:rtds-max-h-[90vh] md:rtds-absolute md:rtds-shadow-lg md:rtds-left-[50vw] md:rtds-top-[50vh] md:-rtds-translate-x-[50%] md:-rtds-translate-y-[50%];
        }   
    
        :where(.rtds-dialog-backdrop.is-active) [role="dialog"] {
            @apply rtds-flex;
        }
    
        /* HEADER */
        .rtds-dialog__header {
            @apply rtds-flex rtds-justify-between rtds-px-4 rtds-py-3 lg:rtds-px-6 lg:rtds-py-4 rtds-gap-2 rtds-items-center rtds-border-b rtds-border-gray-01 rtds-bg-white;
        }
    
        :where(.rtds-modal-dialog--inverse) .rtds-dialog__header  {
            @apply rtds-background-primary rtds-content-inverse;
        }
    
        :where(.rtds-modal-dialog--s) .rtds-dialog__header  {
            @apply lg:rtds-px-4 lg:rtds-py-2;
        }
    
        .rtds-dialog__content {
            @apply rtds-bg-white rtds-flex-1 rtds-overflow-y-auto;        
        }
    
    
        .rtds-dialog__content:focus-visible {
            box-shadow: inset 0 0 0 3px var(--tw-ring-color);
        }
    
        .rtds-dialog__content,
        .rtds-dialog__actions {
            @apply rtds-px-4 rtds-py-3 lg:rtds-px-6 lg:rtds-py-4;
        }
    
        .rtds-dialog__actions {
            @apply rtds-pt-4 rtds-flex rtds-gap-2 sm:min-h-480:rtds-sticky rtds-bottom-0 rtds-border-t rtds-border-gray-01 rtds-bg-white;
        }
    
        :where(.rtds-modal-dialog--s) .rtds-dialog__actions {
            @apply lg:rtds-p-4 ;
        }
    
        .rtds-dialog__actions .rtds-btn {
            @apply rtds-flex-1 lg:rtds-flex-none;
        }
    
        .rtds-dialog__close-button {
            @apply rtds-bg-transparent rtds-border-transparent focus:rtds-bg-transparent focus:rtds-border-transparent;
        }
    
    
        @media screen and (min-width: 768px) {
            :where(.rtds-dialog) [role="dialog"] {
                min-width: calc(768px - (15px * 2));
            /* == breakpoint - left+right margin */
            }
        }
    
        /* dialog::backdrop, */
        .rtds-dialog-backdrop {
            @apply rtds-hidden rtds-fixed rtds-overflow-y-auto rtds-w-full rtds-h-full rtds-top-0 rtds-right-0 rtds-bottom-0 rtds-left-0 rtds-z-40 rtds-bg-black/25
        }
    
        .rtds-dialog-backdrop.is-active {
            @apply rtds-block;
        }
    
        .no-scroll {
            overflow-y: auto !important;
        }
    
        /* this is added to the body when a dialog is open */
        .has-dialog {
            overflow: hidden !important;
        }
    
    }
  • URL: /components/raw/modal-dialog/modal-dialog.css
  • Filesystem Path: components/04-organisms/modal-dialog/modal-dialog.css
  • Size: 2.5 KB
  • Content:
    /*
     *   This content is licensed according to the W3C Software License at
     *   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
     */
    
    'use strict';
    
    var aria = aria || {};
    
    aria.Utils = aria.Utils || {};
    
    (function () {
      /*
       * When util functions move focus around, set this true so the focus listener
       * can ignore the events.
       */
      aria.Utils.IgnoreUtilFocusChanges = false;
    
      aria.Utils.dialogOpenClass = 'has-dialog';
    
      /**
       * @description Set focus on descendant nodes until the first focusable element is
       *       found.
       * @param element
       *          DOM node for which to find the first focusable descendant.
       * @returns {boolean}
       *  true if a focusable element is found and focus is set.
       */
      aria.Utils.focusFirstDescendant = function (element) {
        for (var i = 0; i < element.childNodes.length; i++) {
          var child = element.childNodes[i];
          if (
            aria.Utils.attemptFocus(child) ||
            aria.Utils.focusFirstDescendant(child)
          ) {
            return true;
          }
        }
        return false;
      }; // end focusFirstDescendant
    
      /**
       * @description Find the last descendant node that is focusable.
       * @param element
       *          DOM node for which to find the last focusable descendant.
       * @returns {boolean}
       *  true if a focusable element is found and focus is set.
       */
      aria.Utils.focusLastDescendant = function (element) {
        for (var i = element.childNodes.length - 1; i >= 0; i--) {
          var child = element.childNodes[i];
          if (
            aria.Utils.attemptFocus(child) ||
            aria.Utils.focusLastDescendant(child)
          ) {
            return true;
          }
        }
        return false;
      }; // end focusLastDescendant
    
      /**
       * @description Set Attempt to set focus on the current node.
       * @param element
       *          The node to attempt to focus on.
       * @returns {boolean}
       *  true if element is focused.
       */
      aria.Utils.attemptFocus = function (element) {
        if (!aria.Utils.isFocusable(element)) {
          return false;
        }
    
        aria.Utils.IgnoreUtilFocusChanges = true;
        try {
          element.focus();
        } catch (e) {
          // continue regardless of error
        }
        aria.Utils.IgnoreUtilFocusChanges = false;
        return document.activeElement === element;
      }; // end attemptFocus
    
      /* Modals can open modals. Keep track of them with this array. */
      aria.OpenDialogList = aria.OpenDialogList || new Array(0);
    
      /**
       * @returns {object} the last opened dialog (the current dialog)
       */
      aria.getCurrentDialog = function () {
        if (aria.OpenDialogList && aria.OpenDialogList.length) {
          return aria.OpenDialogList[aria.OpenDialogList.length - 1];
        }
      };
    
      aria.closeCurrentDialog = function () {
        var currentDialog = aria.getCurrentDialog();
        if (currentDialog) {
          currentDialog.close(true);
          return true;
        }
        return false;
      };
    
      aria.handleEscape = function (event) {
        var key = event.which || event.keyCode;
    
        if (key === aria.KeyCode.ESC) {
          // Se il focus è all'interno di un dropdown dopo la chiusura della modale
          const activeElement = document.activeElement;
          if (activeElement && activeElement.closest('.rtds-dropdown-menu__list')) {
            const dropdownMenu = activeElement.closest('.rtds-dropdown-menu');
            if (dropdownMenu) {
              const dropdownList = dropdownMenu.querySelector('.rtds-dropdown-menu__list');
              const dropdownTrigger = dropdownMenu.querySelector('.rtds-dropdown-trigger');
              if (dropdownList && dropdownTrigger) {
                dropdownList.classList.add('rtds-hidden');
                dropdownTrigger.setAttribute('aria-expanded', 'false');
                dropdownTrigger.focus();
                event.stopPropagation();
                return;
              }
            }
          }
          
          // Gestione normale della chiusura della modale
          if (aria.closeCurrentDialog()) {
            event.stopPropagation();
          }
        }
      };
    
      document.addEventListener('keyup', aria.handleEscape);
    
      /**
       * @class
       * @description Dialog object providing modal focus management.
       *
       * Assumptions: The element serving as the dialog container is present in the
       * DOM and hidden. The dialog container has role='dialog'.
       * @param dialogId
       *          The ID of the element serving as the dialog container.
       * @param focusAfterClosed
       *          Either the DOM node or the ID of the DOM node to focus when the
       *          dialog closes.
       * @param focusFirst
       *          Optional parameter containing either the DOM node or the ID of the
       *          DOM node to focus when the dialog opens. If not specified, the
       *          first focusable element in the dialog will receive focus.
       */
      aria.Dialog = function (dialogId, focusAfterClosed, focusFirst) {
        this.dialogNode = document.getElementById(dialogId);
        if (this.dialogNode === null) {
          throw new Error('No element found with id="' + dialogId + '".');
        }
    
        var validRoles = ['dialog', 'alertdialog'];
        var isDialog = (this.dialogNode.getAttribute('role') || '')
          .trim()
          .split(/\s+/g)
          .some(function (token) {
            return validRoles.some(function (role) {
              return token === role;
            });
          });
        if (!isDialog) {
          throw new Error(
            'Dialog() requires a DOM element with ARIA role of dialog or alertdialog.'
          );
        }
    
        // Wrap in an individual backdrop element if one doesn't exist
        // Native <dialog> elements use the ::backdrop pseudo-element, which
        // works similarly.
        var backdropClass = 'rtds-dialog-backdrop';
        if (this.dialogNode.parentNode.classList.contains(backdropClass)) {
          this.backdropNode = this.dialogNode.parentNode;
        } else {
          this.backdropNode = document.createElement('div');
          this.backdropNode.className = backdropClass;
          this.dialogNode.parentNode.insertBefore(
            this.backdropNode,
            this.dialogNode
          );
          this.backdropNode.appendChild(this.dialogNode);
        }
        this.backdropNode.classList.add('is-active');
    
        // Disable scroll on the body element
        document.body.classList.add(aria.Utils.dialogOpenClass);
    
        if (typeof focusAfterClosed === 'string') {
          this.focusAfterClosed = document.getElementById(focusAfterClosed);
        } else if (typeof focusAfterClosed === 'object') {
          this.focusAfterClosed = focusAfterClosed;
        } else {
          throw new Error(
            'the focusAfterClosed parameter is required for the aria.Dialog constructor.'
          );
        }
    
        if (typeof focusFirst === 'string') {
          this.focusFirst = document.getElementById(focusFirst);
        } else if (typeof focusFirst === 'object') {
          this.focusFirst = focusFirst;
        } else {
          this.focusFirst = null;
        }
    
        // Bracket the dialog node with two invisible, focusable nodes.
        // While this dialog is open, we use these to make sure that focus never
        // leaves the document even if dialogNode is the first or last node.
        var preDiv = document.createElement('div');
        this.preNode = this.dialogNode.parentNode.insertBefore(
          preDiv,
          this.dialogNode
        );
        this.preNode.tabIndex = 0;
        var postDiv = document.createElement('div');
        this.postNode = this.dialogNode.parentNode.insertBefore(
          postDiv,
          this.dialogNode.nextSibling
        );
        this.postNode.tabIndex = 0;
    
        // If this modal is opening on top of one that is already open,
        // get rid of the document focus listener of the open dialog.
        if (aria.OpenDialogList.length > 0) {
          aria.getCurrentDialog().removeListeners();
        }
    
        this.addListeners();
        aria.OpenDialogList.push(this);
        this.dialogNode.className = 'default-dialog'; // make visible
    
        if (this.focusAfterClosed && this.focusAfterClosed.hasAttribute('aria-expanded')) {
          this.focusAfterClosed.setAttribute('aria-expanded', 'true');
        }
    
        if (this.focusFirst) {
          this.focusFirst.focus();
        } else {
          aria.Utils.focusFirstDescendant(this.dialogNode);
        }
    
        this.lastFocus = document.activeElement;
      }; // end Dialog constructor
    
      //aria.Dialog.prototype.clearDialog = function () {
        // Array.prototype.map.call(
        //   this.dialogNode.querySelectorAll('input'),
        //   function (input) {
        //     input.value = '';
        //   }
        // );
      //};
    
      /**
       * @description
       *  Hides the current top dialog,
       *  removes listeners of the top dialog,
       *  restore listeners of a parent dialog if one was open under the one that just closed,
       *  and sets focus on the element specified for focusAfterClosed.
       */
      aria.Dialog.prototype.close = function (isKeyboardClose) {
        aria.OpenDialogList.pop();
        this.removeListeners();
        aria.Utils.remove(this.preNode);
        aria.Utils.remove(this.postNode);
        this.dialogNode.className = 'rtds-hidden';
        this.backdropNode.classList.remove('is-active');
    
        // Gestione speciale per trigger all'interno di dropdown solo se chiuso da tastiera
        if (isKeyboardClose && this.focusAfterClosed && this.focusAfterClosed.closest('.rtds-dropdown-menu__list')) {
          const dropdownMenu = this.focusAfterClosed.closest('.rtds-dropdown-menu');
          if (dropdownMenu) {
            const dropdownList = dropdownMenu.querySelector('.rtds-dropdown-menu__list');
            if (dropdownList) {
              dropdownList.classList.remove('rtds-hidden');
              // Gestione dell'attributo aria-expanded del trigger
              const dropdownTrigger = dropdownMenu.querySelector('.rtds-dropdown-trigger');
              if (dropdownTrigger) {
                dropdownTrigger.setAttribute('aria-expanded', 'true');
              }
            }
          }
        } else if (this.focusAfterClosed && this.focusAfterClosed.closest('.rtds-dropdown-menu__list')) {
          // Se chiuso da mouse, chiudi il dropdown
          const dropdownMenu = this.focusAfterClosed.closest('.rtds-dropdown-menu');
          if (dropdownMenu) {
            const dropdownList = dropdownMenu.querySelector('.rtds-dropdown-menu__list');
            const dropdownTrigger = dropdownMenu.querySelector('.rtds-dropdown-trigger');
            if (dropdownList && dropdownTrigger) {
              dropdownList.classList.add('rtds-hidden');
              dropdownTrigger.setAttribute('aria-expanded', 'false');
            }
          }
        }
    
        this.focusAfterClosed.focus();
    
        if (this.focusAfterClosed.hasAttribute('aria-expanded')) {
          this.focusAfterClosed.setAttribute('aria-expanded', 'false');
        }
    
        // If a dialog was open underneath this one, restore its listeners.
        if (aria.OpenDialogList.length > 0) {
          aria.getCurrentDialog().addListeners();
        } else {
          document.body.classList.remove(aria.Utils.dialogOpenClass);
        }
      }; // end close
    
      /**
       * @description
       *  Hides the current dialog and replaces it with another.
       * @param newDialogId
       *  ID of the dialog that will replace the currently open top dialog.
       * @param newFocusAfterClosed
       *  Optional ID or DOM node specifying where to place focus when the new dialog closes.
       *  If not specified, focus will be placed on the element specified by the dialog being replaced.
       * @param newFocusFirst
       *  Optional ID or DOM node specifying where to place focus in the new dialog when it opens.
       *  If not specified, the first focusable element will receive focus.
       */
      aria.Dialog.prototype.replace = function (
        newDialogId,
        newFocusAfterClosed,
        newFocusFirst
      ) {
        aria.OpenDialogList.pop();
        this.removeListeners();
        aria.Utils.remove(this.preNode);
        aria.Utils.remove(this.postNode);
        this.dialogNode.className = 'rtds-hidden';
        this.backdropNode.classList.remove('is-active');
    
        var focusAfterClosed = newFocusAfterClosed || this.focusAfterClosed;
        new aria.Dialog(newDialogId, focusAfterClosed, newFocusFirst);
      }; // end replace
    
      aria.Dialog.prototype.addListeners = function () {
        document.addEventListener('focus', this.trapFocus, true);
      }; // end addListeners
    
      aria.Dialog.prototype.removeListeners = function () {
        document.removeEventListener('focus', this.trapFocus, true);
      }; // end removeListeners
    
      aria.Dialog.prototype.trapFocus = function (event) {
        if (aria.Utils.IgnoreUtilFocusChanges) {
          return;
        }
        var currentDialog = aria.getCurrentDialog();
        if (currentDialog.dialogNode.contains(event.target)) {
          currentDialog.lastFocus = event.target;
        } else {
          aria.Utils.focusFirstDescendant(currentDialog.dialogNode);
          if (currentDialog.lastFocus == document.activeElement) {
            aria.Utils.focusLastDescendant(currentDialog.dialogNode);
          }
          currentDialog.lastFocus = document.activeElement;
        }
      }; // end trapFocus
    
      window.openDialog = function (dialogId, focusAfterClosed, focusFirst) {
        new aria.Dialog(dialogId, focusAfterClosed, focusFirst);
      };
    
      window.closeDialog = function (closeButton) {
        var topDialog = aria.getCurrentDialog();
        if (topDialog && topDialog.dialogNode.contains(closeButton)) {
          topDialog.close(false);
        }
      }; // end closeDialog
    
      window.replaceDialog = function (
        newDialogId,
        newFocusAfterClosed,
        newFocusFirst
      ) {
        var topDialog = aria.getCurrentDialog();
        if (topDialog.dialogNode.contains(document.activeElement)) {
          topDialog.replace(newDialogId, newFocusAfterClosed, newFocusFirst);
        }
      }; // end replaceDialog
    
      // Gestione automatica dei modali con classe rtds-dialog--visible
      document.addEventListener('DOMContentLoaded', function() {
        var visibleDialogs = document.querySelectorAll('.rtds-dialog--visible');
        visibleDialogs.forEach(function(dialog) {
          var dialogElement = dialog.querySelector('[role="dialog"]');
          if (dialogElement) {
            openDialog(dialogElement.id, document.body);
          }
        });
      });
    })();
    'use strict';
    /**
     * @namespace aria
     */
    
    var aria = aria || {};
    
    /**
     * @description
     *  Key code constants
     */
    aria.KeyCode = {
      BACKSPACE: 8,
      TAB: 9,
      RETURN: 13,
      SHIFT: 16,
      ESC: 27,
      SPACE: 32,
      PAGE_UP: 33,
      PAGE_DOWN: 34,
      END: 35,
      HOME: 36,
      LEFT: 37,
      UP: 38,
      RIGHT: 39,
      DOWN: 40,
      DELETE: 46,
    };
    
    aria.Utils = aria.Utils || {};
    
    // Polyfill src https://developer.mozilla.org/en-US/docs/Web/API/Element/matches
    aria.Utils.matches = function (element, selector) {
      if (!Element.prototype.matches) {
        Element.prototype.matches =
          Element.prototype.matchesSelector ||
          Element.prototype.mozMatchesSelector ||
          Element.prototype.msMatchesSelector ||
          Element.prototype.oMatchesSelector ||
          Element.prototype.webkitMatchesSelector ||
          function (s) {
            var matches = element.parentNode.querySelectorAll(s);
            var i = matches.length;
            while (--i >= 0 && matches.item(i) !== this) {
              // empty
            }
            return i > -1;
          };
      }
    
      return element.matches(selector);
    };
    
    aria.Utils.remove = function (item) {
      if (item.remove && typeof item.remove === 'function') {
        return item.remove();
      }
      if (
        item.parentNode &&
        item.parentNode.removeChild &&
        typeof item.parentNode.removeChild === 'function'
      ) {
        return item.parentNode.removeChild(item);
      }
      return false;
    };
    
    aria.Utils.isFocusable = function (element) {
      if (element.tabIndex < 0) {
        return false;
      }
    
      if (element.disabled) {
        return false;
      }
    
      switch (element.nodeName) {
        case 'A':
          return !!element.href && element.rel != 'ignore';
        case 'INPUT':
          return element.type != 'hidden';
        case 'BUTTON':
        case 'SELECT':
        case 'TEXTAREA':
          return true;
        default:
          return false;
      }
    };
    
    aria.Utils.getAncestorBySelector = function (element, selector) {
      if (!aria.Utils.matches(element, selector + ' ' + element.tagName)) {
        // Element is not inside an element that matches selector
        return null;
      }
    
      // Move up the DOM tree until a parent matching the selector is found
      var currentNode = element;
      var ancestor = null;
      while (ancestor === null) {
        if (aria.Utils.matches(currentNode.parentNode, selector)) {
          ancestor = currentNode.parentNode;
        } else {
          currentNode = currentNode.parentNode;
        }
      }
    
      return ancestor;
    };
    
    aria.Utils.hasClass = function (element, className) {
      return new RegExp('(\\s|^)' + className + '(\\s|$)').test(element.className);
    };
    
    aria.Utils.addClass = function (element, className) {
      if (!aria.Utils.hasClass(element, className)) {
        element.className += ' ' + className;
      }
    };
    
    aria.Utils.removeClass = function (element, className) {
      var classRegex = new RegExp('(\\s|^)' + className + '(\\s|$)');
      element.className = element.className.replace(classRegex, ' ').trim();
    };
    
    aria.Utils.bindMethods = function (object /* , ...methodNames */) {
      var methodNames = Array.prototype.slice.call(arguments, 1);
      methodNames.forEach(function (method) {
        object[method] = object[method].bind(object);
      });
    };
    
  • URL: /components/raw/modal-dialog/modal-dialog.js
  • Filesystem Path: components/04-organisms/modal-dialog/modal-dialog.js
  • Size: 16.9 KB

Modal Dialog

Componente per la gestione di finestre modali accessibili e personalizzabili.


Comportamento

Il componente viene renderizzato come:

  • <div class="rtds-dialog"> — contenitore esterno che avvolge l’elemento role="dialog"; rimane nel DOM anche quando la modale è chiusa
  • <div role="dialog"> — pannello modale interno, inizialmente nascosto (rtds-hidden); viene reso visibile da JavaScript all’apertura

Il componente richiede un dialogId univoco per collegare il pannello al proprio titolo tramite aria-labelledby.


Varianti

Varianti di contenuto

  • default: Modale base con icona a sinistra del titolo (leftIcon: "outline--information-circle")
  • filters: Variante per modali con filtri; include un solo pulsante di azione (Annulla)
  • s: Variante dimensione piccola — heading ridotto (rtds-heading-4), pulsante di chiusura senza classe rtds-btn--l
  • visible: Modale pre-aperta in contesto di layout; utile per preview e test

Parametri di configurazione

Contesto di sviluppo Fractal/Nunjucks — Questa sezione riguarda esclusivamente i parametri del context .config.yml usati durante lo sviluppo nella libreria.

Parametri Base

  • dialogId (stringa, obbligatorio): ID univoco della modale; usato per id, aria-labelledby e collegamento con il trigger esterno
  • label (stringa, obbligatorio): Testo del titolo visualizzato nell’<h2> dell’intestazione
  • content (stringa, opzionale): Contenuto testuale della modale; accetta HTML (|safe)
  • titleClasses (stringa, opzionale): Classi CSS aggiuntive sull’elemento <h2> del titolo (es. rtds-heading-3, rtds-heading-4)
  • leftIcon (stringa, opzionale): ID dell’icona dallo sprite SVG da mostrare a sinistra del titolo (es. outline--information-circle)
  • classes (stringa, opzionale): Classi CSS aggiuntive sull’elemento radice .rtds-dialog

Parametri Azioni

  • hasActions (booleano, opzionale): Se true, mostra il blocco .rtds-dialog__actions con i pulsanti
  • buttons (array, opzionale): Elenco dei pulsanti di azione renderizzati tramite @button; ogni elemento accetta:
    • label (stringa): Testo del pulsante
    • classes (stringa): Classi CSS del pulsante (es. rtds-btn--primary, rtds-btn--outlined)
    • callback (stringa, opzionale): Funzione JavaScript da eseguire al click (es. closeDialog(this))
    • type (stringa, opzionale): Tipo di pulsante; valori possibili: button-icon-left, button-icon-right — seleziona la variante @button da renderizzare

Parametro closeButton

  • closeButton (oggetto, opzionale): Configura il pulsante di chiusura nell’intestazione; se assente, il pulsante non viene renderizzato. Sottocampi:
    • closeButton.variant (stringa): Variante del componente @button da usare (es. --icon)
    • closeButton.context.label (stringa): Testo accessibile del pulsante (visibile solo agli screen reader se il pulsante è solo icona)
    • closeButton.context.icon (stringa): ID icona dallo sprite SVG (es. mini--x-mark)
    • closeButton.context.id (stringa): ID HTML del pulsante (es. close-dialog-button)
    • closeButton.context.classes (stringa): Classi CSS del pulsante (es. rtds-btn--l rtds-dialog__close-button)
    • closeButton.context.iconSize (stringa): Classi Tailwind per la dimensione dell’icona (es. rtds-w-6 rtds-h-6 md:rtds-w-8 md:rtds-h-8)
    • closeButton.context.callback (stringa): Funzione JavaScript al click (es. closeDialog(this))

Accessibilità

WCAG 2.1

Livello AA — criteri pertinenti:

  • 4.1.2 Nome, Ruolo, Valore — la modale usa role="dialog", aria-modal="true" e aria-labelledby per associarla al titolo
  • 2.1.1 Tastiera — tutti i controlli della modale sono raggiungibili e attivabili da tastiera
  • 2.4.3 Ordine del focus — il focus è gestito correttamente all’apertura e chiusura della modale

Struttura e semantica

  • <div class="rtds-dialog"> — contenitore presentazionale; non ha ruolo semantico proprio
  • <div role="dialog"> — ruolo esplicito dialog; annunciato dagli screen reader come finestra modale; l’attributo aria-modal="true" indica che il contenuto esterno è inaccessibile durante l’apertura
  • <h2 id="Label"> — titolo della modale; collegato tramite aria-labelledby; fornisce il nome accessibile del dialog
  • <div class="rtds-dialog__content" tabindex="0"> — area di contenuto scorrevole; tabindex="0" la rende raggiungibile da tastiera quando il contenuto supera l’altezza del pannello

Attributi ARIA

Attributo Valori Condizione
role="dialog" sempre presente sull’elemento modale interno
aria-modal "true" sempre presente — indica agli screen reader che la modale è attiva
aria-labelledby "Label" sempre presente — collega la modale al titolo <h2>
Tasto Azione
Tab / Shift+Tab Naviga tra gli elementi interattivi della modale (focus trap attivo)
Escape Chiude la modale
Enter / Space Attiva i pulsanti di azione o il pulsante di chiusura

Gestione del focus

  • Focus iniziale: al momento dell’apertura il focus viene spostato sul primo elemento interattivo della modale
  • Focus trap: il focus è intrappolato all’interno della modale tramite nodi sentinella invisibili
  • Focus restoration: alla chiusura il focus ritorna sull’elemento che ha aperto la modale
  • Focus visibile: lo stile :focus-visible è sempre visibile e non viene rimosso via CSS; l’area di contenuto mostra un box-shadow inset al focus

Screen reader

  • Gli screen reader annunciano la modale con il titolo fornito da aria-labelledby al momento dell’apertura
  • aria-modal="true" indica agli screen reader di trattare il contenuto esterno come inaccessibile
  • Il pulsante di chiusura (solo icona) deve avere closeButton.context.label descrittivo (es. “Chiudi la modale corrente”) — il testo viene reso visibile solo agli screen reader tramite la variante @button--icon
  • Lo scroll della pagina è bloccato durante l’apertura (classe has-dialog sul <body>) per prevenire confusione contestuale

Integrazione consumer

Il componente espone solo il pannello dialog — il trigger button è responsabilità del consumer e va realizzato nel markup della pagina che include la modale.

Markup del trigger

<button
  type="button"
  id="apri-dialog1"
  aria-haspopup="dialog"
  aria-controls="dialog1"
  aria-expanded="false"
>
  Apri modale
</button>
Attributo Valore Scopo
aria-haspopup="dialog" "dialog" Comunica agli screen reader che il pulsante apre un dialog
aria-controls="[dialogId]" ID del dialog Collega il trigger all’elemento role="dialog"
aria-expanded "false" iniziale Stato dell’apertura; aggiornato automaticamente da modal-dialog.js a "true" all’apertura e "false" alla chiusura

Chiamata JavaScript

Per aprire la modale usare la funzione globale esposta da modal-dialog.js:

openDialog(dialogId, focusAfterClosed, focusFirst)
Parametro Tipo Obbligatorio Descrizione
dialogId stringa ID dell’elemento role="dialog" da aprire
focusAfterClosed Element | stringa Elemento o ID su cui spostare il focus alla chiusura — tipicamente il trigger stesso (this)
focusFirst Element | stringa no Elemento o ID su cui spostare il focus all’apertura; se omesso, il focus va al primo elemento interattivo

Esempio di collegamento trigger → apertura:

<button
  type="button"
  id="apri-dialog1"
  aria-haspopup="dialog"
  aria-controls="dialog1"
  aria-expanded="false"
  onclick="openDialog('dialog1', this)"
>
  Apri modale
</button>

Gestione di aria-expanded

modal-dialog.js aggiorna automaticamente aria-expanded sul trigger: lo imposta a "true" all’apertura e a "false" alla chiusura (qualunque sia il metodo di chiusura: pulsante, tasto Escape o replaceDialog). Il requisito è che il trigger abbia già l’attributo aria-expanded nel markup — se assente, modal-dialog.js non lo aggiunge.

Esempio completo trigger + dialog

Markup completo come appare nella pagina del consumer:

<!-- Trigger -->
<button
  type="button"
  id="apri-dialog1"
  aria-haspopup="dialog"
  aria-controls="dialog1"
  aria-expanded="false"
  onclick="openDialog('dialog1', this)"
>
  Apri modale
</button>

<!-- Dialog (output di @modal-dialog con dialogId: "dialog1") -->
<div class="rtds-dialog">
  <div role="dialog" id="dialog1" aria-labelledby="dialog1Label" aria-modal="true" class="rtds-hidden">
    <div class="rtds-dialog__header">
      <h2 id="dialog1Label" class="rtds-heading-3">Titolo modale</h2>
      <button class="rtds-btn rtds-btn--l rtds-dialog__close-button" onclick="closeDialog(this)">
        <span class="rtds-sr-only">Chiudi la modale corrente</span>
        <!-- icona mini--x-mark -->
      </button>
    </div>
    <div class="rtds-dialog__content" tabindex="0">
      Contenuto della modale.
    </div>
    <div class="rtds-dialog__actions">
      <button class="rtds-btn rtds-btn--outlined" onclick="closeDialog(this)">Annulla</button>
      <button class="rtds-btn rtds-btn--primary">Salva</button>
    </div>
  </div>
</div>

Quando usare modal-dialog

Componente Quando usarlo
Modal dialog Operazioni che richiedono attenzione esclusiva dell’utente: conferme distruttive, form complessi, filtri avanzati. Blocca l’interazione con il resto della pagina.
Popover Informazioni contestuali non bloccanti, tooltip avanzati, piccoli menu. Non interrompe il flusso.
Toast Notifiche temporanee di feedback (successo, errore, info). Non richiedono azione da parte dell’utente.

Stili CSS

Classi Base

  • .rtds-dialog: Contenitore esterno del componente; non ha stili propri — è un wrapper
  • .rtds-dialog-backdrop: Overlay semitrasparente che copre la pagina quando la modale è aperta; nascosto di default
    • .rtds-dialog-backdrop.is-active: Rende visibile l’overlay (rtds-block)

Classi Elementi (BEM)

  • .rtds-dialog__header: Intestazione della modale con titolo e pulsante di chiusura; flex, justify-between, bordo inferiore
  • .rtds-dialog__title: Classe implicita sull’<h2> — non definita in CSS separato; le classi tipografiche (rtds-heading-3, rtds-heading-4) sono applicate tramite titleClasses
  • .rtds-dialog__content: Area di contenuto scrollabile; flex-1, overflow-y-auto, padding standard
  • .rtds-dialog__actions: Barra dei pulsanti di azione; sticky bottom-0, bordo superiore, sfondo bianco
  • .rtds-dialog__close-button: Modifica il pulsante di chiusura — rimuove sfondo e bordo, anche al focus

Classi Modificatori (su role="dialog" tramite wrapper)

  • .rtds-modal-dialog--inverse: Intestazione con sfondo e colore del testo invertiti (tema scuro)
  • .rtds-modal-dialog--s: Variante piccola — riduce il padding dell’intestazione e delle azioni

Esempi di utilizzo

<!-- Modale base con pulsante di chiusura e azioni -->
<div class="rtds-dialog-backdrop" id="dialogBackdrop">
    <div class="rtds-dialog">
        <div role="dialog" id="dialog1" aria-labelledby="dialog1Label" aria-modal="true">
            <div class="rtds-dialog__header">
                <h2 id="dialog1Label" class="rtds-dialog__title rtds-heading-3">Titolo modale</h2>
                <button id="close-dialog-button" class="rtds-btn rtds-btn--l rtds-dialog__close-button"
                    onclick="closeDialog(this)">
                    <span class="rtds-sr-only">Chiudi la modale corrente</span>
                    <!-- icona x-mark -->
                </button>
            </div>
            <div class="rtds-dialog__content" tabindex="0">
                Contenuto della modale.
            </div>
            <div class="rtds-dialog__actions">
                <button class="rtds-btn rtds-btn--outlined" onclick="closeDialog(this)">Annulla</button>
                <button class="rtds-btn rtds-btn--primary">Salva</button>
            </div>
        </div>
    </div>
</div>

<!-- Variante small -->
<div class="rtds-dialog rtds-modal-dialog--s">
    <div role="dialog" id="dialog-s" aria-labelledby="dialog-sLabel" aria-modal="true">
        <div class="rtds-dialog__header">
            <h2 id="dialog-sLabel" class="rtds-dialog__title rtds-heading-4">Modal small</h2>
            <!-- pulsante chiusura senza rtds-btn--l -->
        </div>
        <div class="rtds-dialog__content" tabindex="0">Contenuto compatto.</div>
    </div>
</div>

Note

  • Il componente richiede il file modal-dialog.js per la gestione dell’apertura/chiusura, del focus trap e della classe has-dialog sul <body>
  • Il dialogId deve essere univoco nella pagina e corrispondere all’ID del trigger esterno
  • Per attivare la modale da un trigger esterno, usare aria-haspopup="dialog", aria-controls="" e aria-expanded sul trigger
  • La variante inverse richiede la classe .rtds-modal-dialog--inverse sul wrapper .rtds-dialog, non sull’elemento role="dialog"