]> git.agnieray.net Git - galette.git/commitdiff
Improvements for accessibility and screen readers
authorGuillaume AGNIERAY <dev@agnieray.net>
Mon, 20 Nov 2023 22:08:01 +0000 (23:08 +0100)
committerJohan Cwiklinski <johan@x-tnd.be>
Tue, 21 Nov 2023 06:28:40 +0000 (07:28 +0100)
refs #1735

- add a skip to main content link
- new visually-hidden CSS class for screen readers
- improve semantic structure with <nav>, <main> and <footer>
- remove "Existing email" button

galette/templates/default/elements/footer.html.twig
galette/templates/default/elements/logged_user.html.twig
galette/templates/default/elements/navigation/navigation_aside.html.twig
galette/templates/default/elements/navigation/navigation_sidebar.html.twig
galette/templates/default/elements/navigation/navigation_topbar.html.twig
galette/templates/default/page.html.twig
galette/templates/default/public_page.html.twig
galette/webroot/installer.php
ui/semantic/galette/globals/site.overrides

index ae1df0aea281644060c34bfa12e097c9d9c0596a..ebc05aa3cfb1e394921095f91294468cc978a4cc 100644 (file)
@@ -1,6 +1,6 @@
-        <footer class="ui basic center aligned segment">
+        <footer class="ui basic center aligned segment{% if login.getCompactMenu() %} extended{% endif %}">
             <div class="row">
-                <nav class="ui horizontal bulleted link list">
+                <div class="ui horizontal bulleted link list">
                     <a href="https://galette.eu" class="item">
                         <i class="icon globe europe" aria-hidden="true"></i>
                         {{ _T("Website") }}
                         <i class="icon mastodon" aria-hidden="true"></i>
                         @galette
                     </a>
-                </nav>
+                </div>
             </div>
             <div class="row">
-                <nav class="ui horizontal bulleted link list">
+                <div class="ui horizontal bulleted link list">
                     <a id="copyright" href="https://galette.eu/" class="item">
                         <i class="icon cookie bite" aria-hidden="true"></i>
                         Galette {{ constant('GALETTE_DISPLAY_VERSION') }}
@@ -27,7 +27,7 @@
                         {{ _T("System information") }}
                     </a>
 {% endif %}
