辅助功能:聚焦时移除 <p> 上的轮廓

Accessibility: Remove outline on <p> when focused

需要说明的是,我正在寻找最方便且设计最佳的选项。所以不,我不会删除按钮和链接上的大纲以及任何有用的地方。但我正在努力使 Dialog 工作。为此,我使用这个 example。在此示例中,第一个 <p> 在打开对话框时获得焦点。如示例中所述:

In this dialog, the first paragraph has tabindex=-1. The first paragraph is also contained inside the element that provides the dialog description, i.e., the element that is referenced by aria-describedby. With some screen readers, this may have one negative but relatively insignificant side effect when the dialog opens -- the first paragraph may be announced twice. Nonetheless, making the first paragraph focusable and setting the initial focus on it is the most broadly accessible option.

但是当然这一段有一个大纲,因为它是重点。我想知道该元素是否具有 tabindex=-1 并且不是您可以与之交互的元素。可以去掉这部分的轮廓吗?

提前致谢

简答

如果您愿意,可以安全地删除带有 tabindex="-1" 的段落上的焦点指示器。不过,有一种更好的方法来处理嵌套模态。

长答案

WCAG 在描述事物的方式上有点“模糊”(不够具体),但 guidance for focus indicators 用于“控件”或“交互元素”。

另外主要描述的条件是

Success Criterion 2.4.7 Focus Visible (Level AA): Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible.

由于您无法与该段落进行交互(它不可通过键盘操作),因此很容易争辩说提供焦点指示器实际上更加混乱,因为没有标准操作会起作用。但是,您也可能会争辩说,在没有焦点指示器的情况下登陆新模式也会令人困惑。

因此这是一个判断调用,如果我需要以编程方式聚焦它们,我总是会删除非交互元素上的聚焦指示器,尤其是在按 Enter 时可以提交表格等

有没有更好的方法来避免聚焦 non-interactive 元素的陷阱?

有一种方法可以解决所有这些问题,并且仍然有一个可见的焦点指示器。

我们将关闭按钮添加到模式的顶部(通常位于右上角)并使用 aria-describedby 指向模式标题。

<button id="dialog2_close_btn" aria-describedby="dialog2_label" onclick="closeDialog(this)">Close</button>

这将显示为“关闭验证结果”。然后我们只关注那个按钮而不是模态标题

<button onclick="openDialog('dialog2', this, 'dialog2_close_btn')">
  Verify Address
</button>

我已经在下面的示例中进行了调整,如果您在打开第一个模式后单击“验证地址”,您会看到顶部有一个关闭按钮。

/**
 * @namespace aria
 */

var aria = aria || {};

/**
 * @desc
 *  Key code constants
 */
aria.KeyCode = {
  BACKSPACE: 8,
  TAB: 9,
  RETURN: 13,
  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) {}
        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 || (element.tabIndex === 0 && element.getAttribute('tabIndex') !== null)) {
    return true;
  }

  if (element.disabled) {
    return false;
  }

  switch (element.nodeName) {
    case 'A':
      return !!element.href && element.rel != 'ignore';
    case 'INPUT':
      return element.type != 'hidden' && element.type != 'file';
    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);
  });
};


/*
*   This content is licensed according to the W3C Software License at
*   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*/

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';

  /**
   * @desc 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
   *  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

  /**
   * @desc Find the last descendant node that is focusable.
   * @param element
   *          DOM node for which to find the last focusable descendant.
   * @returns
   *  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

  /**
   * @desc Set Attempt to set focus on the current node.
   * @param element
   *          The node to attempt to focus on.
   * @returns
   *  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) {
    }
    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 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();
      return true;
    }

    return false;
  };

  aria.handleEscape = function (event) {
    var key = event.which || event.keyCode;

    if (key === aria.KeyCode.ESC && aria.closeCurrentDialog()) {
      event.stopPropagation();
    }
  };

  document.addEventListener('keyup', aria.handleEscape);

  /**
   * @constructor
   * @desc 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 = '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('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.clearDialog();
    this.dialogNode.className = 'default_dialog'; // make visible

    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 = '';
      }
    );
  };

  /**
   * @desc
   *  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 () {
    aria.OpenDialogList.pop();
    this.removeListeners();
    aria.Utils.remove(this.preNode);
    aria.Utils.remove(this.postNode);
    this.dialogNode.className = 'hidden';
    this.backdropNode.classList.remove('active');
    this.focusAfterClosed.focus();

    // 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

  /**
   * @desc
   *  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) {
    var closedDialog = aria.getCurrentDialog();
    aria.OpenDialogList.pop();
    this.removeListeners();
    aria.Utils.remove(this.preNode);
    aria.Utils.remove(this.postNode);
    this.dialogNode.className = 'hidden';
    this.backdropNode.classList.remove('active');

    var focusAfterClosed = newFocusAfterClosed || this.focusAfterClosed;
    var dialog = 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) {
    var dialog = new aria.Dialog(dialogId, focusAfterClosed, focusFirst);
  };

  window.closeDialog = function (closeButton) {
    var topDialog = aria.getCurrentDialog();
    if (topDialog.dialogNode.contains(closeButton)) {
      topDialog.close();
    }
  }; // 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

}());
.hidden {
  display: none;
}

[role="alertdialog"],
[role="dialog"] {
  box-sizing: border-box;
  padding: 15px;
  border: 1px solid #000;
  background-color: #fff;
  min-height: 100vh;
}

@media screen and (min-width: 640px) {
  [role="alertdialog"],
  [role="dialog"] {
    position: absolute;
    top: 2rem;
    left: 50vw;  /* move to the middle of the screen (assumes relative parent is the body/viewport) */
    transform: translateX(-50%);  /* move backwards 50% of this element's width */
    min-width: calc(640px - (15px * 2));  /* == breakpoint - left+right margin */
    min-height: auto;
    box-shadow: 0 19px 38px rgba(0, 0, 0, 0.12), 0 15px 12px rgba(0, 0, 0, 0.22);
  }
}

