{"id":4255,"date":"2017-10-13T16:31:05","date_gmt":"2017-10-13T14:31:05","guid":{"rendered":"https:\/\/florianbrinkmann.com\/en\/?p=4255"},"modified":"2020-02-09T10:59:43","modified_gmt":"2020-02-09T09:59:43","slug":"trap-focus","status":"publish","type":"post","link":"https:\/\/florianbrinkmann.com\/en\/trap-focus-4255\/","title":{"rendered":"Trap focus in specific website area"},"content":{"rendered":"<p>Sometimes it may be useful to trap the focus inside a specific area of the website so that no other focusable elements outside of this area can be reached (for example via using the tab key). Here I show you how to do that.<\/p>\n\n\n\n<!--more-->\n\n\n\n<h2 class=\"wp-block-heading\">The starting point<\/h2>\n\n\n\n<p>I searched for a solution for that problem while updating my Fanoe theme. The theme comes with an off-canvas sidebar, which is displayed above the other content since the update, instead of pushing the content away. You can see it in the following GIF:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><noscript><img decoding=\"async\" width=\"1034\" height=\"487\" src=\"https:\/\/florianbrinkmann.com\/en\/wp-content\/uploads\/sites\/11\/2017\/10\/fanoe-sidebar.gif\" alt class=\"wp-image-4256\"><\/noscript><img decoding=\"async\" width=\"1034\" height=\"487\" src=\"data:image\/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201034%20487%22%3E%3C%2Fsvg%3E\" alt class=\"wp-image-4256 lazyload\" data-src=\"https:\/\/florianbrinkmann.com\/en\/wp-content\/uploads\/sites\/11\/2017\/10\/fanoe-sidebar.gif\"><\/figure>\n\n\n\n<p>If the sidebar is displayed, the focus should be trapped inside it, so keyboard user cannot navigate to focusable elements which are not part of the sidebar until they close the sidebar via the button or Esc key.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Implementing of the solution with ally.js<\/h2>\n\n\n\n<p>The JS library <a href=\"https:\/\/allyjs.io\/\">ally.js<\/a> comes with a solution for the problem which is shown <a href=\"https:\/\/allyjs.io\/tutorials\/accessible-dialog.html\">in a tutorial<\/a>. The important markup inside my theme looks like that:<\/p>\n\n\n<pre class=\"wp-block-code lang-markup\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"sidebar-button -open\"<\/span>&gt;<\/span>\n\t<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">span<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"screen-reader-text\"<\/span>&gt;<\/span>Show Sidebar<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">span<\/span>&gt;<\/span>\n\t<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">span<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">\"true\"<\/span>&gt;<\/span>\u2261<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">span<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">aside<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"sidebar\"<\/span> <span class=\"hljs-attr\">role<\/span>=<span class=\"hljs-string\">\"sidebar\"<\/span>&gt;<\/span>\n\t<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"sidebar-button -close\"<\/span>&gt;<\/span>\n\t\t<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">span<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"screen-reader-text\"<\/span>&gt;<\/span>Close Sidebar<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">span<\/span>&gt;<\/span>\n\t\t<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">span<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">\"true\"<\/span>&gt;<\/span>\u2261<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">span<\/span>&gt;<\/span>\n\t<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n\t<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"sidebar-content\"<\/span>&gt;<\/span>\n\t\t<span class=\"hljs-comment\">&lt;!-- The widgets --&gt;<\/span>\n\t<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">aside<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And that is the important JS part (I use <a href=\"https:\/\/webpack.js.org\/\">Webpack<\/a> to run the <code class=\"lang-js\">import<\/code>, and <a href=\"https:\/\/babeljs.io\/\">Babel<\/a> to convert ES6 syntax to ES5 \u2013 I wrote a <a href=\"https:\/\/florianbrinkmann.com\/en\/4197\/webpack-and-babel\/\">post about setting that up<\/a>):<\/p>\n\n\n<pre class=\"wp-block-code lang-js\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> ally <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'ally.js\/ally'<\/span>;\n\n<span class=\"hljs-comment\">\/**\n * Custom JavaScript functions.\n *\n * @version 2.0.3\n *\n * @package Fanoe\n *\/<\/span>\n{\n\t<span class=\"hljs-comment\">\/**\n\t * Get the html element.\n\t *\n\t * <span class=\"hljs-doctag\">@type <span class=\"hljs-type\">{Element}<\/span><\/span>\n\t *\/<\/span>\n\t<span class=\"hljs-keyword\">const<\/span> root = <span class=\"hljs-built_in\">document<\/span>.documentElement;\n\n\t<span class=\"hljs-comment\">\/**\n\t * Get the elements for sidebar handling.\n\t *\/<\/span>\n\t<span class=\"hljs-keyword\">const<\/span> openButton = <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'.sidebar-button.-open'<\/span>);\n\t<span class=\"hljs-keyword\">const<\/span> closeButton = <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'.sidebar-button.-close'<\/span>);\n\t<span class=\"hljs-keyword\">const<\/span> sidebarElem = <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'.sidebar'<\/span>);\n\t<span class=\"hljs-keyword\">let<\/span> disabledHandle;\n\t<span class=\"hljs-keyword\">let<\/span> tabHandle;\n\t<span class=\"hljs-keyword\">let<\/span> keyHandle;\n\n\t<span class=\"hljs-comment\">\/**\n\t * Call sidebar function sidebar button click.\n\t *\/<\/span>\n\topenButton.addEventListener(<span class=\"hljs-string\">'click'<\/span>, sidebar, <span class=\"hljs-literal\">false<\/span>);\n\n\t<span class=\"hljs-comment\">\/**\n\t * Call sidebar function sidebar button click.\n\t *\/<\/span>\n\tcloseButton.addEventListener(<span class=\"hljs-string\">'click'<\/span>, sidebar, <span class=\"hljs-literal\">false<\/span>);\n\n\t<span class=\"hljs-comment\">\/**\n\t * Catch clicks on the document to close sidebar on mouse click\n\t * outside the open sidebar.\n\t *\/<\/span>\n\t<span class=\"hljs-built_in\">document<\/span>.body.addEventListener(<span class=\"hljs-string\">'click'<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> (<span class=\"hljs-params\">e<\/span>) <\/span>{\n\t\t<span class=\"hljs-comment\">\/**\n\t\t * Check if the sidebar is visible.\n\t\t *\/<\/span>\n\t\t<span class=\"hljs-keyword\">if<\/span> (root.classList.contains(<span class=\"hljs-string\">'active-sidebar'<\/span>)) {\n\t\t\t<span class=\"hljs-comment\">\/**\n\t\t\t * Check if the click was made on the .sidebar element, not the .sidebar-content.\n\t\t\t *\/<\/span>\n\t\t\t<span class=\"hljs-keyword\">if<\/span> (e.explicitOriginalTarget.classList.contains(<span class=\"hljs-string\">'sidebar'<\/span>)) {\n\t\t\t\tdisabledHandle.disengage();\n\t\t\t\ttabHandle.disengage();\n\t\t\t\troot.classList.remove(<span class=\"hljs-string\">'active-sidebar'<\/span>);\n\t\t\t}\n\t\t}\n\t}, <span class=\"hljs-literal\">false<\/span>);\n\n\t<span class=\"hljs-comment\">\/**\n\t * Function to display and hide sidebar.\n\t *\n\t * @link https:\/\/allyjs.io\/tutorials\/accessible-dialog.html\n\t *\/<\/span>\n\t<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">sidebar<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n\t\t<span class=\"hljs-comment\">\/**\n\t\t * Toggle .active-sidebar class.\n\t\t *\/<\/span>\n\t\troot.classList.toggle(<span class=\"hljs-string\">'active-sidebar'<\/span>);\n\n\t\t<span class=\"hljs-comment\">\/**\n\t\t * Check for class name to know if the\n\t\t * sidebar is currently visible or not.\n\t\t *\/<\/span>\n\t\t<span class=\"hljs-keyword\">if<\/span> (root.classList.contains(<span class=\"hljs-string\">'active-sidebar'<\/span>)) {\n\t\t\tdisabledHandle = ally.maintain.disabled({\n\t\t\t\t<span class=\"hljs-attr\">filter<\/span>: sidebarElem,\n\t\t\t});\n\n\t\t\ttabHandle = ally.maintain.tabFocus({\n\t\t\t\t<span class=\"hljs-attr\">context<\/span>: sidebarElem,\n\t\t\t});\n\n\t\t\tkeyHandle = ally.when.key({\n\t\t\t\t<span class=\"hljs-attr\">escape<\/span>: closeSidebarByKey,\n\t\t\t});\n\t\t} <span class=\"hljs-keyword\">else<\/span> {\n\t\t\tdisabledHandle.disengage();\n\t\t\ttabHandle.disengage();\n\t\t}\n\t}\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The code is strongly oriented on the tutorial which I linked above. First, we import the ally lib, get the root element and save the buttons and the sidebar element. After that, we add the needed EventListeners:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>Click on the buttons. This runs the <code class=\"lang-js\">sidebar()<\/code> function, which displays or hides the sidebar.<\/li><li>Click on the <code class=\"lang-markup\">body<\/code> element. After that, we check if the sidebar is currently visible. If so, we test if the user clicked the <code class=\"lang-markup\">.sidebar<\/code> element. If that is the case, the click was done on the greyed-out area outside of the sidebar content (that is because the grey area was created with a <code class=\"lang-css\">.sidebar::before<\/code> rule).<\/li><\/ul>\n\n\n\n<p>In <code class=\"lang-js\">sidebar()<\/code>, we toggle the <code class=\"lang-markup\">active-sidebar<\/code> class on the root element. After that, we check if the class is set (so the sidebar was displayed by the button click). In that case, <code class=\"lang-js\">ally.maintain.disabled<\/code> disables all elements outside the sidebar for being focused. With <code class=\"lang-js\">ally.maintain.tabFocus<\/code> we set the tab focus on the sidebar element, so the user does not jump to the browser UI after the last focusable element in the sidebar but to the first focus element of the sidebar.<\/p>\n\n\n\n<p>Finally, we define that pressing the Esc key runs the <code class=\"lang-js\">sidebar()<\/code> function to close the sidebar. If the <code class=\"lang-markup\">active-sidebar<\/code> class is not set, we remove all the restrictions.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Sometimes it may be useful to trap the focus inside a specific area of the website so that no other focusable elements outside of this area can be reached (for example via using the tab key). Here I show you how to do that.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"wpf_show_in_dewp_planet_feed":false,"flobn_post_versions":"","webmentions_disabled_pings":false,"webmentions_disabled":false,"lazy_load_responsive_images_disabled":false,"footnotes":""},"categories":[6],"tags":[],"class_list":["post-4255","post","type-post","status-publish","format-standard","hentry","category-web-development"],"wp-worthy-pixel":{"ignored":false,"public":"633c37d6957449eaa8ade5e26bf4bbfd","server":"vg07.met.vgwort.de","url":"https:\/\/vg07.met.vgwort.de\/na\/633c37d6957449eaa8ade5e26bf4bbfd"},"wp-worthy-type":"normal","_links":{"self":[{"href":"https:\/\/florianbrinkmann.com\/en\/wp-json\/wp\/v2\/posts\/4255","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/florianbrinkmann.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/florianbrinkmann.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/florianbrinkmann.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/florianbrinkmann.com\/en\/wp-json\/wp\/v2\/comments?post=4255"}],"version-history":[{"count":2,"href":"https:\/\/florianbrinkmann.com\/en\/wp-json\/wp\/v2\/posts\/4255\/revisions"}],"predecessor-version":[{"id":5842,"href":"https:\/\/florianbrinkmann.com\/en\/wp-json\/wp\/v2\/posts\/4255\/revisions\/5842"}],"wp:attachment":[{"href":"https:\/\/florianbrinkmann.com\/en\/wp-json\/wp\/v2\/media?parent=4255"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/florianbrinkmann.com\/en\/wp-json\/wp\/v2\/categories?post=4255"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/florianbrinkmann.com\/en\/wp-json\/wp\/v2\/tags?post=4255"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}