-                </nav>
+                </div>
 {% if login.isLogged() and cur_route == 'dashboard' %}
                 {# Comply with the Twemoji project's requirements about attribution #}
                 <!--
index 92275b8a185edc9e7868174d445d2c8ffebad51e..3ff71dfc59288eedd1dafd0aeb2fef5735c753ce 100644 (file)
@@ -28,7 +28,7 @@
         </div>
     {% else %}
         {% if not login.getCompactMenu() %}
-            <div class="{{ component_classes }}">
+            <nav class="{{ component_classes }}" aria-label="{{ _T('User menu') }}">
                 <div class="ui item">
                     <i class="user circle big icon" aria-hidden="true"></i>
                     {{ login.loggedInAs()|raw }}
@@ -42,7 +42,7 @@
                                 title="{% if login.isDarkModeEnabled() %}{{ _T("Disable dark mode") }}{% else %}{{ _T("Enable dark mode") }}{% endif %}"
                             >
                                 <i class="icon adjust" aria-hidden="true"></i>
-                                <span class="displaynone">{% if login.isDarkModeEnabled() %}{{ _T("Disable dark mode") }}{% else %}{{ _T("Enable dark mode") }}{% endif %}</span>
+                                <span class="visually-hidden">{% if login.isDarkModeEnabled() %}{{ _T("Disable dark mode") }}{% else %}{{ _T("Enable dark mode") }}{% endif %}</span>
                             </a>
                             <a
                                 class="ui {% if login.isImpersonated() %}purple{% else %}red{% endif %} icon button"
                                 data-position="bottom right"
                             >
                                 <i class="icon {% if login.isImpersonated() %}user secret{% else %}sign out alt{% endif %}" aria-hidden="true"></i>
-                                <span class="displaynone">{% if login.isImpersonated() %}{{ _T("Unimpersonate") }}{% else %}{{ _T("Log off") }}{% endif %}</span>
+                                <span class="visually-hidden">{% if login.isImpersonated() %}{{ _T("Unimpersonate") }}{% else %}{{ _T("Log off") }}{% endif %}</span>
                             </a>
                         </div>
                     </div>
                 </div>
-            </div>
+            </nav>
             {% include "elements/modes.html.twig" %}
         {% else %}
             {% set component_classes = "ui vertical centered tiny icon fluid menu" %}
-            <div id="logoutmenu" class="{{ component_classes }}">
+            <nav id="logoutmenu" class="{{ component_classes }}" aria-label="{{ _T('User menu') }}">
                 <div class="ui dropdown navigation item no-touch tooltip" data-html="{{ login.loggedInAs()|raw }}" data-position="right center">
                     <i class="user circle icon" aria-hidden="true"></i>
-                    <span class="displaynone">{{ login.loggedInAs()|raw }}</span>
+                    <span class="visually-hidden">{{ login.loggedInAs()|raw }}</span>
                     <i class="dropdown icon" aria-hidden="true"></i>
                     <div class="menu">
                         <div class="item">
@@ -88,7 +88,7 @@
                     data-position="right center"
                 >
                     <i class="icon adjust" aria-hidden="true"></i>
-                    <span class="displaynone">{% if login.isDarkModeEnabled() %}{{ _T("Disable dark mode") }}{% else %}{{ _T("Enable dark mode") }}{% endif %}</span>
+                    <span class="visually-hidden">{% if login.isDarkModeEnabled() %}{{ _T("Disable dark mode") }}{% else %}{{ _T("Enable dark mode") }}{% endif %}</span>
                 </a>
                 <a
                     class="item{% if login.isImpersonated() %} purple{% else %} red{% endif %}"
@@ -97,9 +97,9 @@
                     data-position="right center"
                 >
                     <i class="icon {% if login.isImpersonated() %}user secret{% else %}sign out alt{% endif %}" aria-hidden="true"></i>
-                    <span class="displaynone">{% if login.isImpersonated() %}{{ _T("Unimpersonate") }}{% else %}{{ _T("Log off") }}{% endif %}</span>
+                    <span class="visually-hidden">{% if login.isImpersonated() %}{{ _T("Unimpersonate") }}{% else %}{{ _T("Log off") }}{% endif %}</span>
                 </a>
-            </div>
+            </nav>
         {% endif %}
     {% endif %}
 {% endif %}
index 67a6068cf8b047489f1b368172db4bb2dd5c6fd1..53dbf0b0bbe25122a3cd4a78913ea7bece40b323 100644 (file)
@@ -1,4 +1,4 @@
-<aside id="sidemenu" class="ui computer only toc{% if login.getCompactMenu() %} compact_menu{% endif %}">
+<aside id="sidemenu" class="ui computer toc{% if login.getCompactMenu() %} compact_menu{% endif %}">
     {% include "elements/logged_user.html.twig" with {
             ui: "menu"
     } %}
     </div>
 {% endif %}
 
-    <div class="ui vertical{% if not login.getCompactMenu() %} accordion compact{% else %} tiny icon{% endif %} fluid menu">
+    <nav class="ui vertical{% if not login.getCompactMenu() %} accordion compact{% else %} tiny icon{% endif %} fluid menu" aria-label="{{ _T('Main menu') }}">
         {% set mode = login.getCompactMenu() ? "compact" : "default" %}
         {% include "elements/navigation/navigation_items.html.twig" with {
                 mode: mode
         } %}
-    </div>
+    </nav>
 
     <div class="ui basic fitted segment">
         <div class="ui toggle mini checkbox">
index cfcd65a2052573301674785a0f981762d581a72d..d19c5c246e16c612568bc898d6c31a6100f7de65 100644 (file)
@@ -1,4 +1,4 @@
-    <div id="sidebarmenu" class="ui simple left vertical accordion menu sidebar">
+    <nav id="sidebarmenu" class="ui simple left vertical accordion menu sidebar">
 {% if not login.isLogged() %}
     {% if cur_route != "login" %}
         {% import "macros.twig" as menus_macros %}
@@ -32,4 +32,4 @@
         <div class="ui basic center aligned segment">
             {% include "elements/modes.html.twig" %}
         </div>
-    </div>
+    </nav>
index 81a021799f8c5f3283fcc47c3d753a1a36865c52..9fa414127e8fbcf4e2650e9314a7506ddf22725a 100644 (file)
@@ -1,26 +1,13 @@
 <header id="top-navbar" class="ui top fixed menu">
-    <div class="ui fluid container">
-        <a class="toc item" tabindex="0">
-            <i class="sidebar icon" aria-hidden="true"></i>
-            <span class="displaynone">{{ _T("Menu") }}</span>
-        </a>
-        {% if login.isLogged() and (cur_route != 'mailing' and existing_mailing == true) %}
-            <a
-                id="recup_mailing"
-                href="{{ url_for("mailing") }}"
-                class="ui blue tooltip item"
-                title="{{ _T("A mailing exists in the current session. Click here if you want to resume or cancel it.") }}"
-                data-position="bottom right"
-            >
-                <i class="paper plane outline blue icon" aria-hidden="true"></i>
-                <span class="displaynone">{{ _T("Existing mailing") }}</span>
-            </a>
-        {% endif %}
-        <div class="header item">
-            <img src="{{ url_for("logo") }}" width="{{ logo.getOptimalWidth() }}" height="{{ logo.getOptimalHeight() }}" alt="[ Galette ]" class="logo" />
-            <span>{{ preferences.pref_nom }}</span>
-        </div>
-
+    <a class="toc item" tabindex="0">
+        <i class="sidebar icon" aria-hidden="true"></i>
+        <span class="visually-hidden">{{ _T("Menu") }}</span>
+    </a>
+    <div class="header item">
+        <img src="{{ url_for("logo") }}" width="{{ logo.getOptimalWidth() }}" height="{{ logo.getOptimalHeight() }}" alt="[ Galette ]" class="logo" />
+        <span>{{ preferences.pref_nom }}</span>
+    </div>
+    <nav class="ui fluid container">
         {% include "elements/navigation/public_pages.html.twig" with {
                 tips_position: "bottom center",
                 sign_in: true,
@@ -37,5 +24,5 @@
                 {% include "elements/modes.html.twig" %}
             </div>
         {% endif %}
-    </div>
+    </nav>
 </header>
index d65b64e99d1c687400a188d568a5bac6eb4d5570..9c6ee07fa46786358474ba8b47c3657b68d29ec6 100644 (file)
@@ -7,9 +7,10 @@
         } %}
     </head>
     <body id="galette_body" class="pushable dimmable{% if login.isLogged() %} loggedin{% endif %} nojs">
+        <a href="#main-content" class="skiptocontent visually-hidden focusable">{{ _T("Skip to content") }}</a>
         {% include 'elements/navigation/navigation_sidebar.html.twig' %}
         {% include 'elements/navigation/navigation_topbar.html.twig' %}
-        <div class="pusher">
+        <main class="pusher">
             <div id="main" class="{% if not login.isLogged() %}container{% else %} full height{% if i18n.isRtl() %} rtl{% endif %}{% endif %}">
 {%  if login.isLogged() %}
     {% include 'elements/navigation/navigation_aside.html.twig' %}
                             <div class="sub header">{% if preferences.pref_slogan %}{{ __(preferences.pref_slogan) }}{% endif %}</div>
                         </div>
                     </div>
+                    <a id="main-content" tabindex="-1"></a>
                     <h1 class="ui block center aligned header" style="position: relative">
 {% else %}
                     <div class="ui horizontal basic segments header">
+                        <div class="ui right aligned compact segment">
+                            <nav class="ui compact tiny menu" aria-label="{{ _T('Language menu') }}">
+                                {% include "elements/language.html.twig" with {
+                                        ui: "dropdown"
+                                } %}
+                            </nav>
+                        </div>
+                        <a id="main-content" tabindex="-1"></a>
                         <div class="ui center aligned segment">
                             <h1>
 {% endif %}
                             </h1>
 {% if login.isLogged() %}
                         </div>
-                        <div class="ui right aligned compact segment">
-                            <div class="ui compact tiny menu">
-            {% if cur_route != 'mailing' and existing_mailing == true %}
-                                <a
-                                    id="recup_mailing"
-                                    href="{{ url_for("mailing") }}"
-                                    class="ui blue tooltip item"
-                                    title="{{ _T("A mailing exists in the current session. Click here if you want to resume or cancel it.") }}"
-                                    data-position="bottom right"
-                                >
-                                    <i class="paper plane outline blue icon" aria-hidden="true"></i>
-                                    <span class="displaynone">{{ _T("Existing mailing") }}</span>
-                                </a>
-            {% endif %}
-                                {% include "elements/language.html.twig" with {
-                                        ui: "dropdown"
-                                } %}
-                            </div>
-                        </div>
                     </div>
 {% endif %}
                     <div class="main-content">
                         {% include "elements/messages_inline.html.twig" %}
                         {% block content %}{{ _T('Page content') }}{% endblock %}
                     </div>
-                    {% include "elements/footer.html.twig" %}
                 </section>
             </div>
-        </div>
+        </main>
+        {% include "elements/footer.html.twig" %}
         <a href="#" id="back2top" class="circular big ui icon button" title="{{ _T("Back to top") }}">
             <i class="arrow up icon" aria-hidden="true"></i>
         </a>
index 91a3d317154905a5d83352bdfbdd55e7af1574b6..2a7ca0618e2345e1dba9d158ad393dc4ad41f42d 100644 (file)
@@ -7,9 +7,10 @@
         } %}
     </head>
     <body class="{% if body_class is defined and body_class == "front_page" %}front-page {% endif %}pushable{% if login.isLogged() %} loggedin{% endif %} nojs">