.dialog_label {
  text-align: center;
}

.dialog_form {
  margin: 15px;
}

.dialog_form .label_text {
  box-sizing: border-box;
  padding-right: 0.5em;
  display: inline-block;
  font-size: 16px;
  font-weight: bold;
  width: 30%;
  text-align: right;
}

.dialog_form .label_info {
  box-sizing: border-box;
  padding-right: 0.5em;
  font-size: 12px;
  width: 30%;
  text-align: right;
  display: inline-block;
}

.dialog_form_item {
  margin: 10px 0;
  font-size: 0;
}

.dialog_form_item .wide_input {
  box-sizing: border-box;
  max-width: 70%;
  width: 27em;
}

.dialog_form_item .city_input {
  box-sizing: border-box;
  max-width: 70%;
  width: 17em;
}

.dialog_form_item .state_input {
  box-sizing: border-box;
  max-width: 70%;
  width: 15em;
}

.dialog_form_item .zip_input {
  box-sizing: border-box;
  max-width: 70%;
  width: 9em;
}

.dialog_form_actions {
  text-align: right;
  padding: 0 20px 20px;
}

.dialog_close_button {
  float: right;
  position: absolute;
  top: 10px;
  left: 92%;
  height: 25px;
}

.dialog_close_button img {
  border: 0;
}

.dialog_desc {
  padding: 10px 20px;
}

/* native <dialog> element uses the ::backdrop pseudo-element */

