Repository: fossunited/fossunited
Pull request #865: Accessibility in events_timeline
Author: login: Neeluiiitd
Labels: name: accessibility
Created: 2025-03-18T19:33:20Z | Merged: 2025-03-30T04:27:01Z


1. ARIA Enhancements & Accessibility Improvements
Role attributes and labels added to improve accessibility:
role="search" with aria-labelledby="search-panel-label" for the search panel.
aria-label and aria-describedby attributes added to inputs & checkboxes for better screen reader support.
Live regions (aria-live="polite") added to filtered results to inform users when results are updated.
Clear usage of sr-only class for screen-reader-only headings and descriptions.

2. Better Semantic HTML:
Grouped the event listings into role="region" and role="group" with clear aria-labelledby linking.
Headings (h2, h3, h4) used meaningfully for:
"Search and Filter Events"
Each group of events
Each month within the group
Helps assistive tech users understand structure better!

3. Input & Filter Panel Enhancements:
Search Input:
Added aria-label="Search events by name and date" for clarity.
Ensured label for attribute is linked properly.
Must Attend Checkbox:
Linked aria-describedby to an explanatory paragraph.
Improved keyboard focus styling for accessibility.

4. Dynamic Event Rendering Logic:
Cleaned up the event rendering logic in Vue:
Ensured v-if="searchText.length == 0" shows the default event list.
Separate block for filtered event cards when search text length > 0, using aria-live for real-time updates.



Diff:
diff --git a/dashboard/src/components/event/TicketTshirtInsightCard.vue b/dashboard/src/components/event/TicketTshirtInsightCard.vue
index 7ce4d4b17..c062e0801 100644
--- a/dashboard/src/components/event/TicketTshirtInsightCard.vue
+++ b/dashboard/src/components/event/TicketTshirtInsightCard.vue
@@ -5,26 +5,30 @@
     :options="{
       title: 'Sold T-shirt Details',
     }"
+    role="dialog"
+    aria-modal="true"
+    aria-labelledby="dialog-title"
   >
     <template #body-content>
       <div class="text-sm">
+        <h2 id="dialog-title" class="sr-only">Sold T-shirt Details</h2>
         <p class="mb-2">T-shirts sold for this event.</p>
       </div>
-      <table class="w-3/4 table-fixed text-center">
+      <table class="w-3/4 table-fixed text-center" role="table" aria-describedby="dialog-title">
         <thead class="text-xs text-gray-700 uppercase bg-gray-50">
-          <tr>
-            <th class="p-2 border">Size</th>
-            <th class="p-2 border">Quantity</th>
+          <tr role="row">
+            <th class="p-2 border" role="columnheader" scope="col">Size</th>
+            <th class="p-2 border" role="columnheader" scope="col">Quantity</th>
           </tr>
         </thead>
         <tbody>
-          <tr v-for="(quantity, size) in insight.tshirt_size_count" :key="size">
-            <td class="p-2 text-base border">{{ size }}</td>
-            <td class="p-2 text-base border">{{ quantity }}</td>
+          <tr v-for="(quantity, size) in insight.tshirt_size_count" :key="size" role="row">
+            <td class="p-2 text-base border" role="cell">{{ size }}</td>
+            <td class="p-2 text-base border" role="cell">{{ quantity }}</td>
           </tr>
-          <tr>
-            <td class="p-2 text-base font-semibold">Total</td>
-            <td class="p-2 text-base font-semibold">{{ insight.tshirts_sold }}</td>
+          <tr role="row">
+            <td class="p-2 text-base font-semibold" role="rowheader">Total</td>
+            <td class="p-2 text-base font-semibold" role="cell">{{ insight.tshirts_sold }}</td>
           </tr>
         </tbody>
       </table>
@@ -32,17 +36,18 @@
   </Dialog>
   <div class="flex flex-col w-full border rounded-sm p-4">
     <div class="text-sm flex gap-2 items-center uppercase font-semibold">
-      <IconShirtFilled class="w-5 h-5" />
+      <IconShirtFilled class="w-5 h-5" aria-hidden="true" />
       <span> T-shirts Sold </span>
     </div>
     <div class="flex gap-2 items-end justify-between mt-6">
       <div class="prose">
-        <h2 class="">{{ insight.tshirts_sold }}</h2>
+        <h2 id="total-tshirts" class="text-lg font-bold">{{ insight.tshirts_sold }}</h2>
       </div>
-      <Button class="w-fit" label="View Details" @click="showDialog = true" />
+      <Button class="w-fit" label="View Details" @click="showDialog = true" aria-labelledby="dialog-title" />
     </div>
   </div>
 </template>