+        <a href="#main-content" class="skiptocontent visually-hidden focusable">{{ _T("Skip to content") }}</a>
         {% include 'elements/navigation/navigation_sidebar.html.twig' %}
         {% include 'elements/navigation/navigation_topbar.html.twig' %}
-        <div class="pusher">
+        <main class="pusher">
             <div id="main" class="{% if cur_route == "login" or cur_route == "password-lost" or cur_route == "password-recovery" or cur_route == "directlink" %}text {% endif %}{% if not login.isLogged() %}ui container{% else %}full height{% if i18n.isRtl() %} rtl{% endif %}{% endif %}">
 {% if login.isLogged() %}
                 {% include "elements/navigation/navigation_aside.html.twig" %}
                             <div class="sub header">{% if preferences.pref_slogan %}{{ __(preferences.pref_slogan) }}{% endif %}</div>
                         </div>
                     </div>
+                    <a id="main-content" tabindex="-1"></a>
                     <h1 class="ui block center aligned header" style="position: relative">
 {% else %}
                     <div class="ui horizontal basic segments header">
+                        <a id="main-content" tabindex="-1"></a>
                         <div class="ui center aligned segment">
                             <h1>
 {% endif %}
                         {% include "elements/messages_inline.html.twig" %}
                         {% block content %}{{ _T("Public page content") }}{% endblock %}
                     </div>