/* dialog::backdrop, */
.dialog-backdrop {
  display: none;
  position: fixed;
  overflow-y: auto;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

@media screen and (min-width: 640px) {
  .dialog-backdrop {
    background: rgba(0, 0, 0, 0.3);
  }
}

.dialog-backdrop.active {
  display: block;
}

.no-scroll {
  overflow-y: auto !important;
}

/* this is added to the body when a dialog is open */
.has-dialog {
  overflow: hidden;
}

/* styling for alert-dialog example */
.notes {
  display: block;
  font-size: 1rem;
  line-height: 1.3;
  min-width: 400px;
  max-width: 100%;
  width: 33%;
}

.toast {
  background-color: rgba(0, 0, 0, 0.9);
  color: #fff;
  padding: 1rem;
  border: none;
  border-radius: 0.25rem;
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
  position: fixed;
  top: 1rem;
  right: 1rem;
  transform: translateY(-150%);
  transition: transform 225ms cubic-bezier(0.4, 0, 0.2, 1);
}

.toast.active {
  transform: translateY(0);
}
<button onclick="openDialog('dialog1', this)">
  Add Delivery Address
</button>
<div role="dialog"
     id="dialog1"
     aria-labelledby="dialog1_label"
     aria-modal="true"
     class="hidden">
  <h2 id="dialog1_label" class="dialog_label">
    Add Delivery Address
  </h2>
  <div class="dialog_form">
    <div class="dialog_form_item">
      <label>
        <span class="label_text">
          Street:
        </span>
        <input type="text" class="wide_input">
      </label>
    </div>
    <div class="dialog_form_item">
      <label>
        <span class="label_text">
          City:
        </span>
        <input type="text" class="city_input">
      </label>
    </div>
    <div class="dialog_form_item">
      <label>
        <span class="label_text">
          State:
        </span>
        <input type="text" class="state_input">
      </label>
    </div>
    <div class="dialog_form_item">
      <label>
        <span class="label_text">
          Zip:
        </span>
        <input type="text" class="zip_input">
      </label>
    </div>
    <div class="dialog_form_item">
      <label for="special_instructions">
        <span class="label_text">
          Special instructions:
        </span>
      </label>
      <input id="special_instructions"
             type="text"
             aria-describedby="special_instructions_desc"
             class="wide_input">
      <div class="label_info" id="special_instructions_desc">
        For example, gate code or other information to help the driver find you
      </div>
    </div>
  </div>
  <div class="dialog_form_actions">
    <button onclick="openDialog('dialog2', this, 'dialog2_close_btn')">
      Verify Address
    </button>
    <button onclick="replaceDialog('dialog3', undefined, 'dialog3_close_btn')">
      Add
    </button>
    <button onclick="closeDialog(this)">
      Cancel
    </button>
  </div>
</div>
<!--  Second modal to open on top of the first modal  -->
<div id="dialog2"
     role="dialog"
     aria-labelledby="dialog2_label"
     aria-describedby="dialog2_desc"
     aria-modal="true"
     class="hidden">
     <button id="dialog2_close_btn" aria-describedby="dialog2_label" onclick="closeDialog(this)">Close</button>
  <h2 id="dialog2_label" class="dialog_label">
    Verification Result
  </h2>
  <div id="dialog2_desc" class="dialog_desc">
    <p tabindex="-1" id="dialog2_para1">
      This is just a demonstration. If it were a real application, it would
      provide a message telling whether the entered address is valid.
    </p>
    <p>
      For demonstration purposes, this dialog has a lot of text. It demonstrates a
        scenario where:
    </p>
    <ul>
      <li>
        The first interactive element, the help link, is at the bottom of the dialog.
      </li>
      <li>
        If focus is placed on the first interactive element when the dialog opens, the
        validation message may not be visible.
      </li>
      <li>
        If the validation message is visible and the focus is on the help link, then
        the focus may not be visible.
      </li>
      <li>
        When the dialog opens, it is important that both:
        <ul>
          <li>
            The beginning of the text is visible so users do not have to scroll back to
            start reading.
          </li>
          <li>
            The keyboard focus always remains visible.
          </li>
        </ul>
      </li>
    </ul>
    <p>
      There are several ways to resolve this issue:
    </p>
    <ul>
      <li>
        Place an interactive element at the top of the dialog, e.g., a button or link.
      </li>
      <li>
        Make a static element focusable, e.g., the dialog title or the first block of
        text.
      </li>
    </ul>
    <p>
      Please
      <em>
        DO NOT
      </em>
      make the element with role dialog focusable!
    </p>
    <ul>
      <li>
        The larger a focusable element is, the more difficult it is to visually
        identify the location of focus, especially for users with a narrow field of view.
      </li>
      <li>
        The dialog has a visual border, so creating a clear visual indicator of focus
        when the entire dialog has focus is not very feasible.
      </li>
      <li>
        Screen readers read the label and content of focusable elements. The dialog
        contains its label and a lot of content! If a dialog like this one has focus, the
        actual focus is difficult to comprehend.
      </li>
    </ul>
    <p>
      In this dialog, the first paragraph has
      <code>
        tabindex=
        <q>
          -1
        </q>
      </code>
      . The first
      paragraph is also contained inside the element that provides the dialog description, i.e., the element that is referenced
      by
      <code>
        aria-describedby
      </code>
      . With some screen readers, this may have one negative
      but relatively insignificant side effect when the dialog opens -- the first paragraph
      may be announced twice. Nonetheless, making the first paragraph focusable and setting
      the initial focus on it is the most broadly accessible option.
    </p>
  </div>
  <div class="dialog_form_actions">
    <a href="#" onclick="openDialog('dialog4', this)">
      link to help
    </a>
    <button onclick="openDialog('dialog4', this)">
      accepting an alternative form
    </button>
    <button onclick="closeDialog(this)">
      Close
    </button>
  </div>
</div>
<!--  Dialog that replaces dialog 1.  -->
<div id="dialog3"
     role="dialog"
     aria-labelledby="dialog3_label"
     aria-describedby="dialog3_desc"
     aria-modal="true"
     class="hidden">
  <h2 id="dialog3_label" class="dialog_label">
    Address Added
  </h2>
  <p id="dialog3_desc" class="dialog_desc">
    The address you provided has been added to your list of delivery addresses. It is ready
    for immediate use. If you wish to remove it, you can do so from
    <a href="#" onclick="openDialog('dialog4', this)">
      your profile.
    </a>
  </p>
  <div class="dialog_form_actions">
    <button id="dialog3_close_btn" onclick="closeDialog(this)">
      OK
    </button>
  </div>
</div>
<div id="dialog4"
     role="dialog"
     aria-labelledby="dialog4_label"
     aria-describedby="dialog4_desc"
     class="hidden"
     aria-modal="true">
  <h2 id="dialog4_label" class="dialog_label">
    End of the Road!
  </h2>
  <p id="dialog4_desc" class="dialog_desc">
    You activated a fake link or button that goes nowhere!
    The link or button is present for demonstration purposes only.
  </p>
  <div class="dialog_form_actions">
    <button id="dialog4_close_btn" onclick="closeDialog(this)">
      Close
    </button>
  </div>
</div>