{"id":7603,"date":"2019-03-17T20:52:47","date_gmt":"2019-03-18T00:52:47","guid":{"rendered":"http:\/\/www.useragentman.com\/blog\/?p=7603"},"modified":"2023-10-25T16:39:48","modified_gmt":"2023-10-25T20:39:48","slug":"creating-accessible-html5-modal-dialogs-for-desktop-and-mobile","status":"publish","type":"post","link":"https:\/\/www.useragentman.com\/blog\/2019\/03\/17\/creating-accessible-html5-modal-dialogs-for-desktop-and-mobile\/","title":{"rendered":"Creating Accessible HTML5 Modal Dialogs For Desktop and Mobile"},"content":{"rendered":"\r\n<!-- Simple pop-up dialog box, containing a form -->\r\n<div class=\"demo-container\">\r\n\r\n<div class=\"demo-container__instructions\">\r\n   The button below is accessible with a mouse, a keyboard and accessibility gestures on a mobile device. The blog post below explains why.\r\n<\/div>\r\n<dialog id=\"favDialog\" aria-labelledby=\"favDialog__label\" aria-describedby=\"favDialog__description\">\r\n    \r\n    <div role=\"document\">\r\n        <button id=\"cancel\" class=\"a11y-modal__button--close\" data-modal-function=\"hide\">\r\n            <img decoding=\"async\" class=\"a11y-modal__button--close-image\" src=\"\/tests\/aria-role-demos\/images\/close-window.svg\" alt=\"close this dialog\">\r\n        <\/button>\r\n        <h2 id=\"favDialog__label\">Login<\/h2>\r\n        <p id=\"favDialog__description\">In order to continue, please log into the application<\/p>\r\n        <form method=\"dialog\">\r\n            <section>\r\n                <div>\r\n                    <label for=\"username\">Username:<\/label>\r\n                    <input id=\"username\" type=\"text\" name=\"u\" \/>\r\n                <\/div>\r\n                <div>\r\n                    <label for=\"password\">Password:<\/label>\r\n                    <input id=\"password\" type=\"password\" name=\"p\" \/>\r\n                <\/div>\r\n            <\/section>\r\n            <menu>\r\n                <button id=\"cancel\" type=\"reset\" onClick=\"return onModalButtonClick();\">Cancel<\/button>\r\n                <button type=\"submit\" onClick=\"return onModalButtonClick();\">Confirm<\/button>\r\n            <\/menu>\r\n        <\/form>\r\n    <\/div>\r\n<\/dialog>\r\n\r\n<menu class=\"image__container\">\r\n    <button class=\"modal__opener\" id=\"updateDetails\">Log in to our website<\/button>\r\n    <!--\r\n        This is a decorative image, so there we set the role to \r\n        \"presentation\" and blank the alt attribute.\r\n    -->\r\n    <img decoding=\"async\" alt=\"\" role=\"presentation\" class=\"image\" src=\"\/aria-role-demos\/images\/point-to-dialog.svg\">\r\n<\/menu>\r\n<\/div>\r\n<p>Accessible modals aren&#8217;t hard to make, and you can make your modal dialogs accessible if you keep in mind the following requirements:<\/p>\n<ol>\n<li><strong><em>Markup:<\/em><\/strong> You should use the correct HTML5 tags and ARIA roles inside the modal to ensure it works well with screen readers.<\/li>\n<li><strong><em>Applying Focus On Modal Open:<\/em><\/strong> When an interactive element (like a button) opens a modal, keyboard focus should go to the first element in the modal (usually the modal&#8217;s close button).  The modal should also announce that a modal opened along with the modal&#8217;s title.<\/li>\n<li><strong><em>Applying Focus On Modal Close:<\/em><\/strong> When the user closes the modal, keyboard focus should go back to the interactive element that opened up the element in step 1.<\/li>\n<li><strong><em>Restricting Focus To The Open Modal:<\/em><\/strong> When the modal is open, users should only be able to access elements within that modal.  This means that keyboard users should not be able to tab into elements behind the modal, and that mobile users should not be able to swipe beneath the modal.<\/li>\n<\/ol>\n<p>Below is a video demonstrating of all of these features in action, on both a desktop and mobile device:<\/p>\n\r\n<div class=\"youtube-container\">\r\n<iframe src=\"https:\/\/www.youtube.com\/embed\/NINogq4BS68?rel=0\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen><\/iframe>\r\n<\/div>\r\n<div class=\"transcript__button-container\">\r\n<button class=\"dialog__button transcript__button\" data-dialog-function=\"show\" data-dialog-id=\"transcript1\" aria-label=\"Transcript for the preceding video demo\">Transcript<\/button>\r\n<\/div>\r\n<dialog id=\"transcript1\" class=\"uam-dialog\" aria-labelledby=\"transcript1__label\">\r\n    \r\n    <div role=\"document\">\r\n        <button id=\"cancel\" class=\"a11y-modal__button--close transcript__close-button\" data-dialog-function=\"hide\" data-dialog-id=\"transcript1\">\r\n            <img decoding=\"async\" class=\"a11y-modal__button--close-image\" src=\"\/tests\/aria-role-demos\/images\/close-window.svg\" alt=\"close this dialog\">\r\n        <\/button>\r\n        <h2 id=\"transcript1__label\">Transcript for Video Demo<\/h2>\r\n        <div class=\"transcript__content\">\r\n(Note: This transcript will mention when code snippets are shown.  These snippets are fully outlined in the blog post in great detail)\r\n\r\n(Scene is a title screen, which reads \"Creating Accessible HTML5 Modal Dialogs For Desktop and Mobile.  Zoltan Hawryluk - useragentman.com\")\r\n\r\nNarrator: \u201cIn this video, I will be demonstrating how to create an accessible modal dialog using the HTML5 tag. It uses a polyfill for browsers that don't support it yet natively. For details on the polyfill and more information on how this demo was built, please checkout the video's accompanying blog post on useragentman.com.\u201d\r\n\r\n(A web page appears inside the OSX Safari web browser.  The web page has a picture of a woman and a gnome looking at an HTML button labelled \"Log in to our website\")\r\n\r\nThe first browser we are going to demonstrate is Safari using VoiceOver, which is built into OSX. Right now, keyboard focus is on the link just before the button labelled \"Log in to our website\", which will open the modal when pressed. So, let's use the keyboard to navigate to the button...\u201d\r\n\r\n(As the narrator uses his keyboard to navigate to the \"Log in to our website\" button, the VoiceOver screen reader reports what items are focused.)\r\n\r\nVoiceOver: \u201cLog in to our website, button, main, two items\u201d\r\n\r\nNarrator: \u201cand now we'll hit the enter key.\u201d\r\n\r\n(Narrator hits the enter key.  A dialog opens up with a log in form in it.  The web browser automatically places focus on the dialog's \"close\" button.)\r\n\r\nVoiceOver: \u201cClose this dialog, web dialog, login, 1 item. In order to continue, please log into the application.\u201d\r\n\r\n(A close up appears on VoiceOver's caption panel to emphasize what the narrator says below.)\r\n\r\nNarrator: \u201cYou may have noticed that when the dialog opened, the close button gains focus.  Also, note that the screen reader said \"close this dialog, button\".  This tells the screen reader user that focus is on a button labelled \"close this dialog\".\u201d\r\n\r\n(Code snippets appear to emphasize the points outlined by the narrator below.)\r\n\r\nNarrator: \u201cIncidentally, this label was coded as alt attribute for the button's tag. You'll also note that the screen reader reports it is a web dialog. That's because the button is inside a HTML dialog tag. VoiceOver would also announce this if the modal was coded as a div tag with its role attribute set to \"dialog\".  In fact, setting the role to dialog is what the polyfill does on all dialog tags so screen readers can report the dialog properly to the user.\u201d\r\n\r\nFinally, you will notice the screen reader reads out the header, and the description in the dialog. This happens because the tag has its aria-labelledby attribute set to the id of the header, and its aria-describedby attribute set the id of the description text.\u201d\r\n\r\n(Scene changes from the code snippets back to the web page.)\r\n\r\nNarrator: \u201cNow, let's find out what happens when we tab past the end of the dialog.\u201d\r\n\r\n(The narrator uses the keyboard to tab to the next interactive element)\r\n\r\nVoiceOver: \u201cUsername, edit text with autofill menu.\u201d\r\n\r\n(The narrator uses the keyboard to tab to the next interactive element)\r\n\r\nVoiceOver: \u201cPassword, secure edit text with...\u201d\r\n\r\n(The narrator uses the keyboard to tab to the next interactive element)\r\n\r\nVoiceOver: \u201cCancel, button.\u201d\r\n\r\n(The narrator uses the keyboard to tab to the next interactive element)\r\n\r\nVoiceOver: \u201cConfirm, button.\u201d\r\n\r\n(The narrator uses the keyboard to tab to the next interactive element. Since there is no interactive element after the confirm button, focus goes back to the dialog's close button)\r\n\r\nVoiceOver: \u201cClose this dialog, button.\u201d\r\n\r\nNarrator: \u201cYou'll notice that when I hit the tab key after the \"Confirm\" button, focus goes to the first element in the dialog. Focus never goes to any interactive element behind the modal. For browsers that support the tag, this automatically happens because it is native browser behavior. However, for browsers that don't, the polyfill steps in and uses focus events to implement this. If focus goes outside the dialog, it loops back to the first element of the modal... if we are tabbing forward, of course. If we are tabbing backward, using a shift-TAB, then focus goes the last element of the modal. And now, let's find out what happens when I close the modal...\u201d\r\n\r\n(The narrator hits the enter key in order to activate the close button.  Focus goes back the button that originally opened the modal.)\r\n\r\nVoiceOver: \u201cLogin in to our website, button, main, 2 items.\u201d\r\n\r\nNarrator: \u201cYou'll notice that focus is placed back to the button that opened it. Surprisingly, this is not default browser behavior, even though this is considered best practice in the WAI-ARIA documentation. It is added by a bit of code in the polyfill. This code is executed for all browsers, including those with native support for the dialog tag. Now let's look at Google Chrome using Android's built in screen reader, Talkback.\u201d\r\n\r\n(Scene changes from Safari on OSX to Google Chrome on Android, showing the same web page.)\r\n\r\nNarrator: \u201cI am using a Samsung tablet for this demonstration, but the behavior for this device is, more or less, the same on any Android device using this software. I'm going to start by navigating to the link just before the button that opens the modal. Since this device doesn't have a keyboard, Talkback users must swipe left and right to navigate back and forth within the document.\u201d\r\n\r\n(Narrator swipes through the document to get to the \"Log in to our website\" button.  Focus seems to go to not just the interactive elements on the page, but on <em>all<\/em> elements on the page.  Talkback says short beginnings of dialog while user swipes through text. Finally, focus gets to the button.)\r\n\r\nTalkback: \u201cLog in to our website, button, out of list.\u201d\r\n\r\nNarrator: \u201cYou'll also note that it looks like focus is not just going to the interactive elements on the page, like the tab key does on a keyboard enabled device.  Instead, Talkback's so-called \"accessibility focus\" goes to all the items on the page that can be read. This is default behavior for Talkback, and iOS's VoiceOver screen reader has similar behavior. This can be changed, but I will leave that for a future video. Now let's double tap to activate this button.\u201d\r\n\r\n(Narrator double taps the screen)\r\n\r\nTalkback: \u201cClose this dialog, button. Double-tap to activate.\u201d\r\n\r\nNarrator: \u201cAgain, you will notice that focus goes to the close button.  However, unlike VoiceOver, Talkback didn't read the dialog's heading or the description. It also doesn't mention that you are now inside a web dialog. This is because different screen readers don't support all the ARIA-attributes equally. This is the reason why I chose \"close this dialog\" as the alt attribute for the close button image. It tells the user that they are inside a dialog, even if a particular screen reader doesn't support the dialog role. This is a good example that, just like in all aspects of web development, you should use progressive-enhancement to make your code bullet-proof. Now, let's try to swipe backwards past the close button at the beginning of the dialog.\u201d\r\n\r\n(Narrator tries to swipe backwards part the close button.  Focus goes to the whole document in the web browser.)\r\n\r\nTalkback: \u201cCreating An Accessible Dialog Using the HTML5 Dialog Tag, web view.\u201d\r\n\r\nNarrator: \u201cYou'll notice that focus doesn't go behind the modal, just like when I was using a desktop screen reader.  However, this is not because of the focus events I described earlier. The problem with most (if not all) mobile screen readers is that they don't fire focus and blur events back to the browser when swiping over the document.\u201d\r\n\r\n(Scene changes to a web browser development tools, which shows aria-hidden=\"true\" being set on HTML elements outside of the dialog HTML element.)\r\n\r\nNarrator: \u201cIn order to code this behavior on mobile, developers must set aria-hidden= \"true\" to all elements outside of the modal dialog. This may seem a little scary if you have a lot of interactive elements outside the dialog. (I mean, who wants to set aria-hidden attributes on all of those DOM nodes?) However, this is an efficient algorithm to do this, and it's in the blog post that accompanies this video.\u201d\r\n\r\n(Scene changes to the useragentman.com website)\r\n\r\nNarrator: \u201cIf you are playing this video on the YouTube website, the link to the post is the description below it.\u201d\r\n\r\n(Scene changes to a picture of the narrator, Zoltan Hawryluk, which shows his twitter handle, @zoltandulac, and his blog's URL, useragentman.com)\r\n\r\nNarrator: \u201cHope this demonstration was helpful! Please feel free to comment on the my YouTube page, or my blog, useragentman.com. I'd love to hear your feedback. Thank you.\u201d\r\n        <\/div>\r\n    <\/div>\r\n<\/dialog>\r\n<p>Unfortunately, some &#8220;accessible&#8221; modal JavaScript libraries work well for keyboard users, but not for mobile ones.  This article will discuss why and how to work around the issues that mobile creates.  <\/p>\n<p>In order to keep this tutorial simple, we will be using the HTML5 <code>&lt;dialog&gt;<\/code> polyfill.  We will be adding code to make not only the polyfill accessible, but also improve the accessibility of native implementations (which, at the time of this writing, is only Blink-based browsers like Chrome and Opera).<\/p>\n<p>If you want to just download the example code used in this article, you can download it from <a href=\"https:\/\/github.com\/zoltan-dulac\/accessible-html5-dialog-polyfill\">the demo&#8217;s github repo<\/a> and start playing with it right away.<\/p>\n<p><a href=\"https:\/\/github.com\/zoltan-dulac\/accessible-html5-dialog-polyfill\" class=\"exampleLink\">Download the demo code from github<\/a><br \/>\n<a href=\"\/tests\/accessible-html5-dialog-demo\/index.html\" class=\"exampleLink\">Go to a cleanroom page of the demo<\/a><\/p>\n<h2>Basic Markup<\/h2>\n<p>Creating a dialog is simple.<\/p>\n<blockquote class=\"code\">\n<pre>\r\n\r\n<!-- Here is the button that opens the modal -->\r\n&lt;button id=\"updateDetails\"&gt;\r\n    Open Modal\r\n&lt;\/button&gt;\r\n\r\n.\r\n.\r\n.\r\n\r\n&lt;!-- Simple pop-up dialog box, containing a form --&gt;\r\n&lt;dialog \r\n    id=\"favDialog\"\r\n    <span class=\"hilite\">aria-labelledby=\"favDialog__label\"<\/span>\r\n    <span class=\"hilite2\">aria-describedby=\"favDialog__description\"<\/span>&gt;\r\n    &lt;div role=\"document\"&gt;\r\n\r\n        &lt;!--\r\n            The cancel button should be the first in the DOM.\r\n            Note it should be a button, not a link, since it\r\n            doesn't go to another page.\r\n        --&gt;\r\n        &lt;button\r\n            id=\"cancel\"\r\n            class=\"a11y-modal__button--close\"\r\n        &gt;\r\n            &lt;img\r\n                class=\"a11y-modal__button--close-image\"\r\n                src=\"images\/close-window.svg\"\r\n                alt=\"close this dialog\"&gt;\r\n        &lt;\/button&gt;\r\n\r\n        &lt;!--\r\n            This is the title of the dialog.  Since it is\r\n            set as the dialog's `aria-labelledby`, it will \r\n            be read when it is opened\r\n        --&gt;\r\n        &lt;h2 <span class=\"hilite\">id=\"favDialog__label\"<\/span>&gt;Login&lt;\/h2&gt;\r\n\r\n        &lt;!--\r\n            This should a more detailed description of the\r\n            purpose of the dialog.  Since it is set as the \r\n            dialog's `aria-describedby`, it will be read\r\n            when the dialog is opened.\r\n        --&gt;\r\n        &lt;p <span class=\"hilite2\">id=\"favDialog__description\"<\/span>&gt;\r\n           In order to continue, please log into the application\r\n        &lt;\/p&gt;\r\n        \r\n        &lt;!-- \r\n            Rest of dialog here \r\n        --&gt;\r\n\r\n        ...\r\n    &lt;\/div&gt;\r\n&lt;\/dialog&gt;\r\n\r\n.\r\n.\r\n.\r\n\r\n&lt;script src=\"js\/shared\/dialog-polyfill.js\"&gt;&lt;\/script&gt;\r\n\r\n\r\n<\/pre>\n<\/blockquote>\n<p>In order for the button to actually open the modal, we will need a little bit of Javascript:<\/p>\n<blockquote class=\"code\">\n<pre>\r\n(function() {\r\n    \/\/ Button that opens the dialog\r\n    const updateButton = document.getElementById('updateDetails');\r\n\r\n    \/\/ Clicking this button opens the dialog\r\n    updateButton.addEventListener('click', function() {\r\n      favDialog.showModal();\r\n    });\r\n\r\n    \/\/ The modal's cancel button\r\n    const cancelButton = document.getElementById('cancel');\r\n\r\n    \/\/ Clicking the cancel button will close the dialog\r\n    cancelButton.addEventListener('click', function() {\r\n      favDialog.close();\r\n    });\r\n\r\n    \/\/ The &lt;dialog&gt; element itself\r\n    const favDialog = document.getElementById('favDialog');\r\n\r\n    \/\/ If we are using the polyfill, then initialize it\r\n    if (window.dialogPolyfill) {\r\n      dialogPolyfill.registerDialog(favDialog);\r\n    }\r\n\r\n  })();\r\n<\/pre>\n<\/blockquote>\n<p>A few accessibility notes here:<\/p>\n<ol>\n<li>Using the <code>&lt;dialog&gt;<\/code> tag, screen readers will report the element as a dialog.  The polyfill will insert a <code>role=\"dialog\"<\/code> inside the dialog tag for browsers that don&#8217;t support <code>&lt;dialog&gt;<\/code> natively.  <strong>If you decide to not to use the HTML5 <code>&lt;dialog&gt;<\/code> tag to create the modal, you <em>must<\/em> add the <code>role=\"dialog\"<\/code> attribute yourself in order to maintain this accessibility feature.<\/strong><\/li>\n<li>The <code>&lt;div role=\"document\"&gt;<\/code> (that is contained in the <code>&lt;dialog&gt;<\/code> code above lets screen readers know the dialog contains a separate document inside of it and will read things appropriately to the user. More inforamation can be found on <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/Accessibility\/ARIA\/Roles\/Document_Role\">Mozilla&#8217;s documentation on the document role<\/a>.<\/li>\n<\/li>\n<\/ol>\n<h2>Applying Focus on Modal Open<\/h2>\n<p>The <a href=\"https:\/\/www.w3.org\/TR\/wai-aria\/\">WAI-ARIA<\/a> spec states that &#8220;When first opened, focus should be set to the first focusable element within the dialog&#8221;.  In a lot of cases, this would be the close button, which is usually an X at the top right of the modal itself, since it is the top-most element (which is nice, since if a user opens the modal by accident, they can easily close it immediately by pressing Enter on the keyboard).<\/p>\n<p>Native implementations of the HTML5 &lt;dialog&gt; element (as well the polyfill) will do this for free.  If you are using any other implementation, you must implement this yourself using <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/HTMLElement\/focus\">the JavaScript <code>focus()<\/code> method<\/a>.<\/p>\n<h2>Applying Focus On Modal Close<\/h2>\n<p>Best practice states that when a modal dialog is closed, focus should go back the element that opened it.  This is something that doesn&#8217;t happen in both the current native implementations of <code>&lt;dialog&gt;<\/code>, nor in the polyfill.  However, <a href=\"https:\/\/gist.github.com\/samthor\/babe9fad4a65625b301ba482dad284d1\">this gist adds this functionality<\/a> to both, so I added it to my version of the code.<\/p>\n<h2>Limiting Focus To An Open Modal<\/h2>\n<p>Native implementations of the HTML5 &lt;dialog&gt; element will do this for free, due to it being part of the spec.  The polyfill partially implements this, so I have included notes on where it fails and how to fix the issues.<\/p>\n<p>In many cases, developers will implement focus state management for desktop devices, but not for mobile ones.  You&#8217;d think you wouldn&#8217;t need to worry about two different ways to do this, but its the way the world is at the moment, so let&#8217;s talk about how to deal with it.<\/p>\n<h3 id=\"how-to-fix-desktop-focus\">Desktop<\/h3>\n<p>For desktop devices, <strong>keyboard focus must never go outside the modal<\/strong>.  Native HTML5 &lt;dialog&gt; implementations, as well as the polyfill, do this for free.  <\/p>\n<div class=\"importantNotes\">\n<p>If you are using some other modal dialog framework, you must implement this yourself if the framework doesn&#8217;t.   One way to do this is using my own <a href=\"https:\/\/github.com\/zoltan-dulac\/accessibility.js\">accessibility.js<\/a> library&#8217;s <code>setKeepFocusInside()<\/code>.  More information is available on <a href=\"https:\/\/github.com\/zoltan-dulac\/accessibility.js\">the accessibility.js GitHub repo<\/a>.<\/p>\n<p>It is also possible to do this with the proposed <a href=\"\">HTML5 <code>inert<\/code><\/a> attribute.  As of this writing, no browser supports this attribute, but there is a <a href=\"https:\/\/github.com\/GoogleChrome\/inert-polyfill\">inert polyfill made by Google<\/a> as well as <a href=\"https:\/\/github.com\/WICG\/inert\">one by WICG<\/a>.  I have not used either of these yet, so <abbr title=\"your mileage may vary\">YMMV <span class=\"visually-hidden\">(Your mileage may vary)<\/span><\/abbr>.<\/p>\n<\/div>\n<h3 id=\"how-to-fix-mobile-focus\">Mobile<\/h3>\n<p>This is the part that usually trips a lot of developers.  As a matter of fact, many commonly used modal frameworks, like <a href=\"https:\/\/getbootstrap.com\/docs\/4.0\/components\/modal\/\">Bootstrap&#8217;s Modal Dialog<\/a>, limit focus correctly on dekstop devices but not on mobile ones.   This is because <code>blur<\/code> and <code>focus<\/code> events, which are commonly used to limiting focus to the modal on desktop, don&#8217;t fire on mobile devices when using a screen-reader like iOS&#8217;s Voiceover or Android&#8217;s Talkback.  Mobile screenreaders have an idea of <strong>accessibility focus<\/strong>, which is not the same as keyboard focus.  Keyboard focus (on desktop computers) allows focus to occur on interactive elements (links, buttons, form elements, and DOM elements with `tabindex` that is anything other than -1.  Accessible focus on mobile devices, on the other hand, allows &#8220;focus&#8221; on any element on a page, including those which have a tabindex of -1.  <\/p>\n<p>So what is a user to do?  <a href=\"https:\/\/www.linkedin.com\/in\/rahulkumar14\/\">Rahul Kumar<\/a> mentions in his Medium article <a href=\"https:\/\/medium.com\/@im_rahul\/focus-trapping-looping-b3ee658e5177\">Focus Trapping for Accessibility (A11Y)<\/a> that developers can ensure accessibility focus will not be applied on elements outside the modal by setting their <code>aria-hidden<\/code> attribute to <code>\"true\"<\/code>.  Since he handwaves how how this should work in a general sense, I had to work out this pseudo-code to do this:<\/p>\n<p><div id=\"attachment_7639\" style=\"width: 278px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.useragentman.com\/blog\/wp-content\/uploads\/2019\/01\/algorithm.jpg\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-7639\" src=\"https:\/\/www.useragentman.com\/blog\/wp-content\/uploads\/2019\/01\/algorithm-268x300.jpg\" alt=\"Picture of a whiteboard with pseudocode on it written in calligraphy.\" width=\"268\" height=\"300\" class=\"size-medium wp-image-7639\" srcset=\"https:\/\/www.useragentman.com\/blog\/wp-content\/uploads\/2019\/01\/algorithm-268x300.jpg 268w, https:\/\/www.useragentman.com\/blog\/wp-content\/uploads\/2019\/01\/algorithm.jpg 640w\" sizes=\"auto, (max-width: 268px) 100vw, 268px\" \/><\/a><p id=\"caption-attachment-7639\" class=\"wp-caption-text\">My sketch of the algorithm that is needed to ensure focus stays in modals in mobile devices.<\/p><\/div><\/p>\n<p>I then translated this into JavaScript<\/p>\n<blockquote class=\"code\">\n<pre>\r\n\/**\r\n * This ensures that a mobile devices \"accessibilityFocus\"\r\n * (which is independant from a browser focus) cannot\r\n * go outside an element, by ensuring\r\n * the least amount of nodes outside the modal are\r\n * marked with aria-hidden=\"true\".\r\n *\r\n * @param {HTMLElement} el - the element that will have the loop.\r\n *\/\r\nfunction setMobileFocusLoop(el) {\r\n  const { body } = document;\r\n  let currentEl = el;\r\n\r\n  do {\r\n    \/\/ for every sibling of currentElement, we mark with\r\n    \/\/ aria-hidden=\"true\".\r\n    const siblings = currentEl.parentNode.childNodes;\r\n    for (let i = 0; i &lt; siblings.length; i++) {\r\n      const sibling = siblings[i];\r\n      if (sibling !== currentEl &amp;&amp; sibling.setAttribute) {\r\n        sibling.setAttribute('data-old-aria-hidden', sibling.ariaHidden || 'null');\r\n        sibling.setAttribute('aria-hidden', 'true');\r\n      }\r\n    }\r\n\r\n    \/\/ we then set the currentEl to be the parent node\r\n    \/\/ and repeat (unless the currentNode is the body tag).\r\n    currentEl = currentEl.parentNode;\r\n  } while (currentEl !== body);\r\n}\r\n<\/pre>\n<\/blockquote>\n<p>Note that I set the <code>data-old-aria-hidden<\/code> attribute to the previous value of the <code>aria-hidden<\/code> attribute (just in case it was set to something before this function was executed).  This data- attribute is also used when undoing all of this when the modal is closed:<\/p>\n<blockquote class=\"code\">\n<pre>\r\n\/**\r\n * reset all the nodes that have been marked as aria-hidden=\"true\"\r\n * in the setMobileFocusLoop() method back to their original\r\n * aria-hidden values.\r\n *\/\r\nfunction removeMobileFocusLoop() {\r\n  const elsToReset = document.querySelectorAll('[\"data-old-aria-hidden\"]');\r\n\r\n  for (let i = 0; i &lt; elsToReset.length; i++) {\r\n    const el = elsToReset[i];\r\n    const ariaHiddenVal = el.getAttribute('data-old-aria-hidden');\r\n    if (ariaHiddenVal === 'null') {\r\n      el.removeAttribute('aria-hidden');\r\n    } else {\r\n      el.setAttribute('aria-hidden', ariaHiddenVal);\r\n    }\r\n    el.removeAttribute('data-old-aria-hidden');\r\n  }\r\n}\r\n\r\n<\/pre>\n<\/blockquote>\n<p>Note that there is a (much easier) alternative to doing this: if you set <code>aria-modal=\"true\"<\/code> on the <code>dialog<\/code> element, user agents are <em>supposed<\/em> to restrict focus to the it.  Unfortunately, support for <code>aria-modal<\/code> is spotty (if supported at all), so for now, it is important to use the above solution until support reaches critical mass. <\/p>\n<h2>What Can I Use To Make My Existing Modal Dialog Code Accessible<\/h2>\n<p>Even though I made only one particular modal library accessible, the thought process of how to do so is written here, you just have to implement it in the one you are using!  A lot of the heavy lifting for my solution was incorporated in <a href=\"https:\/\/github.com\/zoltan-dulac\/accessibility.js\">accessibility.js<\/a>, so feel free to use it, or just steal the parts of the code you need from it.<\/p>\n<p>If you have made a modal library accessible using the information here, please feel free to share your work in the comments below.  I would love to see if this blog post has helped anyone.<\/p>\n<h2>Acknowledgments<\/h2>\n<ul>\n<li>The image used in the demo at the top of this post: English Fairy Tales, Jacobs, J., 1895 New York : Grosset &#038; Dunlap (2nd edition?) Boston Public Lib., obtained from <a href=\"https:\/\/commons.wikimedia.org\/wiki\/File:Page_8_illustration_in_English_Fairy_Tales.png\">Wikimedia Commons<\/a>.<\/li>\n<li><a href=\"https:\/\/github.com\/GoogleChrome\/dialog-polyfill\">The original <code>&lt;dialog&gt;<\/code> polyfill<\/a> was coded by GitHub user &#8220;GoogleChrome&#8221; (I assume this is an account owned by Google, although I&#8217;m not really sure to be honest).  I added extra code from <a href=\"https:\/\/gist.github.com\/samthor\/babe9fad4a65625b301ba482dad284d1\">this Gist by <a href=\"https:\/\/gist.github.com\/samthor\">Sam Thorogood<\/a> which returns focus the button that opened the dialog after the modal dialog is closed.  I also added the mobile specific code to this gist and <a href=\"https:\/\/github.com\/zoltan-dulac\/accessible-html5-dialog-polyfill\">published the result as a fork of the original polyfill<\/a>.<\/li>\n<\/ul>\n\r\n<!--\r\n<script src=\"\/tests\/accessible-html5-dialog-demo\/js\/accessible-html5-dialog.js\"><\/script>\r\n<script src=\"\/tests\/accessible-html5-dialog-demo\/js\/dialog-example.js\"><\/script>\r\n-->\r\n\r\n<script type=\"module\">\r\nimport enableDialog from \"\/enable\/js\/modules\/enable-dialog.js\";\r\n\r\n\/\/ Button that opens the dialog\r\nconst updateButton = document.getElementById('updateDetails');\r\n\r\n\/\/ Clicking this button opens the dialog\r\nupdateButton.addEventListener('click', function() {\r\n  favDialog.showModal();\r\n});\r\n\r\n\/\/ The modal's cancel button\r\nconst cancelButton = document.getElementById('cancel');\r\n\r\n\/\/ Clicking the cancel button will close the dialog\r\ncancelButton.addEventListener('click', function() {\r\n  favDialog.close();\r\n});\r\n\r\n\/\/ The <dialog> element itself\r\nconst favDialog = document.getElementById('favDialog');\r\n\r\n\r\nenableDialog.init();\r\n<\/script>\r\n\r\n<script src=\"\/shared\/js\/textZoomEvent.js\"><\/script>\r\n<script>\r\nfunction setCssTextZoomFactor() {\r\n  \/\/root.style.setProperty('--text-zoom-factor', textZoomEvent.resizeFactor());\r\n  console.log('resizeFactor', textZoomEvent.resizeFactor());\r\n  if (textZoomEvent.resizeFactor() > 1.3125) {\r\n    document.body.classList.add('zoom-over-133');\r\n  } else {\r\n    document.body.classList.remove('zoom-over-133');\r\n  }\r\n}\r\n\/\/ It is better if you give this the value of\r\n\/\/ parseFloat(getComputedStyle(document.documentElement).fontSize\r\n\/\/ when the doc is not zoomed.\r\ntextZoomEvent.init(16);\r\nsetCssTextZoomFactor();\r\ndocument.addEventListener('textzoom', setCssTextZoomFactor);\r\n<\/script>","protected":false},"excerpt":{"rendered":"<p><img decoding=\"async\" alt=\"\" role=\"presentation\" src=\"\/blog\/wp-content\/uploads\/2019\/03\/thumb.jpg\" \/> Accessible modals aren\u2019t hard to make, and you can make your modal dialogs accessible if you keep four simple requirements.  Implementing them can be done easily by using the code examples in this blog post, where I use  the HTML5 <code>&lt;dialog&gt;<\/code> polyfill to do so.  Read this article, and you can do the same with any modal dialog library\/framework that you use today.  This post includes a working demo and a video outlining how it works using both mobile and desktop screen readers.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[191,234,35,235,1],"tags":[228,238,236,211,237,216,214,239,217],"class_list":["post-7603","post","type-post","status-publish","format-standard","hentry","category-accessibility","category-dialog","category-html5","category-modal-role","category-uncategorized","tag-accessibility","tag-dialog","tag-html5","tag-keyboard-accessibility","tag-modal","tag-nvda","tag-screen-reader","tag-talkback","tag-voiceover"],"_links":{"self":[{"href":"https:\/\/www.useragentman.com\/blog\/wp-json\/wp\/v2\/posts\/7603","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.useragentman.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.useragentman.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.useragentman.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.useragentman.com\/blog\/wp-json\/wp\/v2\/comments?post=7603"}],"version-history":[{"count":103,"href":"https:\/\/www.useragentman.com\/blog\/wp-json\/wp\/v2\/posts\/7603\/revisions"}],"predecessor-version":[{"id":8048,"href":"https:\/\/www.useragentman.com\/blog\/wp-json\/wp\/v2\/posts\/7603\/revisions\/8048"}],"wp:attachment":[{"href":"https:\/\/www.useragentman.com\/blog\/wp-json\/wp\/v2\/media?parent=7603"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.useragentman.com\/blog\/wp-json\/wp\/v2\/categories?post=7603"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.useragentman.com\/blog\/wp-json\/wp\/v2\/tags?post=7603"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}