-                    {% include "elements/footer.html.twig" %}
                 </section>
             </div>
-        </div>
+        </main>
+        {% include "elements/footer.html.twig" %}
         <a href="#" id="back2top" class="circular big ui icon button" title="{{ _T("Back to top") }}">
             <i class="arrow up icon" aria-hidden="true"></i>
         </a>
index c9a4e08e7f65078b64817973c780aa4e0937f27c..c38c3ab597aa4f80cc8e5265a13445edcd377e01 100644 (file)
@@ -246,6 +246,7 @@ header('Content-Type: text/html; charset=UTF-8');
         <script type="text/javascript" src="./assets/js/jquery.min.js"></script>
     </head>
     <body class="pushable">
+        <a href="#main-content" class="skiptocontent visually-hidden focusable"><?php echo _T("Skip to content"); ?></a>
         <header id="top-navbar" class="ui fixed menu bgcolor">
             <div class="ui wide container">
                 <div class="header item">
@@ -267,12 +268,13 @@ foreach ($i18n->getList() as $langue) {
                 </div>
             </div>
         </header>
-        <div class="pusher">
-            <div id="main" class="ui wide container">
+        <main class="pusher">
+            <section id="main" class="ui wide container">
                 <div class="ui basic segment">
                     <div class="ui basic center aligned fitted segment">
                         <img class="icon" alt="[ Galette ]" src="./themes/default/images/galette.png"/>
                     </div>
+                    <a id="main-content" tabindex="-1"></a>
                     <h1 class="ui block center aligned header">
                         <?php echo $install->getStepTitle(); ?>
                     </h1>
@@ -432,8 +434,8 @@ if ($install->isCheckStep()) {
                         </nav>
                     </div>
                 </footer>
-            </div>
-        </div>
+            </section>
+        </main>
         <script type="text/javascript" src="./assets/js/galette-main.bundle.min.js"></script>
         <script type="text/javascript" src="./themes/default/ui/semantic.min.js"></script>
     </body>
index e3453f1915f7c3bf544cc4dd433902660f90bfa3..30d4e003b0861f9e0e566774b3c5605917b41dff 100644 (file)
@@ -5,10 +5,30 @@
 /*---------------
      Global
 ----------------*/
+/* Content using displaynone is ignored by screen readers */
 .displaynone {
   display: none !important;
 }
 
+/* Content using visually-hidden is read by screen readers */
+.visually-hidden {
+  position: absolute !important;
+  width: 1px;
+  height: 1px;
+  clip: rect(1px,1px,1px,1px);
+  word-wrap: normal;
+  overflow: hidden;
+
+  &.focusable:active,
+  &.focusable:focus {
+      position: static !important;
+      width: auto;
+      height: auto;
+      clip: auto;
+      overflow: visible;
+  }
+}
+
 .exemple,
 .disabled,
 .disabled a {
@@ -41,6 +61,33 @@ a,
     }
 }
 
+/*---------------------------------
+     Go to main content link
+----------------------------------*/
+.skiptocontent {
+  display: block;
+  width: 100%;
+  text-decoration: none;
+  color: @invertedTextColor;
+  outline: 0;
+  background-color: @black;
+
+  &.focusable:hover,
+  &.focusable:focus {
+    position: absolute !important;
+    width: 100%;
+    z-index: 102;
+    padding: 1rem .5rem;
+    text-align: center;
+    outline: none;
+  }
+
+  &.focusable:hover {
+    color: @invertedTextColor;
+    text-decoration: underline;
+  }
+}
+
 /*--------------------------------
      Base layout and navigation
 ---------------------------------*/
@@ -53,6 +100,7 @@ html {
       flex-direction: column;
       justify-content: center;
       height: 100%;
+      padding-bottom: 120px;
 
       img.logo {
         margin-top: 1rem;
@@ -61,9 +109,16 @@ html {
   }
 }
 
-body.pushable:not(.loggedin) > .pusher {
+body.pushable:not(.loggedin) {
+  & > .pusher {
     display: flex;
     justify-content: center;
+    padding-bottom: 120px;
+  }
+  main ~ footer.ui.segment {
+    margin-top: -120px;
+    z-index: 2;
+  }
 }
 
 .loggedin #main {
@@ -114,6 +169,12 @@ aside .ui.vertical.menu {
   max-width: 100%;
 }
 
+.loggedin .content {
+  .ui.horizontal.segments.header {
+    flex-direction: row-reverse;
+  }
+}
+
 footer,
 footer .ui.horizontal.list .list > .item,
 footer .ui.horizontal.list > .item{
@@ -121,7 +182,7 @@ footer .ui.horizontal.list > .item{
 }
 
 @media only screen and (max-width: 991px) {
-  #top-navbar .item:not(.header):not(.toc):not(#recup_mailing) {
+  #top-navbar nav {
     display: none;
   }
   .sidebar.uncover + .fixed.menu .toc i::before {
@@ -143,6 +204,15 @@ footer .ui.horizontal.list > .item{
     padding-top: 0;
   }
 
+  #sidebarmenu {
+    display: none;
+  }
+  .ui.visible.left.sidebar ~ .pusher {
+    transform: translate3d(0, 0, 0) !important;
+    &.dimmed::after {
+      display: none;
+    }
+  }
   #top-navbar div.item a.button.darkmode span {
     display: none;
   }
@@ -207,6 +277,7 @@ footer .ui.horizontal.list > .item{
     display: block;
   }
 
+  html:not(.public_page) body:not(.front-page) main ~ footer.ui.segment,
   body:not(.front-page) section.content {
     margin-left: 260px;
     flex: 1 1 auto;
@@ -253,6 +324,7 @@ footer .ui.horizontal.list > .item{
     width: 350px;
   }
 
+  html:not(.public_page) body:not(.front-page) main ~ footer.ui.segment,
   body:not(.front-page) section.content {
     margin-left: 350px;
   }