Skip to content

Commit ef5cb77

Browse files
authored
Merge pull request #179 from emulsify-ds/EMULSIF-546--accessibility-tweaks
EMULSIF-546: Accessibility tweaks
2 parents 3f569c5 + a0878a9 commit ef5cb77

6 files changed

Lines changed: 109 additions & 53 deletions

File tree

Lines changed: 75 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,26 @@
11
Drupal.behaviors.accordion = {
22
attach(context) {
3-
// Selectors
43
const items = context.querySelectorAll('.js-accordion-item');
54
const controls = context.querySelectorAll('.accordion__controls__item');
6-
// Classes
75
const itemToggle = '.js-accordion-item__toggle';
86
const itemContent = '.accordion-item__content';
97
const itemState = 'data-accordion-expanded';
108
const buttonState = 'aria-expanded';
119
const contentState = 'aria-hidden';
1210

13-
// Function to expand an accordion item.
1411
const expand = (item, button, content) => {
1512
item.setAttribute(itemState, 'true');
1613
button.setAttribute(buttonState, 'true');
1714
content.setAttribute(contentState, 'false');
1815
};
1916

20-
// Function to collapse an accordion item.
2117
const collapse = (item, button, content) => {
2218
item.setAttribute(itemState, 'false');
2319
button.setAttribute(buttonState, 'false');
2420
content.setAttribute(contentState, 'true');
2521
};
2622

27-
/* eslint-disable */
28-
/**
29-
* getUrl
30-
* @description Get the value of the anchor link in the URL.
31-
* @returns The string value after the hash.
32-
*/
23+
// Get base URL and anchor (unchanged)
3324
function getUrl() {
3425
return (
3526
window.location.protocol +
@@ -38,36 +29,62 @@ Drupal.behaviors.accordion = {
3829
window.location.pathname
3930
);
4031
}
41-
/* eslint-enable */
4232

43-
/**
44-
* getAnchor
45-
* @description Get the value of the anchor link in the URL.
46-
* @return {string} The string value after the hash.
47-
*/
4833
function getAnchor() {
4934
return document.URL.split('#').length > 1
5035
? document.URL.split('#')[1]
5136
: null;
5237
}
5338

54-
// Toggle accordion content when toggle is activated.
39+
// Utility to update aria-disabled on "Expand all" / "Collapse all"
40+
const updateControlStates = (accordionRoot) => {
41+
const expandButton = accordionRoot.querySelector(
42+
'.js-accordion__toggle-all--expand',
43+
);
44+
const collapseButton = accordionRoot.querySelector(
45+
'.js-accordion__toggle-all--collapse',
46+
);
47+
const allItems = accordionRoot.querySelectorAll('.js-accordion-item');
48+
49+
const allExpanded = Array.from(allItems).every(
50+
(item) => item.getAttribute(itemState) === 'true',
51+
);
52+
const allCollapsed = Array.from(allItems).every(
53+
(item) => item.getAttribute(itemState) === 'false',
54+
);
55+
56+
// Set aria-disabled accordingly
57+
if (expandButton) {
58+
expandButton.setAttribute(
59+
'aria-disabled',
60+
allExpanded ? 'true' : 'false',
61+
);
62+
}
63+
if (collapseButton) {
64+
collapseButton.setAttribute(
65+
'aria-disabled',
66+
allCollapsed ? 'true' : 'false',
67+
);
68+
}
69+
};
70+
71+
// Initialize accordion items
5572
items.forEach((item) => {
5673
const button = item.querySelector(itemToggle);
5774
const content = item.querySelector(itemContent);
75+
const accordionRoot = item.closest('.accordion');
5876

59-
const anchor = item.id;
60-
// eslint-disable-next-line
61-
const newUrl = `${getUrl()}` + '#' + `${anchor}`;
62-
63-
// Hide all accordion content sections if JavaScript is enabled.
6477
collapse(item, button, content);
6578

6679
if (item.getAttribute('id') && item.getAttribute('id') === getAnchor()) {
6780
expand(item, button, content);
6881
}
6982

7083
button.addEventListener('click', () => {
84+
const anchor = item.id;
85+
const newUrl = `${getUrl()}#${anchor}`;
86+
87+
// Update URL hash
7188
if (window.location.href !== newUrl) {
7289
window.history.replaceState('', '', newUrl);
7390
} else {
@@ -77,39 +94,48 @@ Drupal.behaviors.accordion = {
7794
window.location.origin + window.location.pathname,
7895
);
7996
}
80-
// Toggle the item's state.
81-
return button.getAttribute(buttonState) === 'true'
82-
? collapse(item, button, content)
83-
: expand(item, button, content);
97+
98+
// Toggle state
99+
if (button.getAttribute(buttonState) === 'true') {
100+
collapse(item, button, content);
101+
} else {
102+
expand(item, button, content);
103+
}
104+
105+
// Update control buttons state after toggling
106+
updateControlStates(accordionRoot);
84107
});
85108
});
86109

110+
// Initialize control buttons (Expand All / Collapse All)
87111
controls.forEach((control) => {
88-
// Get all items relevant to the control.
89-
const allItems =
90-
control.parentNode.parentNode.querySelectorAll('.js-accordion-item');
91-
// Add click listener on the parent <ul>
92-
control
93-
.querySelector('.js-accordion-item__toggle_all')
94-
.addEventListener('click', (e) => {
95-
// Determine which control was activated. `action` will re turn a
96-
// boolean. `true` if the expand control was clicked, otherwise false.
97-
const action = e.target.classList.contains(
98-
'js-accordion__toggle-all--expand',
99-
);
112+
const accordionRoot = control.closest('.accordion');
113+
const allItems = accordionRoot.querySelectorAll('.js-accordion-item');
114+
const toggleAllButton = control.querySelector(
115+
'.js-accordion-item__toggle_all',
116+
);
117+
118+
toggleAllButton.addEventListener('click', (e) => {
119+
const action = e.target.classList.contains(
120+
'js-accordion__toggle-all--expand',
121+
);
100122

101-
// Iterate over
102-
allItems.forEach((item) => {
103-
const button = item.querySelector(itemToggle);
104-
const content = item.querySelector(itemContent);
105-
106-
if (action === false) {
107-
collapse(item, button, content);
108-
} else {
109-
expand(item, button, content);
110-
}
111-
});
123+
allItems.forEach((item) => {
124+
const button = item.querySelector(itemToggle);
125+
const content = item.querySelector(itemContent);
126+
if (action) {
127+
expand(item, button, content);
128+
} else {
129+
collapse(item, button, content);
130+
}
112131
});
132+
133+
// Refresh aria-disabled states after bulk toggle
134+
updateControlStates(accordionRoot);
135+
});
136+
137+
// Initialize on page load
138+
updateControlStates(accordionRoot);
113139
});
114140
},
115141
};

src/components/tabs/_tab-content.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
{% set tab__modifiers = tab__modifiers|default([]) %}
33

4-
<div id="{{ tab__content__id }}" {{ bem(tab__content__base_class, tab__modifiers, tab__content__blockname, [current_class])}}>
4+
<div id="{{ tab__content__id }}" {{ bem(tab__content__base_class, tab__modifiers, tab__content__blockname, [current_class])}} role="tabpanel" aria-labelledby="{{ tab__id }}">
55
{% block tab__content__components %}
66
{{ tab__content }}
77
{% endblock %}

src/components/tabs/_tab-label.twig

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@
2525
{# if Storybook atoms example, add ul wrapper #}
2626
<li{{attributes}} {{ bem(tab__base_class, tab__modifiers, tab__blockname) }}>
2727
{% set current_class = key == 0 ? ' is-active' : '' %}
28+
{% set is_selected = key == 0 %}
2829
{% if tab__icon %}
29-
<a href="{{ tab__link }}" id="{{ tab__id }}" {{ bem('link', [], tab__base_class, [current_class])}}>
30+
<a href="{{ tab__link }}" id="{{ tab__id }}" {{ bem('link', [], tab__base_class, [current_class])}} role="tab" aria-selected="{{ is_selected }}" tabindex="0">
3031
<span {{ bem('text', [], tab__base_class, [current_class])}}>{{ tab__label }}</span>
3132
</a>
3233
{% else %}
33-
<a href="{{ tab__link }}" id="{{ tab__id }}" {{ bem('link', [], tab__base_class, [current_class])}}>
34+
<a href="{{ tab__link }}" id="{{ tab__id }}" {{ bem('link', [], tab__base_class, [current_class])}} role="tab" aria-selected="{{ is_selected }}" tabindex="0">
3435
<span {{ bem('text', [], tab__base_class, [current_class])}}>{{ tab__label }}</span>
3536
</a>
3637
{% endif %}

src/components/tabs/tabs.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,15 @@ Drupal.behaviors.tabs = {
5959
tabNavigationLinks[Number(activeIndex)].classList.remove(
6060
'is-active',
6161
);
62+
tabNavigationLinks[Number(activeIndex)].setAttribute(
63+
'aria-selected',
64+
'false',
65+
);
6266
tabNavigationLinks[Number(index)].classList.add('is-active');
67+
tabNavigationLinks[Number(index)].setAttribute(
68+
'aria-selected',
69+
'true',
70+
);
6371
tabContentContainers[Number(activeIndex)].classList.remove(
6472
'is-active',
6573
);
@@ -279,6 +287,25 @@ Drupal.behaviors.tabsNavScroll = {
279287
mouseNav('left');
280288
}
281289
});
290+
291+
// Make scroll buttons accessible
292+
control.removeAttribute('aria-hidden'); // Remove hidden for screen readers
293+
control.setAttribute('tabindex', '0'); // Make keyboard focusable
294+
control.setAttribute('role', 'button'); // Explicit role
295+
control.setAttribute(
296+
'aria-label',
297+
control.classList.contains('tabs__scroll--right')
298+
? 'Scroll Right'
299+
: 'Scroll Left',
300+
);
301+
302+
// Keyboard support
303+
control.addEventListener('keydown', (e) => {
304+
if (e.key === 'Enter' || e.key === ' ') {
305+
e.preventDefault();
306+
control.click(); // Trigger existing click behavior
307+
}
308+
});
282309
});
283310

284311
tabsNav.addEventListener('scroll', () => {

src/components/tabs/tabs.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
opacity: 0;
7575
visibility: hidden;
7676
padding: 0;
77+
cursor: pointer;
7778
}
7879

7980
&--left {

src/components/tabs/tabs.twig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
<div {{ bem(tabs__base_class, tabs__modifiers, tabs__blockname, ['no-js']) }} data-tabs-display="{{ tabs__display }}">
7676
<div {{ bem('wrapper', tabs__modifiers, tabs__base_class) }}>
7777
{{ scroll_left }}
78-
<ul {{ bem('nav', [], tabs__base_class) }}>
78+
<ul {{ bem('nav', [], tabs__base_class) }} role="tablist">
7979
{% for key, tab in tabs %}
8080
{% set tab__name = tab.tab__label|lower|replace({' ': '-', '&': 'and'}) ~ '-' ~ key %}
8181
{% include "@components/tabs/_tab-label.twig" with {
@@ -102,6 +102,7 @@
102102
tab__blockname: tabs__base_class,
103103
tab__content__id: '#' ~ tab__name,
104104
tab__content: tab.tab__content,
105+
tab__id: tab__name ~ '-tab',
105106
} %}
106107
{% endfor %}
107108
{% endblock %}

0 commit comments

Comments
 (0)