+
 <script setup>
 import { Dialog } from 'frappe-ui'
 import { defineProps, ref } from 'vue'
diff --git a/fossunited/fossunited/web_template/events_timeline/events_timeline.html b/fossunited/fossunited/web_template/events_timeline/events_timeline.html
index fb183108c..134a91e52 100644
--- a/fossunited/fossunited/web_template/events_timeline/events_timeline.html
+++ b/fossunited/fossunited/web_template/events_timeline/events_timeline.html
@@ -1,8 +1,5 @@
 {% from "fossunited/templates/macros/event_card.html" import event_card %}
-{%
-  from
-  "fossunited/templates/macros/hackathon_card.html" import hackathon_card
-%}
+{% from "fossunited/templates/macros/hackathon_card.html" import hackathon_card %}
 
 <style>
   .events-timeline {
@@ -12,6 +9,18 @@
     flex-direction: column;
     gap: 4rem;
   }
+  .sr-only {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    padding: 0;
+    margin: -1px;
+    overflow: hidden;
+    clip: rect(0, 0, 0, 0);
+    white-space: nowrap;
+    border: 0;
+  }
+
   .events-search {
     display: flex;
     padding: 0.5rem;
@@ -31,8 +40,9 @@
     color: var(--Gray-500, #667085);
   }
 
-  #events-search:focus {
-    outline: none;
+  #events-search:focus, .must-attend-checkbox input:focus + label {
+    outline: 2px solid var(--Gray-500, #667085);
+    outline-offset: 2px;
   }
 
   .must-attend-checkbox {
@@ -79,84 +89,97 @@
 <div v-scope>
   <div class="container events-timeline">
     <!--Search filter panel-->
-    <div class="search-filter-controlpanel">
+    <div class="search-filter-controlpanel" role="search" aria-labelledby="search-panel-label">
+      <h2 id="search-panel-label" class="sr-only">Search and Filter Events</h2>
+
+      <!-- Search Input -->
       <div class="events-search">
+        <label for="events-search" class="sr-only">Search Events</label>
         <input
           v-model="searchText"
           type="text"
           id="events-search"
           placeholder="Search by event name and date"
+          aria-label="Search events by name and date"
         />
       </div>
 
+      <!-- Must Attend Checkbox -->
       <div class="must-attend-checkbox">
-        <input type="checkbox" v-model="mustAttendOnly" />
+        <input
+          type="checkbox"
+          v-model="mustAttendOnly"
+          id="must-attend-checkbox"
+          title="Show only must-attend events"
+          aria-describedby="must-attend-desc"
+        />
         <label for="must-attend-checkbox">Must Attend</label>
       </div>
+      <p id="must-attend-desc" class="sr-only">Filter the events to show only those marked as must-attend.</p>
     </div>
 
     <!--Primary Event cards listing-->
     {% set grouped_events = get_grouped_events() %}
     {% for group, events in grouped_events.items() %}
-      <div class="event-group" v-if="searchText.length == 0">
-        <h4>{{ group }}</h4>
+      <div class="event-group" v-if="searchText.length == 0" role="region" aria-labelledby="group-{{ loop.index }}">
+        <h3 id="group-{{ loop.index }}">{{ group }}</h3>
         {% for month, month_events in events.items() %}
-          <h5
-            class="event-month"
-            v-show="ticker > 0 && $el?.nextElementSibling.style.display != 'none'"
-          >
+          <h4 id="month-{{ loop.index }}-{{ loop.parent.index }}">
             {{ month }}
-          </h5>
+          </h4>
           <div
             class="events-grid-4"
+            role="group"
+            aria-labelledby="month-{{ loop.index }}-{{ loop.parent.index }}"
             v-effect="updateTicker()"
             v-show="ticker > 0 && $el?.children.length > 0"
           >
-            {% for event in month_events %}{% if event.event_name %}
-              <div
-                class="event-card-holder"
-                v-if="canShow('{{ event.event_name }}', '{{ event_date_text }}', !!{{ event.must_attend }})"
-              >
-                {{ event_card(event) }}
-              </div>
-            {% else %}
-              <div
-                class="event-card-holder"
-                v-if="canShow('{{ event.hackathon_name }}', '{{ event_date_text }}')"
-              >
-                {{ hackathon_card(event) }}
-              </div>
-            {% endif %}{% endfor %}
+            {% for event in month_events %}
+              {% if event.event_name %}
+                <div
+                  class="event-card-holder"
+                  role="listitem"
+                  v-if="canShow('{{ event.event_name }}', '{{ event_date_text }}', !!{{ event.must_attend }})"
+                >
+                  {{ event_card(event) }}
+                </div>
+              {% else %}
+                <div
+                  class="event-card-holder"
+                  role="listitem"
+                  v-if="canShow('{{ event.hackathon_name }}', '{{ event_date_text }}')"
+                >
+                  {{ hackathon_card(event) }}
+                </div>
+              {% endif %}
+            {% endfor %}
           </div>
         {% endfor %}
       </div>
     {% endfor %}
 
     <!--Searched and Filtered event cards listing-->
-    <div v-if="searchText.length > 0">
-      <div class="events-grid-4">
-        {% for group, events in grouped_events.items() %}{%
-          for month, month_events in
-          events.items()
-        %}
-          {% for event in month_events %}
-            {%
-              set event_date_text =
-              event.event_start_date.strftime("%d %b %Y") if event.event_start_date else
-              event.start_date.strftime("%d %b %Y")
-            %}
-            <div
-              class="event-card-holder"
-              v-if="canShow('{{ event.event_name }}', '{{ event_date_text }}', !!{{ event.must_attend }})"
-            >
-              {% if event.event_name %}
-                {{ event_card(event) }}
-              {% else %}
-                {{ hackathon_card(event) }}
-              {% endif %}
-            </div>
+    <div v-if="searchText.length > 0" aria-live="polite" role="region" aria-labelledby="filtered-events-label">
+      <h3 id="filtered-events-label" class="sr-only">Filtered Event Results</h3>
+      <div class="events-grid-4" role="list">
+        {% for group, events in grouped_events.items() %}
+          {% for month, month_events in events.items() %}
+            {% for event in month_events %}
+              {% set event_date_text = event.event_start_date.strftime("%d %b %Y") if event.event_start_date else event.start_date.strftime("%d %b %Y") %}
+              <div
+                class="event-card-holder"
+                role="listitem"
+                v-if="canShow('{{ event.event_name }}', '{{ event_date_text }}', !!{{ event.must_attend }})"
+              >
+                {% if event.event_name %}
+                  {{ event_card(event) }}
+                {% else %}
+                  {{ hackathon_card(event) }}
+                {% endif %}
+              </div>
+            {% endfor %}
           {% endfor %}
-        {% endfor %}{% endfor %}
+        {% endfor %}
       </div>
     </div>
   </div>


Reviews:
user: login: rahulporuri
body: the following code formatting issues were raised by the continuous integration job - https://github.com/fossunited/fossunited/actions/runs/13931943834/job/38990956998?pr=865 - I've recommended the fixes in the comments.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T06:09:06Z
user: login: harshtandiya
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T04:24:13Z

Review comments:
diff_hunk: @@ -12,6 +9,19 @@
     flex-direction: column;
     gap: 4rem;
   }
+  
path: fossunited/fossunited/web_template/events_timeline/events_timeline.html
user: login: rahulporuri
body: ```suggestion

```

removed empty whitespace
created_at: 2025-03-19T06:05:51Z
author_association: MEMBER
diff_hunk: @@ -79,84 +90,97 @@
 <div v-scope>
   <div class="container events-timeline">
     <!--Search filter panel-->
-    <div class="search-filter-controlpanel">
+    <div class="search-filter-controlpanel" role="search" aria-labelledby="search-panel-label">
+      <h2 id="search-panel-label" class="sr-only">Search and Filter Events</h2>
+
+      <!-- Search Input -->
       <div class="events-search">
+        <label for="events-search" class="sr-only">Search Events</label>
         <input
           v-model="searchText"
           type="text"
           id="events-search"
           placeholder="Search by event name and date"
+          aria-label="Search events by name and date"
         />
       </div>
 
+      <!-- Must Attend Checkbox -->
       <div class="must-attend-checkbox">
-        <input type="checkbox" v-model="mustAttendOnly" />
+        <input 
+          type="checkbox" 
+          v-model="mustAttendOnly" 
path: fossunited/fossunited/web_template/events_timeline/events_timeline.html
user: login: rahulporuri
body: ```suggestion
        <input
          type="checkbox"
          v-model="mustAttendOnly"
```

removed trailing whitespace
created_at: 2025-03-19T06:07:18Z
author_association: MEMBER

PR comments:
user: login: harshtandiya
created_at: 2025-03-19T06:13:17Z
body: Can we change the PR title? 
author_association: CONTRIBUTOR
user: login: harshtandiya
created_at: 2025-03-19T06:13:54Z
body: Make sure you have the pre-commit set up (follow the readme), that can help get rid of formatting issues
author_association: CONTRIBUTOR
user: login: hanthor
created_at: 2025-04-02T06:04:18Z
body: #876 
author_association: CONTRIBUTOR